ObjectOutputStream.writeObject: примеры (JAVA)

Разбор способов работы с writeObject в Java
Раздел: Сериализация/десериализация объектов
ObjectOutputStream.writeObject(Object obj): void

Описание метода ObjectOutputStream.writeObject

Метод writeObject класса java.io.ObjectOutputStream служит для последовательной записи объектов в поток в формате Java-serializable. С его помощью создаётся сериализованное представление объекта (и связанных с ним объектов графа), которое затем может быть восстановлено с помощью ObjectInputStream.readObject().

Сигнатура

public final void writeObject(Object obj) throws IOException

Параметры

  • obj - любой объект Java. Объект должен быть сериализуемым: реализовать интерфейс java.io.Serializable или java.io.Externalizable, либо быть заменён через механизм writeReplace(). Поля, объявленные как transient или static, не включаются в стандартную сериализацию.

Возвращаемое значение

  • Метод ничего не возвращает (void).

Поведение и особенности

  • Записывается весь граф объектов, достижимый из переданного объекта, с сохранением ссылочной целостности: одинаковые объекты записываются один раз, циклические ссылки обрабатываются.
  • Если один и тот же объект записывается повторно без вызова reset(), повторные записи заменяются на контролируемые «хендлы» (handles), то есть при десериализации восстанавливаются ссылки на один и тот же объект.
  • Для записи без разделяемых хендлов применяется writeUnshared(Object obj), который гарантирует, что при последующих чтениях будет создана отдельная копия.
  • Если класс реализует приватные методы writeObject(ObjectOutputStream out) и readObject(ObjectInputStream in), эти методы будут вызваны вместо стандартной сериализации для контроля процесса.
  • Можно реализовать writeReplace() и readResolve() для замены объекта при записи/чтении.
  • Наличие поля serialVersionUID влияет на совместимость версий; при несоответствии возникает InvalidClassException.
  • Серийный поток записывает служебный заголовок при создании ObjectOutputStream. При повторном подключении к тому же потоку для записи новых объектов иногда применяется reset() или создание нового потока с подавлением заголовка (через подклассирование), но это - особая техника.

Исключения

  • NotSerializableException - если объект (или один из объектов в графе) не реализует Serializable и не заменяется через writeReplace.
  • InvalidClassException - при несоответствии версии класса и сериализованных данных.
  • IOException - общие ошибки ввода/вывода.
  • SecurityException - при ограничениях безопасности/политиках.

Когда используется

Метод применяется для долгосрочного сохранения состояния объектов, передачи объектов по сети (в клиент-серверных приложениях), реализации кешей/репликации объектов и других задач, где требуется восстановление состояния объектов Java в исходном виде. В современных приложениях рекомендуется оценивать риск безопасности и рассматривать альтернативы для межсервисного обмена.

Короткие примеры использования

1. Простая сериализация в файл

import java.io.*;
class Person implements Serializable {
  private static final long serialVersionUID = 1L;
  String name;
  int age;
  Person(String n, int a){name=n; age=a;}
}
public class SerSimple {
  public static void main(String[] args) throws Exception{
    Person p = new Person("Иван", 30);
    try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.bin"))){
      oos.writeObject(p);
    }
    System.out.println("Файл создан: person.bin");
  }
}
Файл создан: person.bin

2. Два раза записать один объект: shared handles

import java.io.*;
import java.nio.file.*;
public class SharedDemo{
  public static void main(String[] args) throws Exception{
    StringBuilder sb = new StringBuilder("data");
    try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("shared.bin"))){
      oos.writeObject(sb);
      oos.writeObject(sb); // будет ссылка, а не полное дублирование
    }
    byte[] b = Files.readAllBytes(Paths.get("shared.bin"));
    System.out.println("Размер: " + b.length);
  }
}
Размер: 76

3. writeUnshared - отдельные копии при каждом write

// отличия видны при десериализации (код аналогичен предыдущему),
// здесь только запись:
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("unshared.bin"))){
  StringBuilder sb = new StringBuilder("data");
  oos.writeUnshared(sb);
  oos.writeUnshared(sb);
}
System.out.println("Запись с writeUnshared завершена");
Запись с writeUnshared завершена

4. Ошибка при попытке сериализовать несериализуемый класс

