Сериализация Java: readObject () против readResolve ()

127

Книга « Эффективная Java» и другие источники дают довольно хорошее объяснение того, как и когда использовать метод readObject () при работе с сериализуемыми классами Java. С другой стороны, метод readResolve () остается загадкой. Практически все документы, которые я нашел, либо упоминают только один из двух, либо упоминают оба только по отдельности.

Вопросы, которые остались без ответа:

  • В чем разница между двумя методами?
  • Когда какой метод следует применять?
  • Как следует использовать readResolve (), особенно с точки зрения возврата чего?

Надеюсь, вы сможете пролить свет на этот вопрос.

фураж
источник
Пример из Oracle JDK:String.CaseInsensitiveComparator.readResolve()
kevinarpe

Ответы:

138

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

Майкл Майерс
источник
3
Есть несколько способов обойти это вредоносный код (или даже данные).
Tom Hawtin - tackline
6
Джош Блох говорит об условиях, при которых это ломается, в эффективной Java 2-е изд. Пункт 77. Он упоминает об этом в
своем
17
Я считаю этот ответ несколько неадекватным, поскольку в нем не упоминаются transientполя. readResolveиспользуется для разрешения объекта после его чтения. Пример использования: возможно, объект содержит некоторый кеш, который может быть воссоздан из существующих данных и не требует сериализации; кэшированные данные могут быть объявлены transientи readResolve()могут быть восстановлены после десериализации. Для подобных вещей и предназначен этот метод.
Jason C
2
@JasonC ваш комментарий , что « такие вещи , как , что [транзиторной обработка] то , что этот метод для » вводит в заблуждение. См. Документ Java, где Serializableговорится: «Классы, которым необходимо назначить замену, когда ее экземпляр считывается из потока, должны реализовывать этот [ readResolve] специальный метод ...».
Opher,
2
Метод readResolve также можно использовать в крайнем случае, когда предположим, что вы сериализовали множество объектов и сохранили их в базе данных. Если позже вы захотите перенести эти данные в новый формат, вы можете легко добиться этого с помощью метода readResolve.
Нилеш Раджани
29

Пункт 90, Эффективная Java, 3-е издание охватывает readResolveи writeReplaceдля последовательных прокси - их основное применение. Примеры не записывают readObjectи writeObjectметоды, потому что они используют сериализацию по умолчанию для чтения и записи полей.

readResolveвызывается после readObjectвозврата (и наоборот, writeReplaceвызывается до writeObjectи, вероятно, для другого объекта). Объект, возвращаемый методом, заменяет thisобъект, возвращаемый пользователю, ObjectInputStream.readObjectи любые дальнейшие обратные ссылки на объект в потоке. Оба readResolveи writeReplaceмогут возвращать объекты одного или разных типов. Возвращение того же типа полезно в некоторых случаях, когда должны быть поля finalи либо требуется обратная совместимость, либо значения должны быть скопированы и / или проверены.

Использование readResolveне приводит к принудительному применению свойства singleton.

Том Хотин - tackline
источник
9

readResolve можно использовать для изменения данных, сериализованных с помощью метода readObject. Например, xstream API использует эту функцию для инициализации некоторых атрибутов, которые не были в XML для десериализации.

http://x-stream.github.io/faq.html#Serialization

бесконечный
источник
1
XML и Xstream не имеют отношения к вопросу о сериализации Java, и много лет назад на этот вопрос был дан правильный ответ. -1
Marquis of Lorne
5
В принятом ответе указано, что readResolve используется для замены объекта. Этот ответ предоставляет полезную дополнительную информацию, которую можно использовать для изменения объекта во время десериализации. XStream был приведен как пример, а не как единственная возможная библиотека, в которой это происходит.
Enwired
5

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

Pr0methean
источник
readResolve () был мне понятен, но у меня все еще есть некоторые необъяснимые вопросы, но ваш ответ просто прочитал мои мысли, спасибо
Раджни Гангвар
5

