Глубокое понимание механизма сериализации Java.

Java задняя часть

1. Введение в сериализацию Java

Сериализация относится к процессу, посредством которого объект записывает себя, записывая значения, описывающие его состояние, то есть представляя объект как серию упорядоченных байтов.Java предоставляет методы для записи объектов в потоки и восстановления объектов из потоков. Объекты могут содержать другие объекты, а другие объекты могут содержать другие объекты. Сериализация Java может автоматически обрабатывать вложенные объекты. Для простого поля объекта writeObject() записывает его значение непосредственно в поток. Когда встречается поле объекта, writeObject() вызывается снова, и если этот объект встраивает другой объект, то writeObject() вызывается снова, пока объект не будет записан непосредственно в поток. Все, что нужно сделать программисту, — это передать объект в метод writeObject() потока ObjectOutputStream, а все остальное система сделает автоматически.

Класс для реализации сериализации должен реализовывать интерфейс java.io.Serializable или java.io.Externalizable, иначе будет сгенерировано исключение NotSerializableException. Этот интерфейс не имеет внутри никаких методов, это просто «теговый интерфейс», который просто «помечает» свой собственный объект специального типа. Класс включает свои возможности сериализации, реализуя интерфейс java.io.Serializable. Класс, который не реализует этот интерфейс, не сможет сериализовать или десериализовать какое-либо свое состояние. Все подтипы сериализуемого класса сами сериализуемы. Интерфейсы сериализации не имеют методов или полей и используются только для идентификации сериализуемой семантики. «Сериализация объектов» в Java позволяет преобразовать объект, реализующий интерфейс Serializable, в набор байтов, чтобы при использовании объекта в будущем можно было восстановить байтовые данные и соответствующим образом перестроить объект.

2. Необходимость и цель сериализации

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

Есть две основные функции, поддерживаемые сериализацией Java:

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

Цель сериализации Java (что я могу понять до сих пор):

  • Поддержка двусторонней связи между разными версиями классов, работающих на разных виртуальных машинах;
  • Обеспечивает сериализацию для сохраняемости и RMI;

3. Несколько примеров сериализации

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

  • Когда объект сериализуется, сериализуются только нестатические переменные-члены объекта, и любые методы-члены и статические переменные-члены не могут быть сериализованы.
  • Если переменная-член объекта является объектом, то данные-члены объекта также сохраняются.
  • Если сериализуемый объект содержит ссылку на несериализуемый объект, вся операция сериализации завершится ошибкой и будет выдано исключение NotSerializableException. Пометив эту ссылку как временную, объект все равно можно будет сериализовать. Для некоторых конфиденциальных данных, которые не нужно сериализовать, этот флаг также можно использовать для модификации.
    Давайте рассмотрим встроенный процесс сериализации в Java на простом примере.
class SuperClass implements Serializable{
    private String name;
    private int age;
    private String email;
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public String getEmail() {
    	return email;
    }
    
    public SuperClass(String name,int age,String email) {
    	this.name=name;
    	this.age=age;
    	this.email=email;
    }
}

Давайте посмотрим на процесс сериализации в методе main.Код выглядит следующим образом:

public static void main(String[] args) throws IOException,ClassNotFoundException {
    	System.out.println("序列化对象开始!");
    	SuperClass superClass=new SuperClass("gong",27, "1301334028@qq.com");
    	File rootfile=new File("C:/data");
    	if(!rootfile.exists()) {
    		rootfile.mkdirs();
    	}
    	File file=new File("C:/data/data.txt");
    	if(!file.exists()) {
    		file.createNewFile();
    	}
    	FileOutputStream fileOutputStream=new FileOutputStream(file);
    	ObjectOutputStream objectOutputStream=new ObjectOutputStream(fileOutputStream);
    	objectOutputStream.writeObject(superClass);
    	objectOutputStream.flush();
    	objectOutputStream.close();
    	System.out.println("序列化对象完成!");
    	
    	System.out.println("反序列化对象开始!");
    	FileInputStream fileInputStream=new FileInputStream(new File("C:\\data\\data.txt"));
    	ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
    	SuperClass getObject=(SuperClass) objectInputStream.readObject();
    	System.out.println("反序列化对象数据:");
    	
    	System.out.println("name:"+getObject.getName()+"\nage:"+getObject.getAge()+"\nemail:"+getObject.getEmail());
}

Результат выполнения кода следующий:

序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
name:gong
age:27
email:1301334028@qq.com

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

временное ключевое слово и сериализация

Если теперь мы хотим, чтобы вышеприведенный класс SuperClass прошел возраст и адрес электронной почты, не участвуя в процессе сериализации, нам нужно только добавить ключевое слово transient перед его определением:

private transient int age;
private transient String email;

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

序列化对象开始!
序列化对象完成!
反序列化对象开始!
反序列化对象数据:
name:gong
age:0
email:null

