Serializacja Java: readObject () vs. readResolve ()

129

Książka Efektywna Java i inne źródła dostarczają całkiem dobrego wyjaśnienia, jak i kiedy używać metody readObject () podczas pracy z serializowalnymi klasami Java. Z drugiej strony metoda readResolve () pozostaje nieco tajemnicą. Zasadniczo wszystkie dokumenty, które znalazłem, albo wspominają tylko o jednym z nich, albo wspominają o obu tylko indywidualnie.

Pytania, które pozostają bez odpowiedzi, to:

  • Jaka jest różnica między tymi dwiema metodami?
  • Kiedy należy wdrożyć jaką metodę?
  • Jak powinno się używać readResolve (), zwłaszcza jeśli chodzi o zwracanie czego?

Mam nadzieję, że możesz rzucić trochę światła na tę sprawę.

Pasza
źródło
1
Przykład z Oracle JDK:String.CaseInsensitiveComparator.readResolve()
kevinarpe

Odpowiedzi:

142

readResolvesłuży do zamiany obiektu odczytanego ze strumienia. Jedynym zastosowaniem, jakie kiedykolwiek widziałem, jest wymuszanie singletonów; kiedy obiekt jest odczytywany, zamień go na pojedynczą instancję. Gwarantuje to, że nikt nie może utworzyć innego wystąpienia przez serializację i deserializację singletona.

Michael Myers
źródło
3
Istnieje wiele sposobów obejścia tego problemu przez złośliwy kod (lub nawet dane).
Tom Hawtin - tackline
6
Josh Bloch opowiada o warunkach, w jakich dochodzi do tego w efektywnej Javie wyd. Punkt 77. Wspomina o tym w swoim wystąpieniu, które wygłosił w Google IO kilka lat temu (czasami pod koniec wykładu): youtube.com/watch?v=pi_I7oD_uGI
calvinkrishy
18
Uważam, że ta odpowiedź jest nieco nieodpowiednia, ponieważ nie wspomina o transientpolach. readResolvesłuży do rozwiązywania obiektu po jego przeczytaniu. Przykładowym zastosowaniem jest prawdopodobnie obiekt przechowujący pamięć podręczną, którą można odtworzyć na podstawie istniejących danych i nie trzeba go serializować; dane z pamięci podręcznej można zadeklarować transienti readResolve()odbudować po deserializacji. Do takich rzeczy służy ta metoda.
Jason C
2
@JasonC Twój komentarz, że „Do takich rzeczy [obsługa przejściowa] służy ta metoda ” jest mylący. Zobacz dokumentację Java Serializable: jest napisane, że "Klasy, które muszą wyznaczyć zamiennik, gdy ich instancja jest odczytywana ze strumienia, powinny implementować tę [ readResolve] specjalną metodę ...".
Opher
2
Metoda readResolve może być również używana w przypadku narożnika, w którym załóżmy, że serializowałeś wiele obiektów i zapisałeś je w bazie danych. Jeśli w późniejszym czasie zechcesz przenieść te dane do nowego formatu, możesz to łatwo osiągnąć w metodzie readResolve.
Nilesh Rajani
30

Pozycja 90, Effective Java, 3rd Ed obejmuje readResolvei writeReplacedla serwerów proxy szeregowych - ich główne zastosowanie. W przykładach nie są zapisywane readObjecti writeObjectmetody, ponieważ używają one domyślnej serializacji do odczytu i zapisu pól.

readResolvejest wywoływana po readObjectpowrocie (odwrotnie writeReplacejest wywoływana przed writeObjecti prawdopodobnie na innym obiekcie). Obiekt zwracany przez metodę zastępuje thisobiekt zwrócony do użytkownika ObjectInputStream.readObjectoraz wszelkie dalsze odwołania wsteczne do obiektu w strumieniu. Obie readResolvei writeReplacemogą zwracać obiekty tego samego lub różnych typów. Zwracanie tego samego typu jest przydatne w niektórych przypadkach, gdy pola muszą być finali albo wymagana jest zgodność z poprzednimi wersjami, albo wartości muszą zostać skopiowane i / lub sprawdzone.

Użycie readResolvenie wymusza własności singleton.

Tom Hawtin - haczyk
źródło
9

readResolve może służyć do zmiany danych, które są serializowane za pomocą metody readObject. Na przykład xstream API używa tej funkcji do inicjalizacji niektórych atrybutów, których nie ma w XML do deserializacji.

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