readObject () - это существующий метод в классе ObjectInputStream. во время чтения объекта во время десериализации метод readObject внутренне проверяет, имеет ли десериализуемый объект класса метод readResolve или нет, если метод readResolve существует, тогда он вызовет метод readResolve и вернет тот же пример.

Таким образом, цель написания метода readResolve - это хорошая практика для достижения чистого одноэлементного шаблона проектирования, когда никто не может получить другой экземпляр путем сериализации / десериализации.

Арджун Кумар Мехта
источник
3

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

Канагавелу Сугумар
источник
2

Когда сериализация используется для преобразования объекта, чтобы его можно было сохранить в файле, мы можем запустить метод readResolve (). Метод является частным и хранится в том же классе, объект которого извлекается во время десериализации. Это гарантирует, что после десериализации возвращаемый объект будет таким же, как был сериализован. То есть,instanceSer.hashCode() == instanceDeSer.hashCode()

readResolve () не является статическим методом. After in.readObject()вызывается во время десериализации, он просто проверяет, что возвращаемый объект такой же, как и тот, который был сериализован, как показано ниже, в то время какout.writeObject(instanceSer)

..
    ObjectOutput out = new ObjectOutputStream(new FileOutputStream("file1.ser"));
    out.writeObject(instanceSer);
    out.close();

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

public static ABCSingleton getInstance(){
    return ABCSingleton.instance; //instance is static 
}
hi.nitish
источник
1

Я знаю, что этот вопрос действительно старый и на него есть принятый ответ, но, поскольку он всплывает очень высоко в поиске Google, я думал, что взвесу, потому что не предоставленный ответ охватывает три случая, которые я считаю важными - на мой взгляд, основное использование для них методы. Конечно, все предполагают, что на самом деле существует необходимость в настраиваемом формате сериализации.

Взять, к примеру, классы коллекций. Сериализация связанного списка или BST по умолчанию приведет к огромной потере места с очень небольшим увеличением производительности по сравнению с простой сериализацией элементов по порядку. Это еще более верно, если коллекция является проекцией или представлением - хранит ссылку на более крупную структуру, чем она предоставляет своим общедоступным API.

  1. Если в сериализованном объекте есть неизменяемые поля, требующие настраиваемой сериализации, исходного решения writeObject/readObjectнедостаточно, поскольку десериализованный объект создается до чтения записанной части потока writeObject. Возьмем эту минимальную реализацию связанного списка:

    public class List<E> extends Serializable {
        public final E head;
        public final List<E> tail;
    
        public List(E head, List<E> tail) {
            if (head==null)
                throw new IllegalArgumentException("null as a list element");
            this.head = head;
            this.tail = tail;
        }
    
        //methods follow...
    }

Эта структура может быть сериализована путем рекурсивного написания headполя каждой ссылки, за которым следует nullзначение. Однако десериализация такого формата становится невозможной: readObjectневозможно изменить значения полей-членов (теперь фиксированные null). Вот пара writeReplace/ readResolve:

