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 и прочие текстовые форматы обеспечивают переносимость и безопасность.

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

Список распространённых проблем при использовании 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 из нескольких потоков без синхронизации, так как внутреннее состояние не потокобезопасно.

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

Короткие примеры сопоставимых механизмов в разных языках и ключевые отличия от 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-метаданные и может автоматически восстановить сложные объекты, но это делает формат небезопасным и тесно связанным с версиями классов. Большинство других языков либо имеют свои бинарные форматы, либо рекомендуют текстовые форматы для переносимости.

джава ObjectInputStream.readObject function comments

En
ObjectInputStream.readObject Reads an object from the ObjectInputStream