Система ввода/вывода в Java

Создание хорошей системы ввода/вывода (I/O) является одной из наиболее сложных задач для разработчиков языка.

Доказательством этому служит наличие множества различных подходов. Сложность задачи видится в охвате всех возможностей.

Не только различный исходный код и виды ввода/вывода, с которыми вы можете общаться (файлы, консоль, сетевые соединения), но так же вам необходимо общаться с ними большим числом способов (последовательный, в случайном порядке, буферный, бинарный, посимвольный, построчный, пословный и т.п.).

Разработчики библиотеки Java атаковали эту проблему путем создания множества классов.

Фактически, существует так много классов для системы ввода/вывода в Java, что это может сначала испугать (по иронии, дизайн ввода/вывода Java I/O на самом деле предотвращает взрыв классов).

Также произошли значительные изменения в библиотеке ввода/вывода после версии Java 1.0, когда изначально byte-ориентированная библиотека была пополнена char-ориентированными, основанными на Unicode I/O классами.

Как результат, есть некоторое количество классов, которые необходимо изучить прежде, чем вы поймете достаточно хорошо картину ввода/вывода Java и ее правильно использовать.

 

Класс File

Прежде чем перейти к классам, которые действительно читают и записывают данные в поток, мы посмотрим на утилиты, обеспечивающиеся библиотекой в помощь вам в обработке директории файлов.

Класс File имеет обманчивое имя — вы можете подумать, что он ссылается на файл, но это не так.

Он может представлять либо имя определенного файла, либо имя набора файлов в директории.

Если это набор файлов, вы можете опросить набор с помощью метода list( ), который вернет массив String. Есть смысл возвращать массив, а не гибкий класс контейнера, потому что число элементов фиксировано, и если вам нужен список другого директория, вы просто создаете другой объект File.

Фактически, “FilePath” был бы лучшим именем для класса.

·         Список директории

·         Поиск и создание директориев

 

Ввод и вывод

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

Поток прячет детали того, что случается с данными внутри реального устройства ввода/вывода.

Библиотечные классы Java для ввода/вывода делятся на классы ввода и вывода.

При наследовании, все, что наследуется от классов InputStream или Reader, имеет основной метод, называемый read( ) для чтения единичного байта или массива байт.

Точно так же, все, что наследуется от классов OutputStream или Writer, имеет основной метод, называемый write( ) для записи единичного байта или массива байт.

Однако чаще всего вы не можете использовать эти методы; они существуют для того, чтобы другие классы могли использовать их — эти другие классы обеспечивают более полезные интерфейсы.

Таким образом, вы редко будете создавать ваш объект потока, используя единственный класс, вместо этого вы будите располагать множеством объектом для обеспечения желаемой функциональности. Факт в том что вы создаете более, чем один объект для создания единственного результирующего потока, это главная причина, по которой потоки Java являются запутанными.

Полезно распределить классы по категориям, исходя из их функциональности. В Java 1.0 разработчики библиотеки начали с решения, что все классы, которые могут что-то делать с вводом, должны наследоваться от InputStream, а все классы, которые ассоциируются с выводом, должны наследоваться от OutputStream.

 

Таблица 11-1. Типы InputStream

 

Класс

Функция

Аргументы конструктора

Как его использовать

ByteArray-InputStream

Позволяет использовать буфер в памяти в качестве InputStream

Буфер, их которого извлекаются байты.

Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.

StringBuffer-InputStream

Конвертирует String в InputStream

String. Лежащая в основе реализация на самом деле использует StringBuffer.

Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.

File-InputStream

Для чтения информации из файла.

String, представляющий имя файла, или объекты File или FileDescriptor.

Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.

Piped-InputStream

Производит данные, которые были записаны в ассоциированный PipedOutput-Stream. Реализует концепцию “трубопровода”.

PipedOutputStream

Как источник данных при нескольких нитях процессов. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.

Sequence-InputStream

Преобразует два или более объектов InputStream в единый InputStream.

Два объекта InputStream или Enumeration для контейнера из InputStream.

Как источник данных. Соединить его с объектом FilterInputStream для обеспечения полезного интерфейса.

