2013-07-11 2 views
10

Мне задали этот вопрос в интервью. Интервьюер хотел знать, как сделать объект неизменным. и затем он спросил, что, если я сериализую этот объект - будет ли он нарушать неизменность? Если да, как я могу это предотвратить? Может ли кто-нибудь помочь мне понять это?Как можно сериализовать/десериализовать неизменность?

+0

Я не уверен на 100%, я никогда раньше не ставил слишком много времени на сериализацию, но это может иметь отношение к ключевому слову 'transient'. Если у вас есть, скажем, экземпляр MouseListener, преходящий в неизменяемом классе, путем сериализации, а затем де-сериализации, вы можете очень мутировать класс, модифицируя экземпляр MouseListener. –

+2

@Daft Punk - ключевое слово Transient используется для указания того, что атрибут не обязательно должен быть сериализован. – DRastislav

+1

Я бы сказал, что сериализация и десериализация создают новый объект, поэтому не влияют на неизменность. – parsifal

ответ

9

Неизменяемый объект - тот, который нельзя изменить после его создания. Вы можете создать такой объект, используя модификаторы доступа private и ключевое слово final.

Если неизменяемый объект был сериализован, его необработанные байты могут быть изменены так, что при десериализации объект перестает быть тем же.

Этого нельзя полностью предотвратить. Шифрование, контрольные суммы и CRC помогут предотвратить это.

+3

'private' на самом деле не требуется. 'Public final String foo =" foo "' не сделает класс изменчивым. Это просто плохая инкапсуляция. – zapl

+0

в этом конкретном случае нет. но это только потому, что сам объект String неизменен.Я чувствую, что по-настоящему неизменяемый объект не позволит получить доступ к его внутренним данным, если эти объекты также не являются неизменными. –

+0

зависит от вашего определения. Предположим, здесь измененный означает, что объект нельзя переназначить или его внутреннее состояние не может быть изменилось? –

4

Когда вы сериализуете граф объектов, который имеет несколько ссылок на один и тот же объект, сериализатор отмечает этот факт, так что граф десериализованного объекта имеет одинаковую структуру.

Например,

int[] none = new int[0]; 
int[][] twoArrays = new int[] { none, none }; 
System.out.print(twoArrays[0] == twoArrays[1]); 

напечатает true, и если вы сериализации и десериализации twoArrays тогда вы получите тот же результат, вместо того, чтобы каждый элемент массива, являющегося другой объект, как в

int[][] twoDistinctArrays = new int[] { new int[0], new int[0] }; 

Вы можете использовать эту поддержку для обмена ссылками для управления байтами после сериализованной записи, чтобы поделиться ссылкой с частным объектом или массивом справки, а затем изменить его.

Таким образом, несериализуемый класс может поддерживать инварианты - что частный объект не убегает - что сериализуемый класс не может поддерживать.

+1

Обычная защита от этого заключается в реализации защищенного кода 'readObject()' метода, который делает новые объекты этих десериализованных (чтобы изменить используемые ссылки). Например, если у объекта было поле «Integer» с именем 'id', ваш метод' readObject() 'имел бы идентификатор id = new Integer (id)'. См. Следующие две минуты http://video.javazone.no/talk/49302113 вовремя 21:21, чтобы узнать больше. –

+1

Вы также можете предотвратить наложение псевдонимов с помощью метода 'ObjectInputStream.readUnshared()'. Этого недостаточно для обеспечения безопасности; как заметил Блох в * Эффективной Java *, в сериализованном файле кто-то может многое изменить, чтобы изменить десериализованную версию и испортить инварианты. Хуже того, кто-то может заменить подкласс для класса. Вы сериализуете 'java.util.Date', а кто-то заменяет его подклассом' my.bad.Date', который делает неприятные вещи при доступе. –

+1

