Статьи

Встроенные методы сериализации

Эта статья является частью нашего Академического курса под названием Advanced Java .

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

1. Введение

Эта часть руководства будет посвящена исключительно сериализации : процессу перевода объектов Java в формат, который можно использовать для хранения и последующей реконструкции в той же (или другой) среде ( http: //en.wikipedia). org / wiki / Serialization ). Сериализация не только позволяет сохранять и загружать объекты Java в / из постоянного хранилища, но также является очень важным компонентом взаимодействия современных распределенных систем.

Сериализация не легка, но эффективная сериализация еще сложнее. Помимо стандартной библиотеки Java, существует много методов и платформ сериализации: некоторые из них используют компактное двоичное представление, другие ставят читабельность на первое место. Хотя мы собираемся упомянуть много альтернатив по пути, наше внимание будет сосредоточено на тех из стандартной библиотеки Java (и последние спецификации): Serializable , Externalizable , Java Architecture для привязки XML ( JAXB , JSR-222 ) и Java API для Обработка JSON ( JSON-P , JSR-353 ).

2. Сериализуемый интерфейс

Возможно, самый простой способ пометить класс как доступный для сериализации в Java — реализовать интерфейс java.io.Serializable . Например:

1
2
public class SerializableExample implements Serializable {
}

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

Сериализуемый класс может явным образом представить свой собственный UID последовательной версии , объявив поле с именем serialVersionUID которое должно быть static , final и типа long . Например:

1
2
3
public class SerializableExample implements Serializable {
    private static final long serialVersionUID = 8894f47504319602864L;  
}

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

Как только класс становится сериализуемым (реализует Serializable и объявляет serialVersionUID ), он может быть сохранен и извлечен с использованием, например, ObjectOutputStream / ObjectInputStream :

1
2
3
4
5
6
final Path storage = new File( "object.ser" ).toPath();
 
try( final ObjectOutputStream out =
        new ObjectOutputStream( Files.newOutputStream( storage ) ) ) {
    out.writeObject( new SerializableExample() );
}

После сохранения он может быть получен аналогичным образом, например:

1
2
3
4
5
try( final ObjectInputStream in =
        new ObjectInputStream( Files.newInputStream( storage ) ) ) {
    final SerializableExample instance = ( SerializableExample )in.readObject();
    // Some implementation here
}

Как мы видим, интерфейс Serializable не обеспечивает большого контроля над тем, что и как следует сериализовать (за исключением ключевого слова transient которое помечает поля как не сериализуемые). Кроме того, это ограничивает гибкость изменения представления внутреннего класса, поскольку может нарушить процесс сериализации / десериализации. Вот почему появился другой интерфейс, Externalizable .

3. Внешний интерфейс

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

1
2
3
4
public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

В свою очередь, каждый класс, который реализует интерфейс Externalizable должен обеспечивать реализацию этих двух методов. Давайте посмотрим на пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class ExternalizableExample implements Externalizable {
    private String str;
    private int number;
    private SerializableExample obj;
         
    @Override
    public void readExternal(final ObjectInput in)
            throws IOException, ClassNotFoundException {
        setStr(in.readUTF());
        setNumber(in.readInt());
        setObj(( SerializableExample )in.readObject());
    }
     
    @Override
    public void writeExternal(final ObjectOutput out)
            throws IOException {
        out.writeUTF(getStr());
        out.writeInt(getNumber());
        out.writeObject(getObj());
    }
}

Аналогично классам, реализующим Serializable , классы, реализующие Externalizable могут храниться и извлекаться с использованием, например, ObjectOutputStream / ObjectInputStream :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
final Path storage = new File( "extobject.ser" ).toPath();
         
final ExternalizableExample instance = new ExternalizableExample();
instance.setStr( "Sample String" );
instance.setNumber( 10 );
instance.setObj( new SerializableExample() );
         
try( final ObjectOutputStream out =
        new ObjectOutputStream( Files.newOutputStream( storage ) ) ) {
    out.writeObject( instance );
}
         
try( final ObjectInputStream in =
        new ObjectInputStream( Files.newInputStream( storage ) ) ) {
    final ExternalizableExample obj = ( ExternalizableExample )in.readObject();
    // Some implementation here
}

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

4. Подробнее о Сериализуемый интерфейс

В предыдущем разделе мы упоминали, что интерфейс Serializable не обеспечивает большого контроля над тем, что и как следует сериализовать. На самом деле, это не совсем верно (по крайней мере, когда используются ObjectOutputStream / ObjectInputStream ). Существуют некоторые специальные методы, которые любой сериализуемый класс может реализовать для управления сериализацией и десериализацией по умолчанию.

1
private void writeObject(ObjectOutputStream out) throws IOException;

Этот метод отвечает за запись состояния объекта для его конкретного класса, чтобы соответствующий метод readObject мог его восстановить (механизм по умолчанию для сохранения полей объекта можно вызвать, вызвав out.defaultWriteObject ).

1
private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundException;

Этот метод отвечает за чтение из потока и восстановление состояния объекта (механизм по умолчанию для восстановления полей объекта можно вызвать, вызвав in.defaultReadObject ).

1
private void readObjectNoData() throws ObjectStreamException;

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

1
Object writeReplace() throws ObjectStreamException;

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

1
Object readResolve() throws ObjectStreamException;

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

Механизм сериализации по умолчанию (использующий интерфейс Serializable ) может стать очень громоздким в Java, если вы знаете внутренние детали реализации и те специальные методы, которые нужно использовать. Чем больше кода вы пишете для поддержки сериализации, тем больше будет ошибок и уязвимостей.

Тем не менее, есть способ уменьшить эти риски, используя довольно простой шаблон с именем Serialization Proxy , который основан на использовании writeReplace и readResolve . Основная идея этого шаблона состоит в том, чтобы ввести выделенный сопутствующий класс для сериализации (обычно как private static внутренний класс), который дополняет класс, необходимый для сериализации. Давайте посмотрим на этот пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class SerializationProxyExample implements Serializable {
    private static final long serialVersionUID = 6163321482548364831L;
 
    private String str;
    private int number;       
     
    public SerializationProxyExample( final String str, final int number) {
        this.setStr(str);
        this.setNumber(number);
    }
 
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException( "Serialization Proxy is expected" );
    }
     
    private Object writeReplace() {
        return new SerializationProxy( this );
    }
     
    // Setters and getters here
}