Filter-InputStream

Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность для других классов InputStream. Смотрите таблицу11-3.

Смотрите таблицу 11-3.

Смотрите таблицу 11-3.

 

Таблица 11-2. Типы OutputStream

Класс

Функция

Аргументы конструктора

Как его использовать

ByteArray-OutputStream

Создает буфер в памяти. Все данные, которые вы будете посылать в поток, помещаются в этот буфер.

необязательный начальный размер буфера.

Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.

File-OutputStream

Для отсылки информации в файл.

Строка, представляющая имя файла, или объекты File или FileDescriptor.

Для определения места назначения ваших данных. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.

Piped-OutputStream

Любая информация, записанная сюда, автоматически становится вводом ассоциированного PipedInput-Stream. Реализует концепцию “трубопровода”.

PipedInputStream

Для определения назначения ваших данных со многими нитями процессов. Соедините его с объектом FilterOutputStream для обеспечения полезного интерфейса.

Filter-OutputStream

Абстрактный класс, который является интерфейсом для декоратора, который обеспечивает полезную функциональность другим классам OutputStream. Смотрите Таблицу 11-4.

Смотрите Таблицу 11-4.

Смотрите Таблицу 11-4.

 

Добавление атрибутов и полезных интерфейсов

 

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

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

Это делает основное использование декораторов прозрачным — вы посылаете объекту одни и те же с сообщения не зависимо от того, был он декорирован или нет. Это причина существования “фильтрующих” классов в библиотеке ввода/вывода в Java: абстрактный “фильтрующий” класс - это базовый класс для всех декораторов. (Декоратор должен иметь такой же интерфейс, что и объект, который он декорирует, но декоратор так же может расширить интерфейс, что случается в некоторых “фильтрующих” классах.

К классам, обеспечивающим интерфейс декоратора для управления определенным InputStream или OutputStream, относятся FilterInputStream и FilterOutputStream — которые не имеют интуитивно понятных имен.

FilterInputStream и FilterOutputStream являются абстрактными классами, наследованными от базовых классов библиотеки ввода/вывода InputStream и OutputStream, которые являются ключевым требованием декоратора (так как он обеспечивает общий интерфейс для всех объектов, которые будут декорироваться).

Таблица 11-3. Типы FilterInputStream

Класс

Функция

Аргументы конструктора

Как его использовать

Data-InputStream

Используется в согласии с DataOutputStream, так что вы можете читать примитивные типы (int, char, long, и т.п.) из потока портативным способом.

InputStream

Содержит полный интерфейс, чтобы позволить вам читать примитивные типы.

Buffered-InputStream

Используйте это для предотвращения физического чтения каждый раз, когда вам необходимы дополнительные данные. Вы говорить “Использовать буфер”.

InputStream с необязательным размером буфера.

Сам по себе не обеспечивает интерфейс, просто требует, чтобы использовался буфер. Присоединяет объект интерфейса.

LineNumber-InputStream

Сохраняет историю номеров строк входного потока; вы можете вызвать getLineNumber( ) и setLineNumber(
int).

InputStream

Это просто добавляет нумерацию строк, так что вы, вероятно, присоедините объект интерфейса.

Pushback-InputStream

Имеет буфер для возврата одного символа, так что вы можете поместить обратно один прочитанный символ.

InputStream

Обычно используется в сканерах для компилятора и, вероятно, включено потому, что Java компилятор нуждается в нем. Вы, вероятно, не захотите использовать его.

 

Таблица 11-4. Типы FilterOutputStream

Класс

Функции

Аргументы конструктора

Как это использовать

Data-OutputStream

Используется совместно с DataInputStream, так что вы можете писать примитивные типы (int, char, long и т.п.) в поток портативным образом.

OutputStream

Содержит полный интерфейс, чтобы позволить вам записывать примитивные типы.

PrintStream

Для произведения форматированного вывода. В то время как DataOutputStream обрабатывает хранилище данных, PrintStream обрабатывает отображение.

OutputStream, с необязательным boolean, указывающим, что буфер будет принудительно освобождаться с каждой новой строкой.

Должен быть в финале оборачивать ваш объект OutputStream. Вы, вероятно, часто будете использовать его.

Buffered-OutputStream

Используйте это для предотвращения физической записи при каждой посылке данных. Вы говорите “Используй буфер”. Вы вызываете flush( ) для очистки буфера.

OutputStream, с необязательным размером буфера.

Это не обеспечивает сам по себе интерфейс, просто является требованием использования буфера. Присоединяется к объекту интерфейса.

 

Читающие и пишущие

 

Kлассы InputStream и OutputStream все еще обеспечивают ценную функциональность в форме байт-ориентированных систем ввода/вывода, в то время как классы Reader и Writer обеспечивают Unicode-совместимый, символьно ориентированный ввод/вывод.

Иногда возникают ситуации, когда вы должны использовать классы из “byte” иерархии в комбинации с классами в “символьной” иерархии.

Чтобы выполнить это, существуют классы - “мосты”: InputStreamReader преобразует InputStream к Reader, и OutputStreamWriter преобразует OutputStream к Writer.

Источники и приемники данных

Почти все оригинальные классы потоков ввода/вывода имеют соответствующие классы Reader и Writer для обеспечения родных манипуляций в Unicode.

Однако есть некоторые места, где байт-ориентированные InputStream и OutputStream являются корректным решением

Здесь приведена таблица, которая показывает соответствие между источниками и приемниками информации (то есть, куда данные приходят на физическом уровне или куда они уходят) в двух иерархиях.

Источники и приемники: класс Java 1.0

Соответствующий класс Java 1.1

InputStream

Reader
конвертер: InputStreamReader

OutputStream

Writer
конвертер: OutputStreamWriter

FileInputStream

FileReader

FileOutputStream

FileWriter

StringBufferInputStream

StringReader

(соответствующего класса нет)

StringWriter

ByteArrayInputStream

CharArrayReader

ByteArrayOutputStream

CharArrayWriter

PipedInputStream

PipedReader

PipedOutputStream

PipedWriter

 

Типичное использование потоков ввода/вывода

Хотя вы можете комбинировать классы потоков ввода/вывода многими различными способами, вы, вероятно, будете использовать несколько комбинаций. Следующий пример может быть использован как отправная точка; он показывает создание и использование типичной конфигурации ввода/вывода. Обратите внимание, что каждая конфигурация начинается с порядкового номера и заголовка, который оглавляет соответствующее объяснение в следующем за ним тексте.

//: c11:IOStreamDemo.java
// Типичные конфигурации потоков ввода/вывода.
import java.io.*;
 
public class IOStreamDemo {
  // Выбрасывание исключения на консоль:
  public static void main(String[] args)   throws IOException {
    
          // 1. Чтение ввода по строкам:
          BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java"));
          String s, s2 = new String();
    
          while((s = in.readLine())!= null)
                    s2 += s + "\n";
    
          in.close();
 
          // 1b. Чтение стандартного ввода:
          BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));      
          System.out.print("Enter a line:");
          System.out.println(stdin.readLine());
 
          // 2. Ввод из памяти
          StringReader in2 = new StringReader(s2);
          int c;
          while((c = in2.read()) != -1)
                    System.out.print((char)c);
 
          // 3. Форматированный ввод из памяти
          try {
                    DataInputStream in3 = new DataInputStream(
                                                                       new ByteArrayInputStream(s2.getBytes()));
                    while(true)
                               System.out.print((char)in3.readByte());
          } catch(EOFException e) {
                    System.err.println("End of stream");
          }
 
          // 4. Вывод в файл
          try {
                    BufferedReader in4 = new BufferedReader(new StringReader(s2));
                    PrintWriter out1 = new PrintWriter(new BufferedWriter(
                                                                                  new FileWriter("IODemo.out")));
                    int lineCount = 1;
                    while((s = in4.readLine()) != null )
                               out1.println(lineCount++ + ": " + s);
      
                    out1.close();
          } catch(EOFException e) {
                    System.err.println("End of stream");
          }
 
    // 5. Хранение и перекрытие данных
    try {
      DataOutputStream out2 =
        new DataOutputStream(
          new BufferedOutputStream(
            new FileOutputStream("Data.txt")));
      out2.writeDouble(3.14159);
      out2.writeChars("That was pi\n");
      out2.writeBytes("That was pi\n");
      out2.close();
      DataInputStream in5 =
        new DataInputStream(
          new BufferedInputStream(
            new FileInputStream("Data.txt")));
      BufferedReader in5br =
        new BufferedReader(
          new InputStreamReader(in5));
      // Необходимо использовать DataInputStream для данных:
      System.out.println(in5.readDouble());
      // Теперь можно использовать "правильный" readLine():
      System.out.println(in5br.readLine());
      // Но выводимая строка забавна.
      // Строка, созданная с помощью writeBytes, в порядке:
      System.out.println(in5br.readLine());
    } catch(EOFException e) {
      System.err.println("End of stream");
    }
 
    // 6. Чтение/запись файлов в произвольном порядке
    RandomAccessFile rf =
      new RandomAccessFile("rtest.dat", "rw");
    for(int i = 0; i < 10; i++)
      rf.writeDouble(i*1.414);
    rf.close();
 
    rf =
      new RandomAccessFile("rtest.dat", "rw");
    rf.seek(5*8);
    rf.writeDouble(47.0001);
    rf.close();
 
    rf =
      new RandomAccessFile("rtest.dat", "r");
    for(int i = 0; i < 10; i++)
      System.out.println(
        "Value " + i + ": " +
        rf.readDouble());
    rf.close();
  }
} ///:~

 

