Java Language
Сериализация
Поиск…
Вступление
Java предоставляет механизм, называемый сериализации объектов, где объект может быть представлен как последовательность байтов, которая включает в себя данные объекта, а также информацию о типе объекта и типах данных, хранящихся в объекте.
После того, как сериализованный объект был записан в файл, он может быть прочитан из файла и десериализован, то есть информация о типе и байты, которые представляют объект и его данные, могут использоваться для воссоздания объекта в памяти.
Базовая сериализация в Java
Что такое сериализация
Сериализация - это процесс преобразования состояния объекта (включая его ссылки) в последовательность байтов, а также процесс перестройки этих байтов в живой объект в будущем. Сериализация используется, когда вы хотите сохранить объект. Он также используется Java RMI для передачи объектов между JVM, либо как аргументы в вызове метода от клиента к серверу, либо как возвращаемые значения из вызова метода, либо как исключения, создаваемые удаленными методами. В общем случае сериализация используется, когда мы хотим, чтобы объект существовал за время существования JVM.
java.io.Serializable
- это интерфейс маркера (не имеет тела). Он просто используется, чтобы «маркировать» классы Java как сериализуемые.
Сериализация runtime связывает каждый сериализуемый класс с номером версии, называемым serialVersionUID
, который используется во время де- сериализации для проверки того, что отправитель и получатель сериализованного объекта загружают классы для этого объекта, которые совместимы с сериализацией. Если получатель загрузил класс для объекта с другим serialVersionUID
чем класс соответствующего класса отправителя, то десериализация приведет к InvalidClassException
. Сериализуемый класс может объявить свой собственный serialVersionUID
явно, объявив поле с именем serialVersionUID
которое должно быть static, final,
и long
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
Как сделать класс подходящим для сериализации
Чтобы сохранить объект, соответствующий класс должен реализовать интерфейс java.io.Serializable
.
import java.io.Serializable;
public class SerialClass implements Serializable {
private static final long serialVersionUID = 1L;
private Date currentTime;
public SerialClass() {
currentTime = Calendar.getInstance().getTime();
}
public Date getCurrentTime() {
return currentTime;
}
}
Как написать объект в файл
Теперь нам нужно записать этот объект в файловую систему. Для этой цели мы используем java.io.ObjectOutputStream
.
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
public class PersistSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = new SerialClass(); //We will write this object to file system.
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(time); //Write byte stream to file system.
out.close();
} catch(IOException ex){
ex.printStackTrace();
}
}
}
Как воссоздать объект из его сериализованного состояния
Сохраненный объект может быть прочитан из файловой системы позже, используя java.io.ObjectInputStream
как показано ниже:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;
public class ReadSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = null;
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
time = (SerialClass)in.readObject();
in.close();
} catch(IOException ex){
ex.printStackTrace();
} catch(ClassNotFoundException cnfe){
cnfe.printStackTrace();
}
// print out restored time
System.out.println("Restored time: " + time.getTime());
}
}
Сериализованный класс находится в двоичной форме. Отмена десериализации может быть проблематичной, если определение класса изменяется: подробности см. В главе «Версии Serialized Objects» спецификации Java Serialization .
Сериализация объекта сериализует весь граф объектов, который является корнем, и работает корректно в присутствии циклических графов. Метод reset()
предоставляется, чтобы заставить ObjectOutputStream
забыть об объектах, которые уже были сериализованы.
Переходные поля - Сериализация
Сериализация с помощью Gson
Сериализация с Gson проста и выведет правильный JSON.
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(Сериализация)
//Skills
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");
//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);
//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}
Обратите внимание, что вы не можете сериализовать объекты с круговыми ссылками, поскольку это приведет к бесконечной рекурсии.
(Десериализация)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Сериализация с помощью Jackson 2
Ниже приведена реализация, демонстрирующая, как объект может быть сериализован в соответствующую строку JSON.
class Test {
private int idx;
private String name;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Сериализация:
Test test = new Test();
test.setIdx(1);
test.setName("abc");
ObjectMapper mapper = new ObjectMapper();
String jsonString;
try {
jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
System.out.println(jsonString);
} catch (JsonProcessingException ex) {
// Handle Exception
}
Выход:
{
"idx" : 1,
"name" : "abc"
}
Вы можете опустить Default Pretty Printer, если вам это не нужно.
Используемая здесь зависимость выглядит следующим образом:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
Пользовательская сериализация
В этом примере мы хотим создать класс, который будет генерировать и выводить на консоль случайное число между двумя целыми числами, которые передаются в качестве аргументов во время инициализации.
public class SimpleRangeRandom implements Runnable {
private int min;
private int max;
private Thread thread;
public SimpleRangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
private void WriteObject(ObjectOutputStreamout) throws IO Exception;
private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Теперь, если мы хотим сделать этот класс Serializable, возникнут некоторые проблемы. Thread является одним из определенных классов системного уровня, которые не являются Serializable. Поэтому нам нужно объявить поток как переходный . Сделав это, мы сможем сериализовать объекты этого класса, но у нас все еще будет проблема. Как вы можете видеть в конструкторе, мы устанавливаем минимальные и максимальные значения нашего рандомизатора, после чего начинаем поток, который отвечает за создание и печать случайного значения. Таким образом, при восстановлении сохраненного объекта вызовом readObject () конструктор не будет запускаться снова, поскольку нет создания нового объекта. В этом случае нам нужно разработать пользовательскую сериализацию , предоставив два метода внутри класса. Эти методы:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Таким образом, добавив нашу реализацию в readObject (), мы можем инициировать и запускать наш поток:
class RangeRandom implements Serializable, Runnable {
private int min;
private int max;
private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized
public RangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
thread = new Thread(this);
thread.start();
}
}
Вот основной пример нашего примера:
public class Main {
public static void main(String[] args) {
System.out.println("Hello");
RangeRandom rangeRandom = new RangeRandom(1,10);
FileOutputStream fos = null;
ObjectOutputStream out = null;
try
{
fos = new FileOutputStream("test");
out = new ObjectOutputStream(fos);
out.writeObject(rangeRandom);
out.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
RangeRandom rangeRandom2 = null;
FileInputStream fis = null;
ObjectInputStream in = null;
try
{
fis = new FileInputStream("test");
in = new ObjectInputStream(fis);
rangeRandom2 = (RangeRandom)in.readObject();
in.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
catch(ClassNotFoundException ex)
{
ex.printStackTrace();
}
}
}
Если вы запустите main, вы увидите, что для каждого экземпляра RangeRandom выполняется два потока, и это связано с тем, что метод Thread.start () теперь находится как в конструкторе, так и в readObject () .
Версии и serialVersionUID
Когда вы реализуете интерфейс java.io.Serializable
для создания сериализуемого класса, компилятор ищет static final
поле с именем serialVersionUID
типа long
. Если класс не указал это поле явно, то компилятор создаст одно такое поле и присвоит ему значение, которое выходит из зависимого от реализации вычисления serialVersionUID
. Это вычисление зависит от различных аспектов класса, и оно следует за спецификациями сериализации объектов, данными Sun. Но, во всех реализациях компилятора не гарантируется одинаковое значение.
Это значение используется для проверки совместимости классов в отношении сериализации, и это выполняется при де-сериализации сохраненного объекта. Последовательность выполнения Serialization проверяет, что serialVersionUID
считанный из де-сериализованных данных, и serialVersionUID
объявленные в классе, точно такие же. Если это не так, это вызывает InvalidClassException
.
Настоятельно рекомендуется явно объявить и инициализировать статическое окончательное поле типа long и с именем «serialVersionUID» во всех ваших классах, которые вы хотите сделать Serializable, вместо того, чтобы полагаться на вычисление значения по умолчанию для этого поля, даже если вы не собираетесь использование управление версиями. Вычисление «serialVersionUID» чрезвычайно чувствительно и может варьироваться от одной реализации компилятора к другой, и, следовательно, вы можете получить InvalidClassException
даже для того же класса только потому, что вы использовали разные реализации компилятора на отправителе и на концах приемника процесса сериализации.
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
Пока serialVersionUID
тот же, Java Serialization может обрабатывать разные версии класса. Совместимые и несовместимые изменения;
Совместимые изменения
- Добавление полей: когда восстанавливаемый класс имеет поле, которое не встречается в потоке, это поле в объекте будет инициализировано значением по умолчанию для его типа. Если требуется инициализация, специфичная для класса, класс может предоставить метод readObject, который может инициализировать поле для значений небезопасности.
- Добавление классов: поток будет содержать иерархию типов каждого объекта в потоке. Сравнение этой иерархии в потоке с текущим классом может обнаруживать дополнительные классы. Поскольку в потоке, из которого инициализируется объект, нет информации, поля класса будут инициализированы значениями по умолчанию.
- Удаление классов: сравнение иерархии классов в потоке с потоком текущего класса может обнаружить, что класс был удален. В этом случае поля и объекты, соответствующие этому классу, считываются из потока. Примитивные поля отбрасываются, но объекты, на которые ссылается удаленный класс, создаются, так как они могут быть переданы позже в потоке. Они будут собирать мусор, когда поток собирается или сбрасывается мусором.
- Добавление методов writeObject / readObject: если версия, читающая поток, имеет эти методы, тогда readObject, как обычно, ожидается, чтобы прочитать необходимые данные, записанные в поток по сериализации по умолчанию. Он должен сначала вызвать defaultReadObject перед чтением любых дополнительных данных. Метод writeObject, как обычно, должен вызвать функцию defaultWriteObject для записи необходимых данных, а затем может записывать дополнительные данные.
- Добавление java.io.Serializable: это эквивалентно добавлению типов. В потоке для этого класса не будет значений, поэтому его поля будут инициализированы значениями по умолчанию. Поддержка подклассификации несериализуемых классов требует, чтобы супертип класса имел конструктор no-arg, и сам класс будет инициализирован значениями по умолчанию. Если конструктор no-arg недоступен, генерируется исключение InvalidClassException.
- Изменение доступа к полю: модификаторы доступа public, package, protected и private не влияют на возможность сериализации присвоить значения полям.
- Изменение поля от статического до нестатического или переходного к нетрансходящему: если полагаться на сериализацию по умолчанию для вычисления сериализуемых полей, это изменение эквивалентно добавлению поля в класс. Новое поле будет записано в поток, но предыдущие классы будут игнорировать значение, поскольку сериализация не будет присваивать значения статическим или переходным полям.
Несовместимые изменения
- Удаление полей: если поле удалено в классе, то записанный поток не будет содержать его значения. Когда поток считывается более ранним классом, значение поля будет установлено на значение по умолчанию, потому что в потоке не доступно значение. Однако это значение по умолчанию может отрицательно повлиять на способность более ранней версии выполнять свой контракт.
- Перемещение классов вверх или вниз по иерархии: это невозможно, так как данные в потоке отображаются в неправильной последовательности.
- Изменение нестатического поля в статическом или нетрансловом поле на переходный период: если полагаться на сериализацию по умолчанию, это изменение эквивалентно удалению поля из класса. Эта версия класса не будет записывать эти данные в поток, поэтому она не будет доступна для чтения более ранними версиями класса. Как и при удалении поля, поле более ранней версии будет инициализировано значением по умолчанию, которое может привести к сбою класса неожиданным образом.
- Изменение объявленного типа примитивного поля: каждая версия класса записывает данные с объявленным типом. Более ранние версии класса, пытающиеся прочитать поле, будут терпеть неудачу, потому что тип данных в потоке не соответствует типу поля.
- Изменение метода writeObject или readObject, чтобы он больше не записывал или не читал данные поля по умолчанию или не менял его так, чтобы он пытался записать его или прочитать, когда предыдущая версия не выполнялась. Данные поля по умолчанию должны последовательно отображаться или не отображаться в потоке.
- Изменение класса от Serializable до Externalizable или наоборот - это несовместимое изменение, поскольку поток будет содержать данные, которые несовместимы с реализацией доступного класса.
- Изменение класса из типа, отличного от enum, до типа перечисления или наоборот, поскольку поток будет содержать данные, которые несовместимы с реализацией доступного класса.
- Удаление Serializable или Externalizable является несовместимым изменением, поскольку после написания он больше не будет предоставлять поля, необходимые для более старых версий класса.
- Добавление метода writeReplace или readResolve к классу несовместимо, если поведение приведет к созданию объекта, который несовместим с любой более старой версией класса.
Пользовательская десериализация JSON с Джексоном
Мы используем API для отдыха как формат JSON, а затем отключаем его до POJO. Джексон's org.codehaus.jackson.map.ObjectMapper «просто работает» из коробки, и мы действительно ничего не делаем в большинстве случаев. Но иногда нам нужен настраиваемый десериализатор для выполнения наших собственных потребностей, и этот учебник поможет вам в создании собственного пользовательского десериализатора.
Допустим, у нас есть следующие сущности.
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
А также
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
Давайте сначала сериализуем / маршалируем объект.
User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");
Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");
ObjectMapper objectMapper = new ObjectMapper();
final String json = objectMapper.writeValueAsString (программа); System.out.println (JSON);
Вышеприведенный код будет содержать следующие JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
Теперь можно сделать обратное очень легко. Если у нас есть этот JSON, мы можем развязать объект программы с помощью ObjectMapper следующим образом:
Скажем так, это не настоящий случай, у нас будет другой JSON из API, который не соответствует нашему классу Program
.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
Посмотрите на строку JSON, вы можете видеть, у нее есть другое поле, которое является owenerId.
Теперь, если вы хотите сериализовать этот JSON, как мы это делали ранее, у вас будут исключения.
Есть два способа избежать исключений и сделать это сериализованным -
Игнорировать неизвестные поля
Игнорируйте onwerId
. Добавьте в класс программы следующую аннотацию
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
Напишите пользовательский десериализатор
Но есть случаи, когда вам действительно нужно это поле owerId
. Предположим, вы хотите связать его как идентификатор класса User
.
В таком случае вам нужно написать собственный десериализатор,
Как вы можете видеть, сначала вам нужно получить доступ к JsonNode
из JonsParser
. А затем вы можете легко извлекать информацию из JsonNode
с помощью метода get()
. и вы должны убедиться в имени поля. Это должно быть точное имя, ошибка орфографии приведет к исключениям.
И , наконец, вы должны зарегистрировать свой ProgramDeserializer в ObjectMapper
.
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
mapper.registerModule(module);
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);
Кроме того, вы можете использовать аннотацию для регистрации десериализатора напрямую -
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}