JavaRush /Курси /JAVA 25 SELF /Ідентичність об’єктів під час бінарної серіалізації

Ідентичність об’єктів під час бінарної серіалізації

JAVA 25 SELF
Рівень 44 , Лекція 3
Відкрита

1. Вступ

У Java (і загалом в ООП) кожен об’єкт має ідентичність — це не просто набір значень його полів, а саме «хто він такий» у пам’яті. Два об’єкти можуть бути «еквівалентними» (наприклад, a.equals(b) повертає true), але відрізнятися за ідентичністю (a != b). Ідентичність — це коли дві змінні посилаються на один і той самий об’єкт у пам’яті (a == b).

Чому це важливо для серіалізації?

Коли ви серіалізуєте (зберігаєте) граф об’єктів (наприклад, дерево, список або просто два об’єкти, які посилаються на один і той самий вкладений об’єкт), важливо зберегти не лише значення полів, а й структуру посилань між об’єктами.

Якщо у вашому графі є повторні посилання (кілька об’єктів посилаються на один і той самий вкладений об’єкт) або навіть циклічні посилання (A → B → A), то після десеріалізації ці зв’язки мають лишитися такими самими.

Приклад: у вас є два об’єкти A і B, обидва посилаються на один і той самий об’єкт C. Після серіалізації та десеріалізації має бути: a.c == b.c (один і той самий об’єкт у пам’яті). Якщо серіалізація просто «копіює» об’єкти, то після відновлення вийдуть два різні об’єкти C — і це вже інший граф.

2. Як ObjectOutputStream розв’язує проблему ідентичності

У Java для бінарної серіалізації використовується пара класів: ObjectOutputStream (для запису) і ObjectInputStream (для читання).

ObjectOutputStream — це не просто «записати об’єкт у файл». Він «розумний»:

  • Відстежує всі об’єкти, які вже були серіалізовані в межах одного потоку запису (однієї сесії).
  • Якщо зустрічається посилання на об’єкт, який уже серіалізовано, він не пише його заново, а записує спеціальне «повторне посилання» (reference handle).
  • Під час десеріалізації ObjectInputStream відновлює структуру так, щоб повторні посилання вказували на один і той самий об’єкт у пам’яті.

Це працює навіть для циклічних посилань! Тобто, якщо у вас є A → B → A, серіалізація та десеріалізація не увійдуть у нескінченний цикл і не «заваляться»: посилання буде відновлено коректно.

Як це реалізовано?

Усередині ObjectOutputStream використовується спеціальна таблиця (identity map), де зберігаються вже серіалізовані об’єкти. Коли трапляється новий об’єкт, він додається в таблицю і серіалізується повністю. Якщо зустрічається об’єкт із таблиці — у потік записується лише «повторне посилання» (handle).

3. Демонстрація на прикладі

Приклад 1: Граф із циклічними посиланнями (A → B, B → A)

import java.io.*;

class Node implements Serializable {
    String name;
    Node next;

    Node(String name) {
        this.name = name;
    }
}

public class CyclicSerializationDemo {
    public static void main(String[] args) throws Exception {
        Node a = new Node("A");
        Node b = new Node("B");
        a.next = b;
        b.next = a; // Цикл!

        // Серіалізація
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
            out.writeObject(a);
        }

        // Десеріалізація
        Node a2;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic.dat"))) {
            a2 = (Node) in.readObject();
        }

        System.out.println("a2.name = " + a2.name); // A
        System.out.println("a2.next.name = " + a2.next.name); // B
        System.out.println("a2.next.next == a2: " + (a2.next.next == a2)); // true!
    }
}

Виведення:

a2.name = A
a2.next.name = B
a2.next.next == a2: true

Коментар:
Цикл збережено! Після десеріалізації a2.next.next знову вказує на a2.

Приклад 2: Повторні посилання (A → C, B → C)

import java.io.*;

class Wrapper implements Serializable {
    String name;
    Object ref;

    Wrapper(String name) { this.name = name; }
}

