63471

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

Лекция

Информатика, кибернетика и программирование

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

Русский

2014-06-20

96 KB

0 чел.

I. Сериализация объектов                                                                                                          Лекция 6.

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

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

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

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

Для сериализации объекта вы создаете определенный сорт объекта OutputStream, а затем вкладываете его в объект ObjectOutputStream. После этого вам достаточно вызвать writeObject ( ) и ваш объект будет сериализован и послан в OutputStream. Чтобы провести обратный процесс, вы вкладываете InputStream внутрь ObjectOutputStream и вызываете readObject ( ). То, что приходит, обычно это ссылка на родительский Object, так что вы должны выполнить обратное приведение, чтобы сделать вещи правильными.

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

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

Сериализация объектов поддерживает байтовый ввод/вывод, поэтому используются потоки InputStream и OutputStream.

Запись объекта:

Worm w = new Worm (6,’a’);

System.out.println (“w =”  + w);

ObjectOutputStream out = new ObjectOutputStream (

                        new  FileOutputStream (“worm.out”));

out.writeObject ( “Worm strorage” );

out.writeObject ( w );

out.close ( );

Чтение объекта:

ObjectInputStream in = new ObjectInputStream (

                     new  FileInputStream (“worm.out”));

String s = (String) in.redObject ( );

Worm w2 = (Worm) in.readObject ( );

In.close ( );

Нахождение класса

Вы можете задаться вопросом, что необходимо для восстановления объекта из его сериализованного состояния. Например, предположим, вы сериализовали объект и послали его в файл или по сети на другую машину. Может ли программа на другой машине реконструировать объект, используя только содержимое файла?

Если вы собираетесь выполнять с восстановленным объектом какие-либо действия, вы должны убедиться, что JVM может найти соответствующий .class либо по локальному пути классов, либо где-то в Internet. Поместите его в переменную окружения CLASSPATH.

Управление процессом сериализации

Как вы можете видеть, стандартный механизм сериализации тривиален в использовании. Но что, если вам нужны специальные требования? Может быть, вы имеете особые требования по безопасности и вы не хотите сериализовать часть вашего объекта, или, может быть, не имеет смысла сериализовать один из подобъектов, если эта часть вашего объекта будет вновь создана при восстановлении объекта.

Вы можете управлять процессом сериализации, реализовав интерфейс Externalizable вместо интерфейса Serializable. Интерфейс Externalizable расширяет интерфейс Serializable и добавляет два метода:

writeExternal ( ) и  readExternal ( ), которые автоматически вызываются для вашего объекта во время сериализации и десериализации, так что вы можете выполнить специальные операции.

!!! При восстановлении вызывается конструктор по умолчанию. Это отличается от восстановления объекта реализующего интерфейс Serializable, в котором конструирование целиком происходит из сохраненных бит без вызова конструктора. Для объектов Externalizable происходит нормальный процесс конструирования (включая инициализацию в точке определения полей), а затем вызывается readExternal ( ).

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

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

import java.io.*;

import java.util.*;

class Blip3 iplements Externalizable {

   int i;

   String s; / / Без инициализации

   public Blip3 ( )   {

        System. out.printn (“Blip3 Constructor”);

        / / s, i не инициализируется

   }

   public Blip3 (String x, int a )   {

        System. out.printn (“Blip3 (String x, int a )”);

        s = x;

        i = a;

        / / s & i инициализируются только в

       / / конструкторе не по умолчанию.

   }

   public String toString ( )   { return s + I; }

   public void writeExternal (ObjectOutput out) throws IOException  {

        System.out.println (“Blip3.writeExternal”);

        / / Вы обязаны сделать это:

         out.writeObject (s);

}

public void readExternal (ObjectInput in)

            throws IOException, ClassNotFoundException {

  System.outprintln ( “Blip3.readExternal” );

 / / Вы обязаны сделать это:

s = (String) in.readObject ( );

 i = in.read ( );

}

public static void main ( String [ ] args )