Потоки в виде трубопровода

PipedInputStream, PipedOutputStream, PipedReader и PipedWriter будут упомянуты только вскользь в этой главе. Это не означает, что они бесполезны, но их значение не будет очевидно, пока вы не поймете многонитевые процессы, так как потоки в виде трубопровода используются для общения между нитями.

 

Чтение из стандартного ввода

Стандартная модель ввода/вывода в Java имеет System.in, System.out и System.err. На протяжении всей этой книге вы видели, как писать в стандартный вывод, используя System.out, который представляет собой объект PrintStream. System.err аналогичен PrintStream, а System.in является производной InputStream без каких-либо включений. Это означает, что в то время, когда вы можете использовать System.out и System.err как они есть, System.in должен куда-то включаться (быть обернут), прежде, чем вы сможете прочесть из него.

Обычно вы захотите читать ввод построчно, используя readLine( ), так что вы захотите поместить System.in в BufferedReader. Чтобы сделать это, вы можете конвертировать System.in в Reader, используя InputStreamReader. Вот пример, который просто повторяет каждую строку, которую вы печатаете:

//: c11:Echo.java
// Как читать стандартный ввод.
import java.io.*;
 
public class Echo {
  public static void main(String[] args)
  throws IOException {
    BufferedReader in =
        new BufferedReader(
          new InputStreamReader(System.in));
    String s;
    while((s = in.readLine()).length() != 0)
      System.out.println(s);
    // Пустая строка прерывает выполнение программы
  }
} ///:~