private Object writeReplace() {
    return new Serializable() {
        private transient List<E> contents = List.this;

        private void writeObject(ObjectOutputStream oos) {
            List<E> list = contents;
            while (list!=null) {
                oos.writeObject(list.head);
                list = list.tail;
            }
            oos.writeObject(null);
        }

        private void readObject(ObjectInputStream ois) {
            List<E> tail = null;
            E head = ois.readObject();
            if (head!=null) {
                readObject(ois); //read the tail and assign it to this.contents
                this.contents = new List<>(head, this.contents)
            }                     
        }


        private Object readResolve() {
            return this.contents;
        }
    }
}

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

  1. Возможно, мы захотим десериализовать объект другого класса, чем мы написали в ObjectOutputStream. Это может быть в случае с представлениями, такими как java.util.Listреализация списка, которая предоставляет фрагмент из более длинного ArrayList. Очевидно, сериализация всего списка поддержки - плохая идея, и мы должны записывать только элементы из просматриваемого фрагмента. Однако зачем останавливаться на этом и иметь бесполезный уровень косвенного обращения после десериализации? Мы могли бы просто прочитать элементы из потока в ArrayListи вернуть их напрямую, вместо того, чтобы заключать их в наш класс представления.

  2. В качестве альтернативы выбор конструкции может быть аналогичным классом делегата, предназначенным для сериализации. Хорошим примером может быть повторное использование нашего кода сериализации. Например, если у нас есть класс построителя (аналогичный StringBuilder для String), мы можем написать делегат сериализации, который сериализует любую коллекцию, записывая в поток пустой построитель, за которым следуют размер коллекции и элементы, возвращаемые итератором коллекции. Десериализация будет включать чтение построителя, добавление всех впоследствии прочитанных элементов и возврат результата final build()от делегатов readResolve. В этом случае нам нужно будет реализовать сериализацию только в корневом классе иерархии коллекции, и не потребуется дополнительный код из текущих или будущих реализаций, при условии, что они реализуют абстрактные iterator()иbuilder()метод (последний для воссоздания однотипной коллекции, что само по себе было бы очень полезной функцией). Другим примером может быть иерархия классов, код которой мы не полностью контролируем - наши базовые классы из сторонней библиотеки могут иметь любое количество частных полей, о которых мы ничего не знаем и которые могут меняться от одной версии к другой, нарушая наши сериализованные объекты. В этом случае было бы безопаснее записать данные и перестроить объект вручную при десериализации.

Турин
источник
0

Метод readResolve

Для классов Serializable и Externalizable метод readResolve позволяет классу заменять / разрешать объект, считанный из потока, прежде чем он будет возвращен вызывающей стороне. Реализуя метод readResolve, класс может напрямую управлять типами и экземплярами своих десериализуемых экземпляров. Метод определяется следующим образом:

ANY-ACCESS-MODIFIER Объект readResolve () выдает исключение ObjectStreamException;

Метод readResolve вызывается, когда ObjectInputStream считывает объект из потока и готовится вернуть его вызывающей стороне. ObjectInputStream проверяет, определяет ли класс объекта метод readResolve. Если метод определен, вызывается метод readResolve, чтобы позволить объекту в потоке обозначить возвращаемый объект. Возвращаемый объект должен иметь тип, совместимый с любым использованием. Если он несовместим, при обнаружении несоответствия типов будет выброшено исключение ClassCastException .

Например, можно создать класс Symbol, для которого на виртуальной машине существовал только один экземпляр каждой привязки символа. Метод readResolve будет реализован, чтобы определить, был ли этот символ уже определен, и заменить существующий эквивалентный объект Symbol, чтобы сохранить ограничение идентичности. Таким образом, уникальность объектов Symbol может сохраняться в процессе сериализации.

Анкуш Сони
источник
0

Как уже было сказано, readResolveэто частный метод, используемый в ObjectInputStream при десериализации объекта. Это вызывается непосредственно перед возвратом фактического экземпляра. В случае синглтона здесь мы можем принудительно вернуть уже существующую ссылку на экземпляр синглтона вместо ссылки на десериализованный экземпляр. То же самое и с writeReplaceObjectOutputStream.

Пример для readResolve:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SingletonWithSerializable implements Serializable {
private static final long serialVersionUID = 1L;

public static final SingletonWithSerializable INSTANCE = new SingletonWithSerializable();

private SingletonWithSerializable() {
    if (INSTANCE != null)
        throw new RuntimeException("Singleton instance already exists!");
}

private Object readResolve() {
    return INSTANCE;
}

public void leaveTheBuilding() {
    System.out.println("SingletonWithPublicFinalField.leaveTheBuilding() called...");
}

public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
    SingletonWithSerializable instance = SingletonWithSerializable.INSTANCE;

    System.out.println("Before serialization: " + instance);

    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("file1.ser"))) {
        out.writeObject(instance);
    }

    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("file1.ser"))) {
        SingletonWithSerializable readObject = (SingletonWithSerializable) in.readObject();
        System.out.println("After deserialization: " + readObject);
    }

}

}

Вывод:

Before serialization: com.ej.item3.SingletonWithSerializable@7852e922
After deserialization: com.ej.item3.SingletonWithSerializable@7852e922
Omkar
источник