Пользовательский процесс сериализации

Если процесс сериализации по умолчанию не соответствует потребностям, мы также можем настроить весь процесс сериализации. На данный момент нам нужно только определить метод writeObject и метод readObject в классе, который необходимо сериализовать. Возьмем в качестве примера SuperClass. Теперь добавим пользовательский процесс сериализации. Ключевое слово transient заставляет встроенный процесс сериализации Java игнорировать измененные переменные. Мы используем пользовательский процесс сериализации или сериализуем возраст и адрес электронной почты. Давайте посмотрим. изменять:

private String name;
private transient int age;
private transient String email;

public String getName() {
	return name;
}

public int getAge() {
	return age;
}

public String getEmail() {
	return email;
}

public SuperClass(String name,int age,String email) {
	this.name=name;
	this.age=age;
	this.email=email;
}

private void writeObject(ObjectOutputStream objectOutputStream) 
		throws IOException {
	objectOutputStream.defaultWriteObject();
	objectOutputStream.writeInt(age);
	objectOutputStream.writeObject(email);
}


private void readObject(ObjectInputStream objectInputStream) 
		throws ClassNotFoundException,IOException {
	objectInputStream.defaultReadObject();
	age=objectInputStream.readInt();
	email=(String)objectInputStream.readObject();
}

Результаты приведены ниже:

反序列化对象数据:
name:gong
age:27
email:1301334028@qq.com

Мы видим, что результат выполнения соответствует результату по умолчанию.Мы изменили процесс сериализации по умолчанию, настроив механизм сериализации (сделав ключевое слово transient бесполезным).
Уведомление:
Внимательные студенты могут обнаружить, что мы вызываем методы defaultWriteObject() и defaultReadObject() в процессе пользовательской сериализации. Эти два метода вызываются процессом сериализации по умолчанию. Если наш пользовательский процесс сериализации вызывает только эти два метода без каких-либо дополнительных операций, это фактически ничем не отличается от процесса сериализации по умолчанию, вы можете попробовать его.

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

Подкласс поддерживает сериализацию, суперкласс не поддерживает сериализацию

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

    class SuperClass{
    protected String name;
    protected int age;
    
    public String getName() {
    	return name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public SuperClass(String name,int age) {
    	this.name=name;
    	this.age=age;
    }
    }
    
    class DeriveClass extends SuperClass implements Serializable{
    private String email;
    private String address;
    
    public DeriveClass(String name,int age,String email,String address) {
    	super(name,age);
    	this.email=email;
    	this.address=address;
    }
    
    public String getEmail() {
    	return email;
    }
    
    public String getAddress() {
    	return address;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException {  
        out.defaultWriteObject();  
        out.writeObject(name);
        out.writeInt(age);
    }  
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        name=(String)in.readObject();
        age=in.readInt();
    }   
    
    @Override
    public String toString() {
    	return "name:"+getName()+"\nage:"+getAge()+"\nemail:"+getEmail()+"\naddress"+getAddress();
    }
}

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

DeriveClass superClass=new DeriveClass("gong",27,"1301334028@qq.com","NJ");
DeriveClass getObject=(DeriveClass) objectInputStream.readObject();
System.out.println("反序列化对象数据:");
System.out.println(getObject);

Выполнение кода обнаружило ошибку, ошибка выглядит следующим образом:

Exception in thread "main" java.io.InvalidClassException: com.learn.example.DeriveClass; no valid constructor
	at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(Unknown Source)
	at java.io.ObjectStreamClass.checkDeserialize(Unknown Source)
	at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.readObject(Unknown Source)
	at com.learn.example.RunMain.main(RunMain.java:88)

Давайте подробнее рассмотрим, почему это происходит. DeriveClass поддерживает сериализацию, но его родительский класс не поддерживает сериализацию, поэтому в этом случае подклассу необходимо дополнительно сериализовать поля родительского класса при сериализации (если есть такая необходимость). Затем при десериализации, потому что при создании экземпляра DeriveClass вам нужно сначала вызвать конструктор родительского класса, а затем свой собственный конструктор. При десериализации для создания родительского объекта в качестве родительского объекта по умолчанию может быть вызван только конструктор родительского класса без аргументов, поэтому, когда мы берем значение переменной родительского объекта, его значение равно после вызова конструктора без аргументов. конструктор аргумента значения родительского класса. Если вы принимаете во внимание эту сериализацию, инициализируйте переменную в родительском конструкторе без аргументов. Или присваивать значения в методе readObject. Нам просто нужно добавить пустой конструктор в SuperClass:

public SuperClass() {}

Родительский класс поддерживает сериализацию

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

5. Сериализация и serialVersionUID

В приведенном выше примере мы не видели поля serialVersionUID, почему мы можем нормально сериализовать и десериализовать? Это связано с тем, что Eclipse по умолчанию генерирует для нас сериализованный идентификатор.
Два стратегии генерации предоставляются в Eclipse, один является фиксированным 1л, а другой - случайным образом генерировать не повторяющиеся данные длительного типа (фактически генерируемые инструментами JDK). Вот предложение. Если нет особых требований, используйте По умолчанию 1L достаточно, что гарантирует, что десериализация успешна, когда код согласуется.
Примечание. Разрешает ли виртуальная машина десериализацию, зависит не только от согласованности пути к классам и кода функции, очень важным моментом является согласованность идентификаторов сериализации двух классов (то есть privatestatic final long serialVersionUID = 1L). коды абсолютно одинаковые, но идентификаторы сериализации разные, и их нельзя сериализовать и десериализовать друг с другом (особенно после передачи по сети нужно быть внимательным при удаленном создании объектов)

6. Серийное хранилище

В предыдущем примере мы сериализовали данные в файл data.txt. Давайте воспользуемся инструментом просмотра двоичных файлов, чтобы увидеть, как сериализованный поток байтов в Java хранится в файле. Каков его формат? Мы преобразуем приведенный выше класс SuperClass:

class SuperClass implements Serializable{
	
	private static final int serialVersionUID=1;
	
	protected String name;
	protected int age;
	
	public SuperClass() {}
	
	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}
	
	public SuperClass(String name,int age) {
		this.name=name;
		this.age=age;
	}
	
	 @Override
    public String toString() {
    	return "name:"+getName()+"\nage:"+getAge();
    }
}

