ObjectInputStream.readObject: примеры (JAVA)

Чтение объектов через ObjectInputStream.readObject
Раздел: Сериализация/десериализация объектов
ObjectInputStream.readObject: Object

Общее описание и сигнатура метода

Метод ObjectInputStream.readObject() используется для восстановления сериализованных объектов из входного потока байт. Он читает следующее представление объекта из потока и возвращает десериализованный экземпляр в виде java.lang.Object. Восстановление включает проверку версии класса (serialVersionUID), разрешение классов в текущем ClassLoader, применение методов readObject в классах и вызов механизма readResolve при наличии.

Сигнатура метода:

public final Object readObject() throws IOException, ClassNotFoundException

Параметров у метода нет. Возвращаемое значение - десериализованный объект как Object. Возможные исключения:

  • ClassNotFoundException - если класс объекта не найден во время восстановления.
  • IOException - при ошибках ввода/вывода, повреждении потока или нарушении формата.

Поведение при десериализации включает несколько шагов: чтение метаданных объекта, разрешение класса (вызов resolveClass при пользовательском ObjectInputStream), создание экземпляра без вызова обычного конструктора, восстановление значений полей, вызов специальных методов обратного вызова (например, readObject с сигнатурой private void readObject(ObjectInputStream)), применение readResolve при наличии, и проверку целостности классов по serialVersionUID.

Класс ObjectInputStream также поддерживает связанные API: readUnshared() для чтения «неразделяемой» копии объекта, enableResolveObject и resolveObject для замены объектов во время чтения, а с более новых версий Java - фильтры десериализации через ObjectInputFilter.

Короткие практические примеры

Ниже приведены упрощённые примеры записи и чтения объектов с помощью ObjectOutputStream и ObjectInputStream.readObject().

1) Простая сериализация и десериализация:

import java.io.*;

class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    int age;
    Person(String name, int age) { this.name = name; this.age = age; }
}

// Сериализация
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
    oos.writeObject(new Person("Ivan", 30));
}
byte[] data = baos.toByteArray();

// Десериализация
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
    Object obj = ois.readObject();
    Person p = (Person) obj;
    System.out.println(p.name + " " + p.age);
}
Ivan 30

2) Обработка отсутствующего класса (ClassNotFoundException):

// Ситуация: на стороне чтения отсутствует определение класса Person
// При попытке ois.readObject() будет выброшено ClassNotFoundException
java.lang.ClassNotFoundException: Person

3) Использование readUnshared для получения отдельной копии:

// Записан один и тот же объект дважды
ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos2)) {
    Person p = new Person("Anna", 25);
    oos.writeObject(p);
    oos.writeObject(p);
}
byte[] d2 = baos2.toByteArray();

try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(d2))) {
    Person a = (Person) ois.readObject();
    Person b = (Person) ois.readUnshared(); // отдельная копия
    System.out.println(a == b); // false
}
false

Похожие механизмы в Java

В Java присутствуют несколько близких по назначению API:

  • readUnshared() - возвращает экземпляр, который гарантированно не будет тот же объект в пуле ссылок десериализатора. Предпочтителен, когда требуется независимость объектов.
  • Externalizable (вместо Serializable) - даёт полный контроль над форматом через методы writeExternal и readExternal. Предпочтение в случаях, когда требуется явный формат и совместимость между версиями.
  • ObjectInputStream с переопределением resolveClass/resolveProxyClass/resolveObject - применяется для кастомного разрешения классов и подмен объектов во время чтения.
  • JSON-библиотеки (Jackson, Gson) - альтернатива для человекочитаемых форматов и межплатформенной совместимости.

Выбор между ними зависит от требований: бинарная сериализация Java удобна для JVM-to-JVM обмена с сохранением внутренней структуры объектов; Externalizable и кастомные resolve-методы дают больше контроля; JSON и прочие текстовые форматы обеспечивают переносимость и безопасность.

Аналоги и их отличия в других языках