@Slanec, Спасибо за указатель на этот разговор. Да, вы можете решить проблемы безопасности с сериализацией Java, переопределив большие куски, но когда мне нужно безопасно преобразовывать байты в объекты, я просто отказываюсь от встроенной сериализации Java и вместо этого использую генератор парсера, чтобы я мог статически связать методы который может быть вызван в результате разбора. –

1

Сделайте его неизменным, сохраняя всю информацию о состоянии в форме, где она не может быть изменена после создания объекта.

В некоторых случаях Java не допускает идеальной неизменности.

Serializable - это то, что вы можете сделать, но это не идеально, потому что должен быть способ воссоздать точную копию объекта при десериализации, и может быть недостаточно использовать те же конструкторы для десериализации и создания объект в первую очередь. Это оставляет дыру.

Некоторые вещи, чтобы сделать:

  • ничего, кроме частных или конечных свойств.
  • Конструктор устанавливает любое из тех свойств, которые имеют решающее значение для работы.

Некоторые другие вещи, чтобы думать:

  • статические переменные, вероятно, плохая идея, хотя статическая окончательная константа не является проблемой. Невозможно установить их извне при загрузке класса, но не удалять их позже.
  • Если одно из свойств, переданных конструктору, является объектом, вызывающий может хранить ссылку на этот объект и, если он также не является неизменным, изменить некоторое внутреннее состояние этого объекта.Это эффективно изменяет внутреннее состояние вашего объекта, который сохранил копию этого, теперь измененного объекта.
  • Кто-то может теоретически взять сериализованную форму и изменить ее (или просто построить сериализованную форму с нуля), а затем использовать ее для десериализации, создав таким образом модифицированную версию объекта. (Я полагаю, что это, вероятно, не стоит беспокоиться в большинстве случаев.)
  • вы можете написать собственный код сериализации/десериализации, который подписывает сериализованную форму (или шифрует ее), чтобы изменения были обнаружены. Или вы можете использовать некоторую форму передачи сериализованной формы, которая гарантирует, что она не будет изменена. (Это предполагает, что у вас есть некоторый контроль над сериализованной формой, когда он не находится в пути.)
  • Существуют манипуляторы байтового кода, которые могут делать все, что захотят. Например, добавьте метод setter в неизменяемый объект.

Простой ответ заключается в том, что в большинстве случаев просто следуйте двум правилам в верхней части этого ответа, и это будет достаточно хорошо, чтобы удовлетворить ваши потребности в неизменности.

4

Вы должны прочитать эффективную Java, написанную Джошуа Блохом. Существует целая глава о проблемах безопасности, связанных с сериализацией, и советы по правильному дизайну вашего класса.

В нескольких словах: вы должны узнать о методах readObject и readResolve.

Подробный ответ: Да, сериализация может нарушить неизменность.

Давайте предположим, что у вас есть класс Период (это пример из книги Иисуса Навина):