            throws IOException,  ClassNotFoundException {

   System.out.println ( “Constructing objects: “);

   Blip3 b3 = new Blip3 ( “A String “,  47);

   System.out.println ( b3 );

   ObjectOutputStream o =

         new  ObjectOutputStream (  new FileOutputStream ( “Blip3.out” ));

   System.out.println ( “ Saving object: “);

   o.writeObject ( b3 );

   o.close ( );

   / / Теперь получим обратно:

   ObjectInputStream in =

         new  ObjectInputStream (  new FileInputStream ( “Blip3.out” ));

   System.out.println ( “ Recovering b3: “ );

   b3 = ( Blip3 )  in.readObject ( );

   System.out.println ( b3 );

   in.close ( );

 }

}

Поля s и i инициализируются только во втором конструкторе, но не в конструкторе по умолчанию. Это значит, что если вы не инициализируете s и i в readExternal ( ), они будут равны null (так как хранилище объектов заполняется нулями при первом шаге создания объектов). Если вы закомментируете две строки кода, следующих за фразой «вы обязаны сделать это», и запустите программу, вы увидите, что при восстановлении объекта s равно null , а i равно нулю.

 

Если вы наследуете от объекта с интерфейсом Externalizable, обычно вы будете вызывать методы writeExternal ( ) и  readExternal ( ) базового класса для обеспечения правильного хранения,  восстановления компонент базового класса.

Таким образом, чтобы все сделать правильно, вы должны не только записать важные данные из объектов в методе writeExternal ( ) (для объектов Externalizable не существует автоматической записи объектов-членов), но и восстановить эти данные в методе readExternal ( ) . Сначала можно запутаться и подумать, что из-за вызова конструктора по умолчанию объекта все необходимые действия по записи и восстановлению объектов Externalizable происходят сами по себе. Но это не так.

  

Ключевое слово transient

Когда вы управляете сериализацией, возможно появление определенных подобъектов, для которых вы не захотите применять механизм сериализации Java для автоматического сохранения и восстановления. В основном это происходит в том случае, если такой подобъект представляет важную информацию, которую вы не хотите сериализовать, например, пароль. Даже если такая информация имеет модификатор private в объекте, она сериализуется, и кто-либо может получить доступ для чтения файла или перехватить сетевую передачу.

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

Однако если вы работаете с Serializable объектом, то вся сериализация происходит автоматически (что удобно). Для управления этим, вы можете выключить сериализацию полей индивидуально, используя ключевое слово transient, которое говорит: «Не беспокойтесь о сохранении и восстановлении этого – я позабочусь об этом».

Так как объекты с интерфейсом Externalizable не сохраняют никакие из своих полей автоматически, поэтому ключевое слово transient используется только для объектов с интерфейсом Serializable.

Альтернатива интерфейсу Externalizable 

Если вы недостаточно сильны в реализации интерфейса Externalizable, существует другой подход. Вы можете реализовать интерфейс Serializable и добавить (обратите внимание, «добавить», а не «перекрыть» или «реализовать») методы, называемые writeObject ( ) и readObject ( ), которые будут автоматически вызваны, когда объект будет, соответственно, сериализоваться и десериализоваться. То есть, если вы обеспечите эти два метода, они будут использоваться взамен сериализации по умолчанию.

Методы должны иметь следующие точные сигнатуры:

private void

   writeObject (ObjectOutputStream stream)

      throws IOException;

private void

   readObject (ObjectInputStream stream)

      throws IOException, ClassNotFoundException

С точки зрения проектирования, это мистические вещи. Прежде всего, вы можете подумать так, потому что эти методы не являются частью базового класса или интерфейса, следовательно, они не будут определены в своем собственном интерфейсе. Но обратите внимание, что они объявлены как private, что означает, что они будут вызываться только другим членом этого класса. Однако на самом деле вы не вызываете их из других членов этого класса, в вместо этого методы writeObject ( ) и readObject ( ), принадлежащие объекту ObjectOutputStream и ObjectInputStream, вызывают методы writeObject ( ) и readObject ( ) вашего объекта. Вы можете быть удивлены, как объекты ObjectOutputStream и ObjectInputStream получают доступ к private методам вашего класса. Мы можем только иметь в виду, что эта часть составляет магию сериализации.

В любом случае, все, что определено в интерфейсе, автоматически становится public, поэтому, если writeObject ( ) и readObject ( ) должны быть private, то они не могут быть частью интерфейса. Так как вы должны следовать точным сигнатурам, получаемый эффект тот же самый, как если бы вы реализовывали interface.

