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() возвращает новый/другой объект, повторные ссылки могут указывать на разные экземпляры.
  • Будьте осторожны: вы можете как «сломать» идентичность, так и намеренно обеспечить её (классический пример — Singleton через 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: ожидание «новых» объектов после сериализации. Сериализация не создаёт «свежую» структуру — она восстанавливает исходные ссылки, как в оригинале. Это может быть неожиданным при работе с кэшами или шаблонными объектами.

1
Задача
JAVA 25 SELF, 44 уровень, 3 лекция
Недоступна
Совместная работа с общим документом: сохранение разделяемых объектов 📝
Совместная работа с общим документом: сохранение разделяемых объектов 📝
1
Задача
JAVA 25 SELF, 44 уровень, 3 лекция
Недоступна
Управление инвентарем: идентичность объектов при раздельном хранении 📦
Управление инвентарем: идентичность объектов при раздельном хранении 📦
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