Записаны следующие данные:

SuperClass superClass=new SuperClass("gong",27);

Давайте откроем data.txt, чтобы увидеть сохраненное содержимое: Конкретное содержимое хранилища показано на рисунке:

Ниже мы подробно объясним каждый шаг.

Часть 1 — это заголовок файла сериализации.

  • AC ED: Протокол сериализации STREAM_MAGIC
  • 00 05: версия протокола сериализации STREAM_VERSION
  • 73: TC_OBJECT объявляет, что это новый объект

Часть 2 — это описание класса, который нужно сериализовать, в данном случае это класс SerializableObject.

  • 72: TC_CLASSDESC объявляет, что здесь начинается новый класс
  • 00 1С:28 в десятичном виде, указывающее, что длина имени класса составляет 28 байт
  • 63 6F 6D ... 61 73 73: представляет собой строку символов "com.learn.example.SuperClass", которая действительно составляет 28 байт, если вы считаете
  • 00 00 00 00 00 00 00 01: SerialVersion, значение, которое мы устанавливаем в этом классе, равно 1, если мы его не установим, Eclipse автоматически установит его для нас.
  • 02: номер тега, указывающий, что объект поддерживает сериализацию.
  • 00 02: количество доменов, содержащихся в этом классе, равно 2.

Третья часть представляет собой описание каждого элемента свойства в объекте.

  • 4C: символ «L», указывающий, что свойство является типом объекта, а не примитивным типом.
  • 00 03 Десятичное число 3, указывающее длину имени атрибута
  • 61 67 65: Строка «возраст», имя свойства
  • 4C: символ «L», указывающий, что свойство является типом объекта, а не примитивным типом.
  • 00 04 Десятичный 4, указывающий длину имени атрибута
  • 6E 61 6D 65: Строка «имя», имя атрибута
  • 74: TC_STRING, представляющий новую строку, используйте строку для ссылки на объект

Четвертая часть - это информация родительского класса объекта, если нет родительского класса, то нет и такой части. Наличие родительского класса почти такое же, как в части 2.

  • 00 12: десятичное число 18, указывающее длину родительского класса.
  • 4C 6A 61 ... 6E 67 3B: "L/java/lang/String;" представляет атрибут родительского класса
  • 78: TC_ENDBLOCKDATA, признак конца блока объекта
  • 70: TC_NULL, указывающий на отсутствие других флагов суперкласса.

Часть 5 выводит фактическое значение элемента свойства объекта.Если элемент свойства является объектом, этот объект также будет сериализован здесь, правила такие же, как в части 2

  • 00 00 00 1B: значение атрибута age=27
  • 74: TC_STRING, представляющий новую строку, используйте строку для ссылки на объект
  • 00 04 Десятичный 4, указывающий длину имени атрибута
  • 67 6F 6E 67 Значение атрибута имени перейти
    Из приведенного выше анализа сериализованного бинарного файла мы можем сделать следующие ключевые выводы:
  • 1. После сериализации информация об объекте сохраняется
  • 2. Свойства, объявленные как переходные, не будут сериализованы, это роль ключевого слова transient.
  • 3. Свойства, объявленные как статические, не будут сериализованы.Эта проблема может быть понята таким образом.Сериализация сохраняет состояние объекта, но переменные, измененные статическими, принадлежат классу, а не объекту, поэтому сериализация не будет сериализоваться Это