nieskończony
źródło
1
XML i Xstream nie odnoszą się do pytania o serializację w Javie, a odpowiedź na to pytanie wiele lat temu była poprawna. -1
Markiz Lorne
5
Zaakceptowana odpowiedź stwierdza, że ​​readResolve służy do zamiany obiektu. Ta odpowiedź zawiera przydatne dodatkowe informacje, których można użyć do zmodyfikowania obiektu podczas deserializacji. XStream podano jako przykład, a nie jako jedyną możliwą bibliotekę, w której to się dzieje.
Enwired
6

readObject () jest metodą istniejącą w klasie ObjectInputStream. podczas odczytu obiektu w czasie deserializacji metoda readObject wewnętrznie sprawdza, czy obiekt klasy, który jest deserializowany z metodą readResolve, czy nie, jeśli istnieje metoda readResolve, to wywoła metodę readResolve i zwróci to samo instancja.

Tak więc intencja pisania metody readResolve jest dobrą praktyką w celu uzyskania czystego wzorca projektowania singleton, w którym nikt nie może uzyskać innego wystąpienia przez serializację / deserializację.

arjun kumar mehta
źródło
5

readResolve służy do sytuacji, gdy może być konieczne zwrócenie istniejącego obiektu, np. ponieważ sprawdzasz, czy nie ma zduplikowanych danych wejściowych, które powinny zostać scalone, lub (np. w ostatecznie spójnych systemach rozproszonych), ponieważ jest to aktualizacja, która może pojawić się, zanim się o tym dowiesz wszelkie starsze wersje.

Pr0methean
źródło
readResolve () było dla mnie jasne, ale wciąż mam w głowie kilka niewyjaśnionych pytań, ale twoja odpowiedź po prostu przeczytaj mi w myślach, dzięki
Rajni Gangwar
3

Funkcja readResolve () zapewni pojedynczy kontrakt podczas serializacji.
Proszę odnieść się

Kanagavelu Sugumar
źródło
2

Gdy serializacja jest używana do konwersji obiektu, aby można go było zapisać w pliku, możemy wyzwolić metodę readResolve (). Metoda jest prywatna i jest przechowywana w tej samej klasie, której obiekt jest pobierany podczas deserializacji. Zapewnia, że ​​po deserializacji zwracany obiekt jest taki sam, jak został serializowany. To jest,instanceSer.hashCode() == instanceDeSer.hashCode()

Metoda readResolve () nie jest metodą statyczną. After in.readObject()jest wywoływane podczas deserializacji, po prostu upewnia się, że zwrócony obiekt jest taki sam jak ten, który został serializowany jak poniżej whileout.writeObject(instanceSer)

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

W ten sposób pomaga również w implementacji pojedynczego wzorca projektowego , ponieważ za każdym razem zwracana jest ta sama instancja.

public static ABCSingleton getInstance(){
    return ABCSingleton.instance; //instance is static 
}
hi.nitish
źródło
1

Wiem, że to pytanie jest naprawdę stare i ma akceptowaną odpowiedź, ale ponieważ pojawia się bardzo wysoko w wyszukiwarce Google, pomyślałem, że zważę, ponieważ żadna udzielona odpowiedź nie obejmuje trzech przypadków, które uważam za ważne - moim zdaniem ich podstawowe zastosowanie metody. Oczywiście wszyscy zakładają, że w rzeczywistości istnieje potrzeba niestandardowego formatu serializacji.

Weźmy na przykład klasy kolekcji. Domyślna serializacja połączonej listy lub BST spowodowałaby ogromną utratę miejsca z bardzo małym wzrostem wydajności w porównaniu do zwykłej serializacji elementów w kolejności. Jest to jeszcze bardziej prawdziwe, jeśli kolekcja jest projekcją lub widokiem - zachowuje odwołanie do większej struktury niż ujawnia jej publiczny interfejs API.

  1. Jeśli serializowany obiekt ma niezmienne pola, które wymagają niestandardowej serializacji, oryginalne rozwiązanie writeObject/readObjectjest niewystarczające, ponieważ obiekt deserializowany jest tworzony przed odczytaniem części strumienia zapisanego w writeObject. Weź tę minimalną implementację połączonej listy:

    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...
    }
    

Ta struktura może być serializowana przez rekursywne zapisywanie headpola każdego linku, po którym następuje nullwartość. Deserializacja takiego formatu staje się jednak niemożliwa: readObjectnie można zmienić wartości pól składowych (teraz naprawiono null). Oto writeReplace/ readResolvepara:

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;
        }
    }
}