Причина указания исключения в том, что readLine( ) может выбросить IOException. Обратите внимание, что System.in обычно должен быть буферизирован, как и большинство потоков.

Замена System.out на PrintWriter

System.out - это PrintStream, который является OutputStream. PrintWriter имеет конструктор, который принимает в качестве аргумента OutputStream. Таким образом, если вы хотите конвертировать System.out в PrintWriter, используйте этот конструктор:

//: c11:ChangeSystemOut.java
// Перевод System.out в PrintWriter.
import java.io.*;
 
public class ChangeSystemOut {
  public static void main(String[] args) {
    PrintWriter out = 
      new PrintWriter(System.out, true);
    out.println("Hello, world");
  }
} ///:~

Важно использовать двухаргументную версию конструктора PrintWriter и установить второй аргумент в true, чтобы позволить автоматическое освобождение буфера, в противном случае вы можете не увидеть вывода.

Перенаправление стандартного ввода/вывода

Класс Java System позволяет вам перенаправлять стандартный ввод, вывод и поток вывода ошибок, используя простой вызов статического метода:

setIn(InputStream)
setOut(PrintStream)
setErr(PrintStream)

 

 

Компрессия

Библиотека ввода/вывода Java содержит классы, поддерживающие чтение и запись потоков в компрессированном формате. Они являются оберткой для существующих классов ввода/вывода для обеспечения возможности компрессирования.

