# Java 序列化 – 执行正确的序列化

> 原文： [https://howtodoinjava.com/java/serialization/a-mini-guide-for-implementing-serializable-interface-in-java/](https://howtodoinjava.com/java/serialization/a-mini-guide-for-implementing-serializable-interface-in-java/)

**Java 序列化**允许将 Java 对象写入文件系统以进行永久存储，也可以将其写入网络以传输到其他应用程序。 Java 中的序列化是通过`Serializable`接口实现的。 Java [`Serializable`接口](https://docs.oracle.com/javase/6/docs/api/java/io/Serializable.html "Serializable interface")保证[可以序列化对象](https://howtodoinjava.com/java/serialization/custom-serialization-readobject-writeobject/)的能力。 此接口建议我们也使用`serialVersioUID`。

现在，即使您在应用程序类中同时使用了两者，您是否知道**哪怕现在会破坏您的设计**？ 让我们确定类中将来的更改，这些更改将是**兼容的更改**，而其他类别将证明**不兼容的更改**。

```java
Table of contents

1\. Java serialization incompatible changes
2\. Java serialization compatible changes
3\. serialVersionUID
4\. readObject() and writeObject() methods
5\. More serialization best practices
6\. Sample class following serialization best practices
7\. Serialization and deserialization example
```

## 1\. Java 序列化不兼容的更改

对类的不兼容更改是指不能保持互操作性的那些更改。 下面给出了在演化类时可能发生的不兼容更改（考虑默认序列化或[反序列化](https://howtodoinjava.com/java/serialization/how-deserialization-process-happen-in-java/)）：

1.  **删除字段** – 如果在类中删除了字段，则写入的流将不包含其值。 当较早的类读取流时，该字段的值将设置为默认值，因为流中没有可用的值。 但是，此默认值可能会不利地损害早期版本履行其契约的能力。
2.  **将类上移或下移** – 不允许这样做，因为流中的数据显示顺序错误。
3.  **将非静态字段更改为静态或将非瞬态字段更改为瞬态** – 当依赖默认序列化时，此更改等效于从类中删除字段。 该版本的类不会将该数据写入流，因此该类的早期版本将无法读取该数据。 与删除字段时一样，早期版本的字段将被初始化为默认值，这可能导致类以意外方式失败。
4.  **更改原始字段的声明类型** – 该类的每个版本都使用其声明类型写入数据。 尝试读取该字段的早期版本的类将失败，因为流中的数据类型与该字段的类型不匹配。
5.  **更改`writeObject`或`readObject`方法，使其不再写入或读取默认字段数据**，或对其进行更改，以使其尝试写入或读取以前的版本时不进行读取。 默认字段数据必须一致地出现在流中或不出现在流中。
6.  **将类从`Serializable`更改为`Externalizable`，反之亦然**是不兼容的更改，因为流将包含与可用类的实现不兼容的数据。
7.  **将类从非枚举类型更改为枚举类型，反之亦然**，因为流将包含与可用类的实现不兼容的数据。
8.  删除[`Serializable`](https://docs.oracle.com/javase/6/docs/api/java/io/Serializable.html "Serializable interface")或`Externalizable`是不兼容的更改，因为在编写时它将不再提供该类的较早版本所需的字段。
9.  **如果该行为会产生与该类的任何旧版本不兼容的对象，则将`writeReplace`或`readResolve`方法添加到类是不兼容的**。

## 2\. Java 序列化兼容更改

1.  **添加字段** – 当重构的类具有流中未出现的字段时，对象中的该字段将被初始化为其类型的默认值。 如果需要特定于类的初始化，则该类可以提供一个`readObject`方法，该方法可以将字段初始化为非默认值。
2.  **添加类** – 流将包含流中每个对象的类型层次结构。 将流中的此层次结构与当前类进行比较可以检测到其他类。 由于流中没有用于初始化对象的信息，因此该类的字段将被初始化为默认值。
3.  **删除类** – 将流中的类层次结构与当前类的层次结构进行比较可以检测到某个类已被删除。 在这种情况下，从该流中读取与该类相对应的字段和对象。 [原始字段](https://howtodoinjava.com/java/basics/primitive-data-types-in-java/)被丢弃，但是创建了由已删除类引用的对象，因为可以在流中稍后引用它们。 当流被垃圾回收或重置时，它们将被垃圾回收。
4.  **添加`writeObject`/`readObject`方法** – 如果读取流的版本具有这些方法，则通常希望`readObject`读取通过默认序列化写入流中的所需数据。 在读取任何可选数据之前，应先调用`defaultReadObject`。 通常，`writeObject`方法将调用`defaultWriteObject`写入所需的数据，然后再写入可选数据。
5.  **删除`writeObject`/`readObject`方法** – 如果读取流的类没有这些方法，则默认情况下将序列化读取所需数据，而可选数据将被丢弃。
6.  **添加`java.io.Serializable`** – 这等同于添加类型。 该类的流中将没有任何值，因此其字段将被初始化为默认值。 对子类化不可序列化类的支持要求该类的超类型具有无参构造器，并且该类本身将被初始化为默认值。 如果无参构造器不可用，则抛出`InvalidClassException`。
7.  **更改对字段的访问** – 公共，包，保护和私有的[访问修饰符](https://howtodoinjava.com/oops/java-access-modifiers/)对序列化为字段分配值的能力没有影响。
8.  **将字段从静态更改为非静态，或将瞬态更改为非瞬态** – 当依赖默认序列化来计算可序列化字段时，此更改等效于将字段添加到类中。 新字段将被写入流，但是较早的类将忽略该值，因为序列化不会为[静态](https://howtodoinjava.com/java/basics/java-static-keyword/)或[瞬态](https://howtodoinjava.com/java/basics/transient-keyword-in-java-with-real-time-example/)字段分配值。

## 3\. `serialVersionUID`

`serialVersionUID`是`Serializable`类的通用版本标识符。 反序列化使用此数字来确保已加载的类与序列化的对象完全对应。 如果找不到匹配项，则抛出`InvalidClassException`。

1.  **始终将其包含为字段**，例如：` private static final long serialVersionUID = 7526472295622776147L;`，即使在类的第一个版本中也要包含此字段，以提醒其重要性。
2.  **请勿在以后的版本中更改此字段的值，除非您有意对类进行**更改，以使其与旧的序列化对象不兼容。 如果需要，请遵循上述给定的准则。

## 4\. `readObject`和`writeObject`方法

1.  反序列化必须视为任何构造器：**在反序列化**结束时验证对象状态 – 这意味着`readObject`几乎应始终在`Serializable`类中实现，以便执行此验证。
2.  如果构造器**为可变对象字段制作防御性副本**，则必须读取对象。

## 5\. 更多序列化最佳实践

1.  使用 javadoc 的`@serial`标记表示可序列化字段。
2.  `.ser`扩展名通常用于表示序列化对象的文件。
3.  没有静态或瞬态字段接受默认序列化。
4.  除非必要，否则可扩展类不应是可序列化的。
5.  内部类很少（如果有的话）实现`Serializable`。
6.  容器类通常应遵循`Hashtable`的样式，该样式通过存储键和值来实现`Serializable`，而不是大型哈希表数据结构。

## 6\. 遵循序列化最佳实践的示例类

```java
package staticTest;

import java.io.Serializable;
import java.text.StringCharacterIterator;
import java.util.*;
import java.io.*;

public final class UserDetails implements Serializable {

/**
* This constructor requires all fields
*
* @param aFirstName
* contains only letters, spaces, and apostrophes.
* @param aLastName
* contains only letters, spaces, and apostrophes.
* @param aAccountNumber
* is non-negative.
* @param aDateOpened
* has a non-negative number of milliseconds.
*/
public UserDetails(String aFirstName, String aLastName, int aAccountNumber,
						Date aDateOpened) 
{
  super();
  setFirstName(aFirstName);
  setLastName(aLastName);
  setAccountNumber(aAccountNumber);
  setDateOpened(aDateOpened);
  // there is no need here to call verifyUserDetails.
}

// The default constructor
public UserDetails() {
  this("FirstName", "LastName", 0, new Date(System.currentTimeMillis()));
}

public final String getFirstName() {
  return fFirstName;
}

public final String getLastName() {
  return fLastName;
}

public final int getAccountNumber() {
  return fAccountNumber;
}

/**
* Returns a defensive copy of the field so that no one can change this
* field.
*/
public final Date getDateOpened() {
  return new Date(fDateOpened.getTime());
}

/**
* Names must contain only letters, spaces, and apostrophes. Validate before
* setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setFirstName(String aNewFirstName) {
  verifyNameProperty(aNewFirstName);
  fFirstName = aNewFirstName;
}

/**
* Names must contain only letters, spaces, and apostrophes. Validate before
* setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setLastName(String aNewLastName) {
  verifyNameProperty(aNewLastName);
  fLastName = aNewLastName;
}

/**
* Validate before setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setAccountNumber(int aNewAccountNumber) {
  validateAccountNumber(aNewAccountNumber);
  fAccountNumber = aNewAccountNumber;
}

public final void setDateOpened(Date aNewDate) {
  // make a defensive copy of the mutable date object
  Date newDate = new Date(aNewDate.getTime());
  validateAccountOpenDate(newDate);
  fDateOpened = newDate;
}

/**
* The client's first name.
*
* @serial
*/
private String fFirstName;

/**
* The client's last name.
*
* @serial
*/
private String fLastName;

/**
* The client's account number.
*
* @serial
*/
private int fAccountNumber;

/**
* The date the account was opened.
*
* @serial
*/
private Date fDateOpened;

/**
* Determines if a de-serialized file is compatible with this class.
* Included here as a reminder of its importance.
*/
private static final long serialVersionUID = 7526471155622776147L;

/**
* Verify that all fields of this object take permissible values
*
* @throws IllegalArgumentException
* if any field takes an unpermitted value.
*/
private void verifyUserDetails() {
  validateAccountNumber(fAccountNumber);
  verifyNameProperty(fFirstName);
  verifyNameProperty(fLastName);
  validateAccountOpenDate(fDateOpened);
}

/**
* Ensure names contain only letters, spaces, and apostrophes.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void verifyNameProperty(String aName) {
boolean nameHasContent = (aName != null) && (!aName.equals(""));
  if (!nameHasContent) {
    throw new IllegalArgumentException(
    "Names must be non-null and non-empty.");
  }

StringCharacterIterator iterator = new StringCharacterIterator(aName);
char character = iterator.current();
  while (character != StringCharacterIterator.DONE) {
    boolean isValidChar = (Character.isLetter(character)
    || Character.isSpaceChar(character) || character == ''');
    if (isValidChar) {
      // do nothing
    } else {
      String message = "Names can contain only letters, spaces, and apostrophes.";
      throw new IllegalArgumentException(message);
    }
    character = iterator.next();
  }
}

/**
* AccountNumber must be non-negative.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void validateAccountNumber(int aAccountNumber) {
  if (aAccountNumber < 0) {
    String message = "Account Number must be greater than or equal to 0.";
    throw new IllegalArgumentException(message);
  }
}

/**
* DateOpened must be after 1970.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void validateAccountOpenDate(Date aDateOpened) {
  if (aDateOpened.getTime() < 0) {
    throw new IllegalArgumentException(
      "Date Opened must be after 1970.");
  }
}

/**
* Always treat deserialization as a full-blown constructor, by validating
* the final state of the de-serialized object.
*/
private void readObject(ObjectInputStream aInputStream)
throws ClassNotFoundException, IOException {
  // always perform the default deserialization first
  aInputStream.defaultReadObject();

  // make defensive copy of the mutable Date field
  fDateOpened = new Date(fDateOpened.getTime());

  // ensure that object state has not been corrupted or tampered with
  // malicious code
  verifyUserDetails();
}

/**
* This is the default implementation of writeObject. Customise if
* necessary.
*/
private void writeObject(ObjectOutputStream aOutputStream)
throws IOException {
  // perform the default serialization for all non-transient, non-static
  // fields
  aOutputStream.defaultWriteObject();
}
}

```

现在让我们看看如何在 Java 中进行序列化和反序列化。

## 序列化和反序列化示例

```java
package serializationTest;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Calendar;
import java.util.Date;
public class TestUserDetails {
  public static void main(String[] args) {
    // Create new UserDetails object
    UserDetails myDetails = new UserDetails("Lokesh", "Gupta", 102825,
    new Date(Calendar.getInstance().getTimeInMillis()));

    // Serialization code
    try {
      FileOutputStream fileOut = new FileOutputStream("userDetails.ser");
      ObjectOutputStream out = new ObjectOutputStream(fileOut);
      out.writeObject(myDetails);
      out.close();
      fileOut.close();
    } catch (IOException i) {
      i.printStackTrace();
    }

    // deserialization code
    @SuppressWarnings("unused")
    UserDetails deserializedUserDetails = null;
    try {
      FileInputStream fileIn = new FileInputStream("userDetails.ser");
      ObjectInputStream in = new ObjectInputStream(fileIn);
      deserializedUserDetails = (UserDetails) in.readObject();
      in.close();
      fileIn.close();

      // verify the object state
      System.out.println(deserializedUserDetails.getFirstName());
      System.out.println(deserializedUserDetails.getLastName());
      System.out.println(deserializedUserDetails.getAccountNumber());
      System.out.println(deserializedUserDetails.getDateOpened());
    } catch (IOException ioe) {
      ioe.printStackTrace();
    } catch (ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
    }
  }
}
Output:

Lokesh
Gupta
102825
Wed Nov 21 15:06:34 GMT+05:30 2012

```

参考文献：

[http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html](https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html "serialization spec")