Может показаться, что когда вы вызываете ObjectOutputStream.writeObject ( ), объект с интерфейсом Serializable, который вы передаете, опрашиваете (используя рефлексию, не имеет значения) на предмет реализации своего собственного writeObject ( ). Если это так, то нормальный процесс сериализации пропускается, и вызывается writeObject ( ). Аналогичная ситуация наблюдается и для readObject ( ).

Есть еще одна хитрость. Внутри вашего writeObject ( ) вы можете выбрать выполнение стандартного действия writeObject ( ), вызвав defaultWriteObject ( ). Точно также, внутри readObject ( ) вы можете вызвать defaultReadObject ( ). Вот пример, который демонстрирует, как вы можете управлять хранением и восстановлением объектов с интерфейсом Serializable.

import java.io.*;

public class SerialCtl implements Serializable {

     String a;

     transient  String b;

     public SerialCtl ( String aa, String bb )  {

        a = “Not Transient: “ + aa;

        b = “Transient: ” + bb;

    }

    public String toString ( )   {

        return a + “\n” +b;

    }

    private void

      

writeObject ( ObjectInputStream stream )

           throws IOException  {

       stream. defaultWriteObject ( );

       stream. writeObject ( b );

  }

  private void

     readObject ( ObjectInputStream stream )

           throws IOException , ClassNotFoundException  {

       stream. defaultReadObject ( );

      b = (String) stream.readObject ( );

  }

  public static void main ( String [ ] args )

   throws IOException , ClassNotFoundException  {

       SerialCtl  sc =

            new SerialCtl ( “Test1”, “Test2” );

       System.out.println ( “Before: \n” + sc );

       ByteArrayOutputStream buf =

            new ByteArrayOutputStream ( );

       ObjectOutputStream o =

            new ObjectOutputStream ( buf );                

       o.writeObject ( sc );

       / / Теперь получим это назад:

       ObjectInputStream in =

            new ObjectInputStream (

               new ByteArrayInputStream (

                   buf.to ByteArray ( ) ) );

       SerialCtl sc2 = (SerialCtl ) in.readObject ( );

       System.out.println ( “After: \n” + sc2 );

   }

 }

В этом примере есть одно обычное поле String, а другое имеет модификатор transient, для обеспечения возможности сохранения не transient поля с помощью defaultWriteObject ( ), а поля сохраняются и восстанавливаются явно. Поля инициализируются внутри конструктора, а не в точке определения, чтобы удостоверится, что они не инициализируются каким-либо автоматическим механизмом во время десериализации.   

       

Если вы будете использовать стандартный механизм записи не transient частей вашего объекта, вы должны вызвать defaultWriteObject ( ), как первое действие writeObject ( ) и defaultReadObject ( ), как первое действие readObject ( ). Это странный вызов методов. Он может показать, например, что вы вызываете defaultWriteObject ( ) для ObjectOutputStream и не передаете ему аргументов, но все же как-то происходит включение и узнавание ссылки на ваш объект и способа записи всех не transient частей. Мираж.  

Для хранения и восстановления transient объектов используется более знакомый код. И еще, подумайте о том, что происходит тут. В main ( ) создается объект SerialCtl, а затем сериализуется в ObjectOutputStream. (Обратите внимание, что в этом случае используется буфер вместо файла – это все тот же ObjectOutputStream.) Сериализация происходит в строке:

    o.writeObject ( sc );

Метод writeObject ( ) должен проверить sc на предмет существования собственного метода writeObject ( ). (Не с помощью проверки интерфейса – здесь его нет – или типа класса, а реальной охотой за методом, используя рефлексию.) Если метод существует, он используется. Аналогичный подход используется для readObject ( ). Возможно это чисто практический способ, которым можно решить проблему, но  он, несомненно, странен.

Использование постоянства

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

два объекта, оба из которых имеют ссылки на один объект? Когда вы восстановите эти два объекта их  сериализованного состояния, будете ли вы иметь только один экземпляр третьего объекта? Что, если вы сериализуете два объекта в различные файлы, а десериализуете их в различных частях кода?

Пример, показывающий эту проблему:

Объекты Animal содержат поля типа House. Создается ArrayList из этих Animal, и он сериализуется дважды в один поток, а затем снова в другой поток. Когда это десериализуется и распечатается, вы увидите следующий результат одного запуска (объекты будут располагаться в разных участках памяти при каждом запуске ):

       animals: [Bosco the dog [Animal@1cc76c], House@1cc769

       , Ralph the hamster [Animal@1cc76d], House@1cc769

       , Fronk the cat [Animal@1cc76e], House@1cc769

       ]

       animals1: [Bosco the dog [Animal@1cca0c], House@1cca16

       , Ralph the hamster [Animal@1cca17], House@1cca16

       , Fronk the cat [Animal@1cca1b], House@1cca16

       ]

       animals2: [Bosco the dog [Animal@1cca0c], House@1cca16

       , Ralph the hamster [Animal@1cca17], House@1cca16

       , Fronk the cat [Animal@1cca1b], House@1cca16

       ]

       animals3: [Bosco the dog [Animal@1cca52], House@1cca5c

       , Ralph the hamster [Animal@1cca5d], House@1cca5c

       , Fronk the cat [Animal@1cca61], House@1cca5c

       ]

Конечно, вы ожидаете, что десериализованные объекты имеют адреса, отличные от первоначальных. Но обратите внимание, что в animals1 и animals2 появляется один и тот же адрес, включая ссылки на объект House, который они оба разделяют. С другой стороны, когда восстанавливается animals3, у системы нет способа узнать, что объекты в этом потоке являются элайэсами объектов первого потока, так что при этом создается полностью отдельная сеть объектов.

Если вы сериализовали что-то в единственный поток, у вас есть гарантии восстановить ту же сеть объектов, которую вы записали, без случайного дублирования объектов. Конечно, вы можете изменить состояние ваших объектов в промежутке между первой и последней записи, но это ваше дело – объекты будут записаны независимо от того, в каком бы состоянии они не были ( и со всеми соединениями, которые они имеют с другими объектами) в то время, когда вы сериализуете их.

Самым безопасным для сохранения состояния системы является сериализация, как «атомарная» операция. Если вы сериализуете какие-то вещи, выполняете какую-то работу и сериализуете еще, и т.д., то вы не будете держать систему в безопасности. Вместо этого поместите все объекты, которые относятся к состоянию вашей системы, в единственный контейнер и просто запишите этот контейнер в одной операции. Затем вы можете восстановить его так же единственным методом вызова.

static поля  

Сериализацию statics полей необходимо проводить самим

Именно для этой цели предназначены статические методы serializeStaticState ( ) и deserializeStaticState ( ). При этом они явно вызываются как часть процесса сохранения и восстановления. (Обратите внимание, что порядок записи в файл сериализации и чтения из него должен сохраняться.)

abstract  class Shape implements Serializable  {

          public static final int

               RED = 1, BLUE = 2, GREEN = 3;

          private int xPos, yPos, dimension;

          private static Random r = new Random ( );

          private static int counter = 0;

          abstract public void setColor ( int newColor );

          abstract public int getColor ( );

          public Shape ( int xVal, int yVal, int dim )  {

               xPos = xVal;

               yPos = yVal;

               dimension = dim;

          }

          public String toString ( )  {

               return getClass ( )  +

                   “ color [ “ + getColor ( )  +

                   “] xPos [ “ + xPos  +

                   “] yPos [ “ + yPos  +

                   “] dim [ “ + dimension  + “] \n“;

         }

   }

 

 class Line extends Shape   {

        private static int  color = RED;

        public static void

        serializeStaticState ( ObjectOutputStream  os )

               throws IOException  {

          os.writeInt ( );

  }

  public static void

  deserializeStaticState ( ObjectInputStream  os )

               throws IOException  {

      color = os.readInt ( );

  }

. . .

}

    . . .          

    out.writeObject ( lineObject1 );

    Line.serializeStaticState ( out );

    . . .

    lineObject2 = ( ArrayList ) in.readObject ( );

    Line.deserializeStaticState ( in );

Так же стоит подумать о безопасности, ведь сериализация сохраняет поля с модификатором private. Если вы имеете проблемы безопасности, эти поля должны помечаться, как transient. Затем вы должны разработать безопасный способ для хранения такой информации, чтобы при восстановлении данных вы могли установить эти private поля.

Задания к практическим занятиям:

1. Создайте бин с интерфейсом Serializable с полями: - обычным, - static, - tansient . Реализуйте сериализацию в файл.

2. Создайте бин с интерфейсом Externalizable , содержащий поля из задания 1. Реализуйте сериализацию в файл.

3. Создайте 3 бина. Два из них должны содержать ссылку на третий. Сериализуйте первый из них в файл, измените состояние третьего и сериализуйте второй бин в файл. Проведите десериализацию. Что получилось? Проведите сериализацию атомарным способом.


 

А также другие работы, которые могут Вас заинтересовать

41860. Окислительно-восстановительное титрование. Иодометрическое определение пероксида водорода. Иодометрическое определение растворённого в воде кислорода 65.63 KB
  Сформировать умения по стандартизации раствора тиосульфата натрия; выполнению иодометрического определения пероксида водорода; иодометрического определения растворенного в воде кислорода. При этом к определяемому веществу добавляют взятое в заведомом избытке точное количество стандартного раствора иода. Какую среду сильнокислую слабокислую должен иметь раствор после добавления серной кислоты Почему при добавлении крахмала амилозы к раствору иода появляется синее окрашивание Какие ещё вещества могут взаимодействовать с иодом...
41861. Определение удельной теплоты плавления олова 286.55 KB
  Температура при которой вещество плавится называется температурой плавления вещества. Температура плавления для данного вещества при одинаковых условиях одинакова. Однако это не значит что в процессе плавления к телу не надо подводить энергию.
41862. Диаграмма Парето 48.04 KB
  Например если на складе находится большое число деталей проводить контроль всех деталей без всякого различия неэффективно. Но если разделить детали на группы по их стоимости то на долю группы наиболее дорогих деталей группа А составляющих 2030 от общего числа деталей придётся 7080 от общей стоимости всех деталей. На долю группы самых дешёвых деталей группа С составляющей 4050 от всего количества деталей придётся всего 510 от общей стоимости. Контроль деталей на складе будет эффективным если контроль деталей группы А будет...
41863. Редактирование рабочей книги. Построение диаграмм 976.65 KB
  Изучение способов работы с данными в ячейке. Изучение возможностей автозаполнения. Построение диаграмм. Создание и сохранение таблицы (рабочей книги). Форматирование содержимого ячеек, выбор диапазона ячеек и работа с ними, редактирование содержимого ячеек.
41864. Функционально-стоимостной анализ в конструкторской подготовке производства 296 KB
  Функционально-стоимостной анализ — метод, позволяющий отбирать наилучшие технические решения при создании и освоении новой техники (технологии), увязывать в единый комплекс вопросы обеспечения функциональной полезности и качества новой техники (технологии)
41865. Гистограмма. Характерные типы гистограмм 82 KB
  Результаты измерений вводим в электронную таблицу. В ячейку А1 вводим заголовок работы. Начиная с ячейки А3 вводим в столбец порядковые номера измерений с 1 по 100 например при помощи команды ПравкаЗаполнитьПрогрессия . В ячейки В3:В102 вводим значения коэффициента деформации из табл.
41866. Графики. Построение и виды графиков. 53.14 KB
  На третьем шаге вводим заголовки диаграммы и осей основные линии сетки по осям удаляем легенду. Характер изменения выручки а также прогноз даёт линия тренда построить которую можно открыв контекстное меню на ломаной линии и выбрав команду Добавить линию тренда. В открывшемся диалоговом окне на вкладке Тип показаны возможные типы линии тренда. Чтобы выбрать тип линии наилучшим образом аппроксимирующий данные можно поступить следующим образом: поместить на диаграмме линии тренда всех приемлемых типов т.
41867. Построение фигур и линий в CorelDRAW. Рабочая среда и интерфейс пользователя 549.49 KB
  Модель кривой В основе принятой в CorelDRW модели линий лежат два понятия: узел и сегмент. По характеру предшествующих сегментов выделяют три типа узлов: начальный узел незамкнутой кривой прямолинейный и криволинейный. В средней части строки состояния для кривой выводится обозначение класса объекта Кривая на слое 1 а также количество узлов этой кривой. Вместо этого задается расположение узлов будущей кривой и появляется возможность уже в процессе построения воздействовать на положение направляющих точек в каждом из них.