Эти классы не наследуются от классов Reader и Writer, а вместо этого они являются частью иерархии InputStream и OutputStream. Это происходит потому, что библиотека компрессии работает с байтами, а не с символами. Однако вы можете иногда встретить необходимость смешивания двух типов потоков. (Помните, что вы можете использовать InputStreamReader и OutputStreamWriter для обеспечения простой конвертации одного типа в другой.)

Классы компрессии

Функция

CheckedInputStream

GetCheckSum( ) производит контрольную сумму для любого InputStream (только не декомпрессию).

CheckedOutputStream

GetCheckSum( ) производит контрольную сумму для любого OutputStream (только не декомпрессию).

DeflaterOutputStream

Базовый класс для классов компрессии.

ZipOutputStream

DeflaterOutputStream, который компрессирует данные в файл формата Zip.

GZIPOutputStream

DeflaterOutputStream, который компрессирует данные в файл формата GZIP.

InflaterInputStream

Базовый класс для классов декомпрессии.

ZipInputStream

InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата Zip.

GZIPInputStream

InflaterInputStream, который декомпрессирует данные, хранящиеся в файле формата GZIP.

Хотя существует много алгоритмов компрессии, Zip и GZIP, вероятно, наиболее часто используемые. Поэтому вы можете легко манипулировать вашими компрессированными данными с помощью многих инструментов, существующих для чтения и записи этих форматов.

 

Сериализация объектов

Сериализация объектов Java позволяет вам взять любой объект, который реализует интерфейс Serializable и включит его в последовательность байт, которые могут быть полностью восстановлены для регенерации оригинального объекта.

Это также выполняется при передаче по сети, что означает, что механизм сериализации автоматически поддерживается на различных операционных системах. То есть, вы можете создать объект на машине с Windows, сериализовать его и послать по сети на Unix машину, где он будет корректно реконструирован. Вам не нужно будет беспокоиться о представлении данных на различных машинах, порядке следования байт и любых других деталях.

Сама по себе сериализация объектов очень интересна, потому что это позволяет вам реализовать устойчивую живучесть.

Помните, что живучесть означает, что продолжительность жизни объектов не определяется тем, выполняется ли программа — объекты живут в промежутках между вызовами программы.

Вы берете сериализованный объект и записываете его на диск, затем восстанавливаете объект при новом вызове программы, таким образом, вы способны обеспечить эффективную живучесть. Причина названия “устойчивая” в том, что вы не можете просто определить объект, используя какой-либо вид ключевого слова “устойчивый”, и позволить системе заботиться о деталях (хотя это может случиться в будущем). Вместо этого вы должны явно сериализовать и десериализовать объекты в вашей программе.

Сериализация объектов была добавлена в язык для поддержки двух главных особенностей.

Удаленный вызов методов (RMI) в Java позволяет объектам существовать на другой машине и вести себя так, как будто они существуют на вашей машине. Когда посылается сообщение удаленному объекту, необходима сериализация объекта для транспортировки аргументов и возврата значений.

Сериализация объектов так же необходима для JavaBeans. Когда используется компонент (Bean), информация о его состоянии обычно конфигурируется во время дизайна. Эта информации о состоянии должна сохранятся, а затем восстанавливаться, когда программа запускается; cериализация объектов выполняет эту задачу.

Сериализация объекта достаточно проста, если объект реализует интерфейс Serializable (этот интерфейс похож на флаг и не имеет методов).

Для сериализации объекта вы создаете определенный сорт объекта OutputStream, а затем вкладываете его в объект ObjectOutputStream.

После этого вам достаточно вызвать writeObject( ) и ваш объект будет сериализован и послан в OutputStream.

Чтобы провести обратный процесс, вы вкладываете InputStream внутрь ObjectInputStream и вызываете readObject( ). То, что приходит, обычно это ссылка на родительский Object, так что вы должны выполнить обратное приведение, чтобы сделать вещи правильными.

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

 
//: c11:Worm.java
// Демонстрация сериализации объектов.
import java.io.*;
 