private final class Period implements Serializable { 
    private final Date start; 
    private final Date end; 

public Period(Date start, Date end){ 
    this.start = new Date(start.getTime()); 
    this.end = new Date(end.getTime()); 
    if(this.start.compareTo(this.end() > 0) 
     throw new IllegalArgumentException("sth"); 
} 
//getters and others methods ommited 
} 

Это выглядит здорово. Это неизменная (вы не можете изменить начало и конец после инициализации), элегантный, маленький, поточно т.д.

Но ...

Вы должны помнить, что сериализации это еще один способ создания объектов (и это не используя конструкторы). Объекты строятся из байтового потока.

Рассмотрите сценарий, когда кто-то (атакующий) изменит ваш массив байтов сериализации. Если он делает такую ​​вещь, он может нарушить ваше состояние о начале < конца. Кроме того, есть вероятность, что злоумышленник передаст в поток (переданный методу десериализации) ссылку на свой объект Date (который изменен, а неизменность периода будет полностью разрушена).

Лучшая защита не использует сериализацию, если вам не нужно. Если вам нужно сериализовать свой класс, используйте шаблон прокси-сервера Serialization.

Редактировать (по запросу kurzbot): Если вы хотите использовать прокси-сервер Serialization, вы должны добавить статический внутренний класс внутри Периода. Эти объекты класса будут использоваться для сериализации вместо объектов класса Period.

В классе Период написать два новых метода:

private Object writeReplace(){ 
    return new SerializationProxy(this); 
} 

private void readObject(ObjectInputStream stream) throws InvalidObjectException { 
    throw new InvalidObjectException("Need proxy"); 
} 

Первый метод замены по умолчанию объект сериализованная Период с объектом SerializationProxy. Вторая гарантия того, что злоумышленник не будет использовать стандартный метод readObject.

Вы должны написать writeObject метод SerializationProxy так что вы можете использовать:

private Object readResolve() { 
    return new Period(start, end); 
} 

В этом случае вы используете только публичный API и есть уверенность в том, что класс Период будет оставаться непреложно.

+0

Можете ли вы рассказать о том, как шаблон прокси-сервера Serialization разрешит эту проблему? Я не уверен, что понимаю, как он защищается от сценария, который вы дали. – kurtzbot

+0

Я добавил несколько объяснений. –

1

Как уже говорилось, можно утверждать, что сериализация приводит к созданию совершенно нового объекта, который тогда неизменен, поэтому нет, сериализация не нарушает его, но я думаю, что есть большая картина неизменности, которую мы должны прежде чем отвечать на этот вопрос.

Я думаю, что реальный ответ полностью зависит от сериализуемого класса и требуемого уровня неизменности, но поскольку интервьюер не дал нам исходного кода, я придумаю свой собственный. Я также хотел бы отметить, что, как только люди начинают говорить о неизменности, они начинают метаться по ключевому слову final - да, это делает ссылку неизменной, но это не единственный способ добиться неизменности. Хорошо, давайте посмотрим на код:

public class MyImmutableClass implements Serializable{ 
    private double value; 

    public MyImmutableClass(double v){ 
     value = v; 
    } 

    public double getValue(){ return value; } 
} 

Является ли этот класс изменчивый, потому что я реализовал Serializable? Это изменчиво, потому что я не использовал ключевое слово final? Ни в коем случае - это неизменно в каждом практическом смысле этого слова, потому что я не буду изменять исходный код (даже если вы попросите меня красиво), но что более важно, он неизменен, потому что никакой внешний класс не может изменить значение value , за исключением использования Reflection, чтобы сделать его общедоступным, а затем его изменить. Под этим маркером, я полагаю, вы могли бы запустить некоторый промежуточный шестнадцатеричный редактор и вручную изменить значение в ОЗУ тоже, но это не делает его более изменчивым, чем раньше. Расширение классов также не может изменить его. Конечно, вы можете расширить его, а затем переопределить getValue(), чтобы вернуть что-то другое, но при этом не будет изменяться базовый value.

Я знаю, что это может потревожить много людей неправильным образом, но я считаю, что неизменяемость часто является чисто смысловой - например, является ли он неизменным для кого-то, вызывающего ваш код из внешнего класса, или он неизменен от кого-то, использующего BusPirate на вашей материнской плате? Есть ОЧЕНЬ веские причины использовать final, чтобы помочь обеспечить неизменность, но я думаю, что это значение значительно преувеличено более чем в нескольких аргументах. Просто потому, что JVM разрешено делать какую-то магию под капотом, чтобы гарантировать, что работа по сериализации не означает, что уровень неизменности, который требует ваше приложение, как-то сломается.

0

Грязи-простой ответ

class X implements Serializable { 
    private final transient String foo = "foo"; 
} 

Поле Foo будет равен «Foo», если объект создается новое, но будет нулевым, когда десериализации (и не прибегая к грязным трюкам, вы не будете иметь возможность назначить его).

+0

Правильно! Вы не будете разрушать неизменность при сериализации объектов, если вы никогда не сериализуете внутренние данные. Хотя это правда, это не очень полезно. –

Смежные вопросы