Короткие примеры сопоставимых механизмов в разных языках и ключевые отличия от Java-подхода.

  • Python: модуль pickle. Похож по задумке, но формат Python-специфичен. Пример:
import pickle
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
p = Person('Ivan', 30)
data = pickle.dumps(p)
p2 = pickle.loads(data)
print(p2.name, p2.age)
Ivan 30
  • JavaScript (Node.js): стандартной двоичной сериализации объектов нет; чаще применяется JSON.stringify/parse или библиотеки (protobuf, msgpack). JSON не сохраняет методы и прототипы.
const obj = {name: 'Ivan', age: 30};
const data = JSON.stringify(obj);
const o = JSON.parse(data);
console.log(o.name, o.age);
Ivan 30
  • PHP: функции serialize и unserialize. Формат PHP-специфичен и небезопасен при работе с недоверенными данными.
$obj = ['name' => 'Ivan', 'age' => 30];
$data = serialize($obj);
$o = unserialize($data);
print_r($o);
Array
(
    [name] => Ivan
    [age] => 30
)
  • C#: раньше использовался BinaryFormatter.Deserialize, но он признан небезопасным и устаревшим. Рекомендуются System.Text.Json или protobuf для совместимости.
// System.Text.Json пример
using System.Text.Json;
var obj = new { name = "Ivan", age = 30 };
var json = JsonSerializer.Serialize(obj);
var o = JsonSerializer.Deserialize<Dictionary<string, object>>(json);
Console.WriteLine(o["name"] + " " + o["age"]);
Ivan 30
  • Go: пакет encoding/gob предоставляет бинарную сериализацию для Go-типов и похож по назначению, но совместим только между Go-программами.
// Пример с gob
package main
import (
    "bytes"
    "encoding/gob"
    "fmt"
)

type Person struct { Name string; Age int }
func main() {
    var buf bytes.Buffer
    enc := gob.NewEncoder(&buf)
    dec := gob.NewDecoder(&buf)
    p := Person{"Ivan", 30}
    enc.Encode(p)
    var p2 Person
    dec.Decode(&p2)
    fmt.Println(p2.Name, p2.Age)
}
Ivan 30

Ключевые отличия: Java-serializable сохраняет полную JVM-метаданные и может автоматически восстановить сложные объекты, но это делает формат небезопасным и тесно связанным с версиями классов. Большинство других языков либо имеют свои бинарные форматы, либо рекомендуют текстовые форматы для переносимости.

Типичные ошибки и исключения

Список распространённых проблем при использовании readObject() с примерами.

1) ClassNotFoundException - отсутствует определение класса на стороне чтения:

// Попытка десериализации класса, которого нет в classpath
byte[] data = ...; // данные, содержащие объект класса UnknownClass
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
    Object o = ois.readObject();
} catch (ClassNotFoundException e) {
    System.out.println("Класс не найден: " + e.getMessage());
}
Класс не найден: UnknownClass

2) InvalidClassException - несовпадение serialVersionUID или несовместимость полей:

// Если serialVersionUID изменён между версиями классов
// при readObject будет выброшено InvalidClassException
java.io.InvalidClassException: Person; local class incompatible: stream classdesc serialVersionUID = 123, local class serialVersionUID = 456

3) StreamCorruptedException и EOFException - повреждение данных или неожиданное окончание:

// Если поток усечён
byte[] truncated = Arrays.copyOf(data, data.length / 2);
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(truncated))) {
    ois.readObject();
} catch (IOException e) {
    System.out.println(e.getClass().getSimpleName());
}
StreamCorruptedException

4) NotSerializableException - попытка сериализовать объект без интерфейса Serializable:

class NoSer { }
// При записи oos.writeObject(new NoSer())
// будет выброшено NotSerializableException
java.io.NotSerializableException: NoSer

5) Уязвимости безопасности - десериализация недоверенных данных может привести к исполнению вредоносного кода. Начиная с Java 9 доступен механизм фильтрации с помощью ObjectInputFilter для ограничения типов, разрешённых к десериализации.

Изменения и улучшения в последних релизах