class NotSer { String s = "x"; }
// в main:
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("bad.bin"))){
  oos.writeObject(new NotSer());
} catch (Exception e){
  e.printStackTrace();
}
java.io.NotSerializableException: NotSer
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
	... (стек вызовов)

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

  • writeUnshared(Object obj)
  • Гарантирует, что при последующих чтениях объект будет восстановлен как отдельная копия, даже если тот же объект записан ранее.

  • Externalizable
  • Интерфейс даёт полный контроль над сериализацией: требуется реализовать writeExternal и readExternal. Подходит, когда нужно явно управлять форматом записи.

  • java.beans.XMLEncoder / XMLDecoder
  • Сериализация в человекочитаемый XML для JavaBean-объектов. Удобно для конфигураций, но ограничения по типам и совместимости.

  • Custom marshal через DataOutput / DataInput
  • Ручная сериализация с помощью примитивных методов (writeInt, writeUTF и т.д.). Подходит для простых, компактных форматов и межплатформенной совместимости.

  • Библиотеки сторонних форматов (JSON, protobuf, Kryo)
  • Часто предпочтительнее для межсервисной передачи данных, производительности или компактности. Kryo обеспечивает быстрые бинарные потоки, protobuf - сжатую и версионируемую схему.

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

PHP: serialize()/unserialize()

$a = ['name'=>'Иван', 'age'=>30];
$s = serialize($a);
file_put_contents('a.bin', $s);
echo $s;
a:2:{s:4:"name";s:8:"Иван";s:3:"age";i:30;}

JavaScript: JSON.stringify / structuredClone

const obj = { name: 'Иван', age: 30 };
const s = JSON.stringify(obj);
console.log(s);
{"name":"Иван","age":30}

JSON подходит для простых структур, не сохраняет методы и прототипы. Для копирования объектов с сохранением ссылочной целостности и более сложных типов используются другие подходы.

Python: pickle / json

import pickle
obj = {'name':'Иван', 'age':30}
with open('p.bin','wb') as f:
  pickle.dump(obj, f)
print('ok')
ok

pickle сохраняет практически любые объекты Python, но имеет проблемы безопасности при загрузке из недоверенных источников, как и Java-сериализация.

C#: BinaryFormatter (устаревший) и System.Text.Json

// BinaryFormatter (устаревший) - раньше использовался для бинарной сериализации
// Современная рекомендация - использовать JSON или protobuf
(нет вывода)

Go: encoding/gob и encoding/json

package main
import (
  "bytes"
  "encoding/gob"
  "fmt"
)
func main(){
  var buf bytes.Buffer
  enc := gob.NewEncoder(&buf)
  v := map[string]int{"a":1}
  enc.Encode(v)
  fmt.Println("gob bytes:", buf.Len())
}
gob bytes: 39

gob похож на Java-serializable, но специфичен для Go. Для кроссплатформенной совместимости обычно используется JSON или protobuf.

Kotlin

Kotlin может использовать встроенную Java-сериализацию (тот же ObjectOutputStream) или библиотеку kotlinx.serialization, дающую схему и контроль версий.

Отличия от Java ObjectOutputStream

  • Многие языки предлагают собственные механизмы с разными гарантиями совместимости и безопасностью.
  • JSON ориентирован на человекочитаемость и кроссплатформенность, бинарные форматы (gob, protobuf, Kryo) - на производительность и компактность.
  • Во всех языках при восстановлении объектов из недоверенных источников рекомендуется применять фильтрацию и проверку целостности.

Типичные ошибки и их проявления

1. NotSerializableException

import java.io.*;
class A { B b = new B(); }
class B { int x = 1; }
// в main:
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("err.bin"))){
  oos.writeObject(new A());
} catch (Exception e){
  e.printStackTrace();
}
java.io.NotSerializableException: A
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
	... (стек вызовов)

Причина: класс A или вложенный класс не реализует Serializable.

2. InvalidClassException (несовместимость serialVersionUID)

// сериализация с одним serialVersionUID, изменение класса без обновления UID
// при десериализации может возникнуть InvalidClassException
java.io.InvalidClassException: com.example.Person; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

3. WriteAbortedException при ошибке во время writeObject приватного метода writeObject класса

private void writeObject(ObjectOutputStream out) throws IOException{
  throw new IOException("fail");
}
// при записи получим:
java.io.WriteAbortedException: writing aborted; java.io.IOException: fail
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1185)
	... (стек)

4. StreamCorruptedException

Возникает при попытке десериализовать повреждённые или несоответствующие данные (например, нарушение формата потока).

5. Неправильное использование handle table: ожидание обновления объекта после изменения полей