Przepraszam, jeśli powyższy przykład nie kompiluje się (lub nie działa), ale mam nadzieję, że wystarczy, aby zilustrować mój punkt widzenia. Jeśli uważasz, że jest to bardzo daleko idący przykład, pamiętaj, że w JVM działa wiele języków funkcjonalnych i takie podejście staje się w ich przypadku niezbędne.

  1. Możemy chcieć faktycznie deserializować obiekt innej klasy niż napisaliśmy do ObjectOutputStream. Byłoby tak w przypadku widoków, takich jak java.util.Listimplementacja listy, która eksponuje wycinek z dłuższego ArrayList. Oczywiście serializacja całej listy zapasowej jest złym pomysłem i powinniśmy zapisywać tylko elementy z oglądanego wycinka. Po co jednak na tym poprzestać i mieć bezużyteczny poziom pośrednictwa po deserializacji? Moglibyśmy po prostu wczytać elementy ze strumienia do pliku ArrayListi zwrócić go bezpośrednio, zamiast zawijać go w naszej klasie widoku.

  2. Alternatywnie posiadanie podobnej klasy delegata poświęconej serializacji może być wyborem projektu. Dobrym przykładem byłoby ponowne użycie naszego kodu serializacji. Na przykład, jeśli mamy klasę konstruktora (podobną do StringBuilder for String), możemy napisać delegata serializacji, który serializuje dowolną kolekcję, pisząc do strumienia pustego konstruktora, po którym następuje rozmiar kolekcji i elementy zwracane przez iterator kolekcji. Deserializacja obejmowałaby odczytanie konstruktora, dołączenie wszystkich później odczytanych elementów i zwrócenie wyniku końcowego build()z delegatów readResolve. W takim przypadku musielibyśmy zaimplementować serializację tylko w klasie głównej hierarchii kolekcji i nie byłby potrzebny żaden dodatkowy kod z obecnych lub przyszłych implementacji, pod warunkiem, że implementują abstrakcyjne iterator()ibuilder()metoda (ta ostatnia służy do odtwarzania kolekcji tego samego typu - co samo w sobie byłoby bardzo przydatną funkcją). Innym przykładem może być hierarchia klas, której kod nie w pełni kontrolujemy - nasze klasy bazowe z biblioteki zewnętrznej mogą mieć dowolną liczbę pól prywatnych, o których nic nie wiemy i które mogą zmieniać się z jednej wersji na drugą, powodując nasze zserializowane obiekty. W takim przypadku bezpieczniej byłoby zapisać dane i odbudować obiekt ręcznie podczas deserializacji.

Turyn
źródło
0

Metoda readResolve

W przypadku klas Serializable i Externalizable metoda readResolve umożliwia klasie zastąpienie / rozwiązanie obiektu odczytanego ze strumienia przed zwróceniem go do obiektu wywołującego. Implementując metodę readResolve, klasa może bezpośrednio kontrolować typy i wystąpienia swoich własnych wystąpień, które są deserializowane. Metoda jest zdefiniowana w następujący sposób:

ANY-ACCESS-MODIFIER Obiekt readResolve () zgłasza wyjątek ObjectStreamException;

Metoda readResolve jest wywoływana, gdy ObjectInputStream odczytał obiekt ze strumienia i przygotowuje się do zwrócenia go do obiektu wywołującego. ObjectInputStream sprawdza, czy klasa obiektu definiuje metodę readResolve. Jeśli metoda jest zdefiniowana, wywoływana jest metoda readResolve, aby umożliwić obiektowi w strumieniu wyznaczenie obiektu do zwrócenia. Zwrócony obiekt powinien być typu zgodnego ze wszystkimi zastosowaniami. Jeśli nie jest zgodny, ClassCastException zostanie zgłoszony po wykryciu niezgodności typów.

Na przykład można utworzyć klasę Symbol, dla której tylko jedno wystąpienie każdego powiązania symbolu istniało w maszynie wirtualnej. Metoda readResolve zostanie zaimplementowana w celu określenia, czy ten symbol został już zdefiniowany i zastąpi istniejący wcześniej równoważny obiekt Symbol, aby zachować ograniczenie tożsamości. W ten sposób unikatowość obiektów Symbol może zostać zachowana w całej serializacji.

Ankush soni
źródło
0

Jak już odpowiedziałem, readResolvejest to metoda prywatna używana w ObjectInputStream podczas deserializacji obiektu. Jest to wywoływane tuż przed zwróceniem rzeczywistej instancji. W przypadku Singletona możemy wymusić zwrócenie już istniejącego odwołania do instancji singletona zamiast deserializowanego odwołania do instancji. Podobnie jak w writeReplaceprzypadku ObjectOutputStream.

Przykład dla 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);
    }

}

}

Wynik:

Before serialization: com.ej.item3.SingletonWithSerializable@7852e922
After deserialization: com.ej.item3.SingletonWithSerializable@7852e922
Omkar
źródło