За последние версии Java появились механизмы, повышающие безопасность и контроль над процессом десериализации:

  • ObjectInputFilter (JEP 290, Java 9) - позволяет задать правило фильтрации объектов при десериализации, предотвращая восстановление нежелательных типов.
  • Улучшены возможности для настройки поведения в подсистеме сериализации: методы для установки и комбинирования фильтров, более гибкие точки подмены объектов через resolveObject.
  • Рост рекомендаций по отказу от Java-бинарной сериализации для межсистемного обмена: предпочтение отдаётся JSON, protobuf и другим форматам, что повлияло на руководство по безопасности и практики разработки.

Сам метод readObject() сигнатурно не менялся, но экосистема вокруг него получила средства контроля и фильтрации. Нативной депрекации для метода не выявлено.

Расширенные и редкие сценарии использования

Ниже несколько подробных примеров, демонстрирующих тонкие возможности и обходные приёмы.

1) Кастомное разрешение классов при десериализации (подмена имён классов):

Пример java
import java.io.*;

class CustomOIS extends ObjectInputStream {
    CustomOIS(InputStream in) throws IOException { super(in); }
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        String name = desc.getName();
        if ("old.pkg.Person".equals(name)) {
            return Class.forName("new.pkg.Person");
        }
        return super.resolveClass(desc);
    }
}

// Использование: позволяет читать объекты, записанные старой версией с другим именем пакета
// Результат: объекты старого пакета будут восстановлены как new.pkg.Person

2) Фильтрация типов через ObjectInputFilter для безопасности:

Пример java
ObjectInputFilter filter = info -> {
    Class<?> serialClass = info.serialClass();
    if (serialClass != null && serialClass.getName().startsWith("com.myapp.")) {
        return ObjectInputFilter.Status.ALLOWED;
    }
    return ObjectInputFilter.Status.REJECTED;
};
try (ObjectInputStream ois = new ObjectInputStream(in)) {
    ObjectInputFilter.Config.setObjectInputFilter(ois, filter);
    Object o = ois.readObject();
}
// Результат: десериализация только разрешённых типов, прочие - отклоняются

3) Замена объектов во время чтения через resolveObject:

Пример java
ObjectInputStream ois = new ObjectInputStream(in) {
    @Override
    protected Object resolveObject(Object obj) throws IOException {
        if (obj instanceof Date) {
            return new java.sql.Date(((Date) obj).getTime());
        }
        return super.resolveObject(obj);
    }
};
// При readObject() все Date будут заменены на java.sql.Date
// Результат: получаем нужный тип даты без дополнительной обработки после чтения

4) Обработка версий классов: использование serialVersionUID и readObject/readObjectNoData

Пример java
class Person implements Serializable {
    private static final long serialVersionUID = 2L; // управляемая версия
    String name;
    int age; // новое поле в версии 2
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        if (age == 0) {
            age = -1; // значение по умолчанию для обратной совместимости
        }
    }
}

// При чтении старого потока, где age отсутствует, поле получит значение -1
// Результат: корректное восстановление объекта при изменившейся структуре класса

5) Чтение потоков с несколькими типами и обработка ClassNotFound для частичного восстановления:

Пример java
try (ObjectInputStream ois = new ObjectInputStream(in)) {
    while (true) {
        try {
            Object o = ois.readObject();
            // обработка известных типов
        } catch (ClassNotFoundException e) {
            // логирование и продолжение чтения следующих объектов
        }
    }
} catch (EOFException e) {
    // конец потока
}
// Результат: восстановление всех доступных объектов, пропуск неизвестных типов

6) Производительность: использование буферов и предварительной десериализации в отдельных потоках для снижения влияния GC и блокировок ввода/вывода. В многопоточной среде следует избегать совместного использования одного ObjectInputStream из нескольких потоков без синхронизации, так как внутреннее состояние не потокобезопасно.

джава ObjectInputStream.readObject function comments

En
ObjectInputStream.readObject Reads an object from the ObjectInputStream