Когда экземпляры этого класса сериализуются, реализация класса SerializationProxyExample предоставляет взамен объект замены (экземпляр класса SerializationProxy ). Это означает, что экземпляры класса SerializationProxyExample никогда не будут сериализованы (и десериализованы) напрямую. Это также объясняет, почему метод readObject вызывает исключение в случае, если попытка десериализации каким-либо образом происходит. Теперь давайте взглянем на сопутствующий класс SerializationProxy :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private static class SerializationProxy implements Serializable {
    private static final long serialVersionUID = 8368440585226546959L;
 
    private String str;
    private int number;
         
    public SerializationProxy( final SerializationProxyExample instance ) {
        this.str = instance.getStr();
        this.number = instance.getNumber();
    }
         
    private Object readResolve() {
        return new SerializationProxyExample(str, number); // Uses public constructor
    }
}

В нашем несколько упрощенном случае класс SerializationProxy просто дублирует все поля SerializationProxyExample (но это может быть намного сложнее). Следовательно, когда десериализуются экземпляры этого класса, readResolve метод readResolve и SerializationProxy предоставляет замену, на этот раз в форме экземпляра SerializationProxyExample . Таким образом, класс SerializationProxy служит прокси-сервером сериализации для класса SerializationProxyExample .

5. Сериализуемость и удаленный вызов метода (RMI)

В течение некоторого времени Java Remote Method Invocation ( RMI ) был единственным механизмом, доступным для создания распределенных приложений на платформе Java. RMI обеспечивает всю тяжелую работу и позволяет прозрачно вызывать методы удаленных объектов Java из других JVM на одном и том же хосте или на разных физических (или виртуальных) хостах. В основе RMI лежит сериализация объекта, которая используется для маршалирования (сериализации) и демаршализации (десериализации) параметров метода.

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

6. JAXB

Архитектура Java для XML Binding, или просто JAXB , вероятно, является старейшим альтернативным механизмом сериализации, доступным для разработчиков Java. Ниже он использует XML в качестве формата сериализации, предоставляет широкий спектр опций настройки и включает в себя множество аннотаций, что делает JAXB очень привлекательным и простым в использовании (аннотации рассматриваются в части 5 учебного пособия, Как и когда использовать Enums и Аннотации ).

Давайте рассмотрим довольно упрощенный пример простого старого Java-класса (POJO), аннотированного аннотациями JAXB :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
 
@XmlAccessorType( XmlAccessType.FIELD )
@XmlRootElement( name = "example" )
public class JaxbExample {
    @XmlElement(required = true) private String str;
    @XmlElement(required = true) private BigDecimal number;
     
    // Setters and getters here
}

Чтобы сериализовать экземпляр этого класса в формат XML с использованием инфраструктуры JAXB , требуется только экземпляр маршаллера (или сериализатора), например:

01
02
03
04
05
06
07
08
09
10
final JAXBContext context = JAXBContext.newInstance( JaxbExample.class );       
final Marshaller marshaller = context.createMarshaller();
      
final JaxbExample example = new JaxbExample();
example.setStr( "Some string" );
example.setNumber( new BigDecimal( 12.33d, MathContext.DECIMAL64 ) );
         
try( final StringWriter writer = new StringWriter() ) {
    marshaller.marshal( example, writer );
}

Вот XML-представление JaxbExample класса JaxbExample из приведенного выше примера:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<example>
    <str>Some string</str>
    <number>12.33000000000000</number>
</example>

Следуя тому же принципу, экземпляры класса могут быть десериализованы обратно из представления XML в объекты Java с использованием экземпляра unmarshaller (или десериализатора), например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
final JAXBContext context = JAXBContext.newInstance( JaxbExample.class );
         