Если объект изменяется между двумя вызовами writeObject без вызова reset(), при десериализации две записи могут ссылаться на один объект с состоянием на момент первой записи. Для получения актуального состояния потребуется reset() или writeUnshared.

Изменения и рекомендации в последних версиях Java

  • Начиная с Java 9 добавлен API ObjectInputFilter для фильтрации входных сериализованных данных и уменьшения рисков при десериализации.
  • В последних выпусках платформы усиливается внимание к безопасности Java-сериализации: рекомендуется избегать десериализации из недоверенных источников и применять фильтры, либо переходить на альтернативные форматы (JSON, protobuf, CBOR и т.д.).
  • Сам метод writeObject API не претерпел крупных изменений, но экосистема и практика использования смещаются в сторону явных, безопасных форматов и библиотек.

Расширенные и редко встречающиеся примеры

1. Пользовательская сериализация через приватный writeObject/readObject

Пример java
import java.io.*;
class SecureData implements Serializable{
  private static final long serialVersionUID = 1L;
  private transient String secret; // не сохраняется по умолчанию
  SecureData(String s){ secret = s; }
  private void writeObject(ObjectOutputStream out) throws IOException{
    out.defaultWriteObject();
    // минимальная «маскировка» перед записью
    out.writeUTF(secret == null ? "" : new StringBuilder(secret).reverse().toString());
  }
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
    in.defaultReadObject();
    String rev = in.readUTF();
    secret = new StringBuilder(rev).reverse().toString();
  }
  public String toString(){ return "secret="+secret; }
}
public class CustSer{
  public static void main(String[] args) throws Exception{
    SecureData sd = new SecureData("topsecret");
    try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("sdat.bin"))){
      oos.writeObject(sd);
    }
    try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream("sdat.bin"))){
      System.out.println(ois.readObject());
    }
  }
}
secret=topsecret

2. Использование writeReplace / readResolve для прокси-сериализации

Пример java
import java.io.*;
class Singleton implements Serializable{
  private static final long serialVersionUID = 1L;
  static final Singleton INST = new Singleton();
  private Object readResolve(){ return INST; }
}
// при десериализации всегда будет возвращён Singleton.INST
(при десериализации возвращается ссылка на Singleton.INST)

3. Сброс таблицы хендлов для повторной сериализации (reset)

Пример java
// Если нужно повторно записать текущую версию объекта как новый объект в том же потоке
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.bin"));
MyObj o = new MyObj(1);
oos.writeObject(o);
o.setX(2);
oos.reset(); // очищает хендлы
oos.writeObject(o); // теперь при десериализации будет два отдельных состояния
oos.close();
(поток содержит две независимые записи объекта)

4. Сжатая и зашифрованная сериализация

Пример java
// Запись в GZIP и затем в файл
try(OutputStream fos = new FileOutputStream("c.gz");
    BufferedOutputStream bos = new BufferedOutputStream(fos);
    java.util.zip.GZIPOutputStream gz = new java.util.zip.GZIPOutputStream(bos);
    ObjectOutputStream oos = new ObjectOutputStream(gz)){
  oos.writeObject(new Person("Анна",25));
}
System.out.println("Записано в c.gz");
Записано в c.gz

5. Подкласс ObjectOutputStream для подавления заголовка потока

Пример java
// Когда нужно продолжать писать в тот же файл несколькими запусками без второго заголовка
class AppendedObjectOutputStream extends ObjectOutputStream{
  AppendedObjectOutputStream(OutputStream out) throws IOException{ super(out); }
  protected void writeStreamHeader() throws IOException{ /* пусто */ }
}
// Первый запуск создаёт поток обычным ООС, последующие используют подкласс
(позволяет добавлять объекты в файл без второго заголовка)

6. Сетевой пример: отправка и получение объектов по сокету

Пример java
// Сервер
ServerSocket ss = new ServerSocket(12345);
Socket s = ss.accept();
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
 oos.writeObject(new Person("Клиент", 40));
 oos.close(); s.close(); ss.close();
// Клиент использует ObjectInputStream и readObject
(объект доставлен клиенту и десериализован)

Каждый пример иллюстрирует практические приемы: контроль формата через writeObject/readObject, управление ссылочной целостностью через reset/writeUnshared, интеграция с потоками сжатия/шифрования и адаптация поведения создания заголовка через подклассирование.

джава ObjectOutputStream.writeObject function comments

En
ObjectOutputStream.writeObject Writes the specified object to the ObjectOutputStream