public class SharedReferenceDemo {
    public static void main(String[] args) throws Exception {
        Wrapper a = new Wrapper("A");
        Wrapper b = new Wrapper("B");
        Wrapper c = new Wrapper("C");

        a.ref = c;
        b.ref = c;

        // Серіалізація
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
            out.writeObject(new Wrapper[] {a, b});
        }

        // Десеріалізація
        Wrapper[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("shared.dat"))) {
            arr = (Wrapper[]) in.readObject();
        }

        Wrapper a2 = arr[0];
        Wrapper b2 = arr[1];

        System.out.println("a2.ref == b2.ref: " + (a2.ref == b2.ref)); // true!
        System.out.println("a2.ref.name = " + ((Wrapper)a2.ref).name); // C
    }
}

Виведення:

a2.ref == b2.ref: true
a2.ref.name = C

Коментар:
Після десеріалізації обидва посилання знову вказують на один і той самий об’єкт.

4. Зв’язок із writeReplace та readResolve

У Java можна «втрутитися» в процес (де)серіалізації за допомогою спеціальних методів:

writeReplace() — викликається перед серіалізацією об’єкта. Можна повернути інший об’єкт, який буде серіалізовано замість вихідного.
readResolve() — викликається після десеріалізації об’єкта. Можна повернути інший об’єкт, який буде використано замість щойно створеного.

Вплив на ідентичність:

  • Якщо використовується writeReplace() або readResolve(), то саме об’єкт, повернений цими методами, потрапить у таблицю посилань.
  • Це може змінити ідентичність: якщо readResolve() повертає новий/інший об’єкт, повторні посилання можуть вказувати на різні екземпляри.
  • Будьте обережні: ви можете як «зламати» ідентичність, так і навмисно забезпечити її (класичний приклад — синглтон через readResolve(), де всі посилання після десеріалізації вказують на один і той самий синглтон).

5. Практика: код, що демонструє збереження ідентичності

import java.io.*;

class Shared implements Serializable {
    String value;
    Shared(String value) { this.value = value; }
}

class Holder implements Serializable {
    String name;
    Shared shared;
    Holder(String name, Shared shared) {
        this.name = name;
        this.shared = shared;
    }
}

public class IdentityDemo {
    public static void main(String[] args) throws Exception {
        Shared c = new Shared("C");
        Holder a = new Holder("A", c);
        Holder b = new Holder("B", c);

        // Серіалізація
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
            out.writeObject(new Holder[] {a, b});
        }

        // Десеріалізація
        Holder[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("identity.dat"))) {
            arr = (Holder[]) in.readObject();
        }

        Holder a2 = arr[0];
        Holder b2 = arr[1];

        System.out.println("a2.shared == b2.shared: " + (a2.shared == b2.shared)); // true!
        System.out.println("a2.shared.value = " + a2.shared.value); // C
    }
}

Виведення:

a2.shared == b2.shared: true
a2.shared.value = C

Коментар:
Ідентичність об’єкта c збережено: після десеріалізації обидва посилання вказують на один і той самий екземпляр.

6. Підсумки та типові помилки

Помилка № 1: серіалізація об’єктів із повторними посиланнями окремо. Якщо ви записуєте кожен об’єкт окремим екземпляром ObjectOutputStream, посилання між ними не збережуться — десеріалізовані екземпляри будуть різними, навіть якщо в оригіналі це було одне й те саме посилання.

Помилка № 2: некоректне використання writeReplace() та readResolve(). Ці методи можуть підмінити об’єкт під час (де)серіалізації, що змінить його ідентичність. Якщо не розуміти механіку, можна отримати неочікувані екземпляри на виході.

Помилка № 3: неочікувані ефекти від спільних змінюваних посилань. Якщо кілька об’єктів посилаються на один змінюваний вкладений об’єкт (наприклад, список), після десеріалізації це залишиться так само. Зміна в одному місці вплине на всі інші.

Помилка № 4: очікування «нових» об’єктів після серіалізації. Серіалізація не створює «свіжу» структуру — вона відновлює вихідні посилання, як в оригіналі. Це може бути неочікуваним під час роботи з кешами або шаблонними об’єктами.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