final String xml = "" +
    "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\" standalone=\\"yes\\"?>" +
    "<example>" +
    "    <str>Some string</str>" +
    "    <number>12.33000000000000</number>" +
    "</example>";
         
final Unmarshaller unmarshaller = context.createUnmarshaller();
try( final StringReader reader = new StringReader( xml ) ) {
    final JaxbExample example = ( JaxbExample )unmarshaller.unmarshal( reader );
    // Some implementaion here
}

Как мы видим, JAXB довольно прост в использовании, и формат XML по-прежнему остается довольно популярным в наши дни. Однако одной из фундаментальных ошибок XML является многословность: довольно часто необходимые структурные элементы XML значительно превосходят эффективную полезную нагрузку данных.

7. JSON-P

С 2013 года разработчики Java могут использовать JSON в качестве формата сериализации благодаря недавно представленному Java API для обработки JSON ( JSON-P ).

На данный момент JSON-P не является частью стандартной библиотеки Java, хотя существует много дискуссий о включении встроенной поддержки JSON в язык в следующем выпуске Java 9 ( http://openjdk.java.net/jeps/198 ). Тем не менее, он есть и доступен как часть реализации ссылки на обработку JSON Java ( https://jsonp.java.net/ ).

В отличие от JAXB , к классу не нужно ничего добавлять, чтобы сделать его пригодным для сериализации JSON , например:

1
2
3
4
5
public class JsonExample {
    private String str;
    private BigDecimal number;
    // Setters and getters here
}

Сериализация не такая прозрачная, как в JAXB , и требует написания небольшого количества кода для каждого класса, предназначенного для сериализации в JSON , например:

01
02
03
04
05
06
07
08
09
10
11
12
final JsonExample example = new JsonExample();
example.setStr( "Some string" );
example.setNumber( new BigDecimal( 12.33d, MathContext.DECIMAL64 ) );
         
try( final StringWriter writer = new StringWriter() ) {
    Json.createWriter(writer).write(
        Json.createObjectBuilder()
            .add("str", example.getStr() )
            .add("number", example.getNumber() )
            .build()
        );
}

А вот JSON- представление JsonExample класса JsonExample из приведенного выше примера:

1
2
3
4
{
    "str":"Some string",
    "number":12.33000000000000
}

Процесс десериализации идет в том же духе:

1
2
3
4
5
6
7
8
final String json = "{\\"str\\":\\"Some string\\",\\"number\\":12.33000000000000}"
       
try( final StringReader reader = new StringReader( json ) ) {
    final JsonObject obj = Json.createReader( reader ).readObject();
    final JsonExample example = new JsonExample();
    example.setStr( obj.getString( "str" ) );
    example.setNumber( obj.getJsonNumber( "number" ).bigDecimalValue() );
}

Справедливо сказать, что на данный момент поддержка JSON в Java довольно проста. Тем не менее, это хорошая вещь, и сообщество Java работает над расширением поддержки JSON , представляя Java API для JSON Binding (JSON-B, JSR-367 ). С этим API сериализация и десериализация объектов Java в / из JSON должны быть такими же прозрачными, как и в JAXB .

8. Стоимость сериализации

Очень важно понимать, что, хотя сериализация / десериализация выглядит просто в Java, она не бесплатна и в зависимости от модели данных и шаблонов доступа к данным может потребовать довольно много пропускной способности сети, памяти и ресурсов ЦП. Более того, несмотря на то, что в Java есть какая-то поддержка версий для сериализуемых классов (с использованием серийной версии UID, как мы видели в разделе « Сериализуемый интерфейс» ), это значительно усложняет процесс разработки, поскольку разработчики сами по себе выясняют, как управлять развитием модели данных.

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

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

9. Помимо стандартной библиотеки Java и спецификаций

В этом разделе мы рассмотрим альтернативные решения для безболезненной и эффективной сериализации Java, начиная с проекта быстрой сериализации ( http://ruedigermoeller.github.io/fast-serialization/ ): быстрая замена сериализации Java. , Использование быстрой сериализации не сильно отличается от того, что обеспечивает стандартная библиотека Java, но заявляет, что оно намного быстрее и эффективнее.

Другой набор структур имеет другой взгляд на проблему. Они основаны на определении структурированных данных (или протоколе) и сериализуют данные в компактное двоичное представление (соответствующая модель данных может быть даже сгенерирована из определения). Кроме того, эти платформы выходят далеко за рамки просто платформы Java и могут использоваться для межязыковой / межплатформенной сериализации. Наиболее известными библиотеками Java в этом пространстве являются буфер протокола Google ( https://developers.google.com/protocol-buffers/ ), Apache Avro ( http://avro.apache.org/ ) и Apache Thrift ( https: /). /thrift.apache.org/ ).

10. Что дальше

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

11. Скачать исходный код

Вы можете скачать исходный код этого курса здесь: advanced-java-part-10