class Data implements Serializable {
  private int i;
  Data(int x) { i = x; }
  public String toString() {
    return Integer.toString(i);
  }
}
 
public class Worm implements Serializable {
  // Генерируется случайно значение типа int:
  private static int r() {
    return (int)(Math.random() * 10);
  }
  private Data[] d = {
    new Data(r()), new Data(r()), new Data(r())
  };
  private Worm next;
  private char c;
  // Значение i == Номеру сегмента
  Worm(int i, char x) {
    System.out.println(" Worm constructor: " + i);
    c = x;
    if(--i > 0)
      next = new Worm(i, (char)(x + 1));
  }
  Worm() {
    System.out.println("Default constructor");
  }
  public String toString() {
    String s = ":" + c + "(";
    for(int i = 0; i < d.length; i++)
      s += d[i].toString();
    s += ")";
    if(next != null)
      s += next.toString();
    return s;
  }
  // Исключение выбрасывается на консоль:
  public static void main(String[] args) 
  throws ClassNotFoundException, IOException {
    Worm w = new Worm(6, 'a');
    System.out.println("w = " + w);
    ObjectOutputStream out =
      new ObjectOutputStream(
        new FileOutputStream("worm.out"));
    out.writeObject("Worm storage");
    out.writeObject(w);
    out.close(); // Также очищается вывод
    ObjectInputStream in =
      new ObjectInputStream(
        new FileInputStream("worm.out"));
    String s = (String)in.readObject();
    Worm w2 = (Worm)in.readObject();
    System.out.println(s + ", w2 = " + w2);
    ByteArrayOutputStream bout =
      new ByteArrayOutputStream();
    ObjectOutputStream out2 =
      new ObjectOutputStream(bout);
    out2.writeObject("Worm storage");
    out2.writeObject(w);
    out2.flush();
    ObjectInputStream in2 =
      new ObjectInputStream(
        new ByteArrayInputStream(
          bout.toByteArray()));
    s = (String)in2.readObject();
    Worm w3 = (Worm)in2.readObject();
    System.out.println(s + ", w3 = " + w3);
  }
} ///:~

Чтобы сделать пример интереснее, массив объектов Data внутри Worm инициализируется случайными числами. (Этот способ не дает компилятору представление о типе хранимой мета информации.) Каждый сегмент цепочки (Worm) помечается символом (char), который генерируется автоматически в процессе рекурсивной генерации связанного списка Worm. Когда вы создаете Worm, вы говорите конструктору необходимую вам длину. Чтобы сделать следующую ссылку (next), вызывается конструктор Worm с длиной на единичку меньше, и т.д. Последняя ссылка next остается равной null, указывая на конец цепочки Worm.

Все это сделано для создания чего-то достаточно сложного, что не может быть легко сериализовано. Однако действия, направленные на сериализацию, достаточно просты. Как только создается объект ObjectOutputStream из некоторого другого потока, writeObject( ) сериализует объект. Обратите внимание, что вызов writeObject( ) для String такой же. Вы также можете записать все примитивные типы, используя тот же метод DataOutputStream (они задействуют тот же интерфейс).

Здесь есть две различные секции кода, которые выглядят одинаково. Первая пишет и читает файл, а вторая, для разнообразия, пишет и читает ByteArray. Вы можете прочесть и записать объект, используя сериализацию для любого DataInputStream или DataOutputStream, включая, как вы увидите в Главе 15, сеть. Вывод после одного запуска имеет вид:

Worm constructor: 6
Worm constructor: 5
Worm constructor: 4
Worm constructor: 3
Worm constructor: 2
Worm constructor: 1
w = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w2 = :a(262):b(100):c(396):d(480):e(316):f(398)
Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)

Вы можете видеть, что десериализованный объект на самом деле содержит все ссылки, которые были в оригинальном объекте.

Обратите внимание, что в процессе десериализации объекта Serializable не вызывается ни конструктор, ни даже конструктор по умолчанию.

Сериализация объектов является byte-ориентированной, и поэтому используется иерархия InputStream и OutputStream.