1. 程式人生 > >技術分享:Java 序列化

技術分享:Java 序列化

bsp 寫入 ddr 應用 特性 知識點 tom == 生成

  1. 簡介

  1.1. 定義

  序列化:序列化是將對象轉換為字節流。

  反序列化:反序列化是將字節流轉換為對象。

  

技術分享圖片

  1.2. 用途

  序列化的用途有:

  序列化可以將對象的字節序列持久化——保存在內存、文件、數據庫中。

  在網絡上傳送對象的字節序列。

  RMI(遠程方法調用)

  2. 序列化和反序列化

  Java 通過對象輸入輸出流來實現序列化和反序列化:

  序列化:java.io.ObjectOutputStream 類的 writeObject() 方法可以實現序列化;

  反序列化:java.io.ObjectInputStream 類的 readObject() 方法用於實現反序列化。

  序列化和反序列化示例:

  public class SerializeDemo01 {

  enum Sex {

  MALE, FEMALE

  }

  static class Person implements Serializable {

  private static final long serialVersionUID = 1L;

  private String name = null;

  private Integer age = null;

  private Sex sex;

  public Person() {

  System.out.println(call Person());

  }

  public Person(String name, Integer age, Sex sex) {

  this.name = name;

  this.age = age;

  this.sex = sex;

  }

  public String toString() {

  return name: + this.name + , age: + this.age + , sex: + this.sex;

  }

  }

  /**

  * 序列化

  */

  private static void serialize(String filename) throws IOException {

  File f = new File(filename); // 定義保存路徑

  OutputStream out = new FileOutputStream(f); // 文件輸出流

  ObjectOutputStream oos = new ObjectOutputStream(out); // 對象輸出流

  oos.writeObject(new Person(Jack, 30, Sex.MALE)); // 保存對象

  oos.close();

  out.close();

  }

  /**

  * 反序列化

  */

  private static void deserialize(String filename) throws IOException, ClassNotFoundException {

  File f = new File(filename); // 定義保存路徑

  InputStream in = new FileInputStream(f); // 文件輸入流

  ObjectInputStream ois = new ObjectInputStream(in); // 對象輸入流

  Object obj = ois.readObject(); // 讀取對象

  ois.close();

  in.close();

  System.out.println(obj);

  }

  public static void main(String[] args) throws IOException, ClassNotFoundException {

  final String filename = d:/text.dat;

  serialize(filename);

  deserialize(filename);

  }

  }

  輸出:

  name: Jack, age: 30, sex: MALE

  3. Serializable 接口

  被序列化的類必須屬於 Enum、Array 和 Serializable 類型其中的任何一種。

  如果不是 Enum、Array 的類,如果需要序列化,必須實現 java.io.Serializable 接口,否則將拋出 NotSerializableException 異常。這是因為:在序列化操作過程中會對類型進行檢查,如果不滿足序列化類型要求,就會拋出異常。

  我們不妨做一個小嘗試:將 SerializeDemo01 示例中 Person 類改為如下實現,然後看看運行結果。

  public class UnSerializeDemo {

  static class Person { // 其他內容略 }

  // 其他內容略

  }

  輸出:結果就是出現如下異常信息。

  Exception in thread main java.io.NotSerializableException:

  ...

  3.1. serialVersionUID

  請註意 serialVersionUID 字段,你可以在 Java 世界的無數類中看到這個字段。

  serialVersionUID 有什麽作用,如何使用 serialVersionUID?

  serialVersionUID 是 Java 為每個序列化類產生的版本標識。它可以用來保證在反序列時,發送方發送的和接受方接收的是可兼容的對象。如果接收方接收的類的 serialVersionUID 與發送方發送的 serialVersionUID 不一致,會拋出 InvalidClassException。

  如果可序列化類沒有顯式聲明 serialVersionUID,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID 值。盡管這樣,還是建議在每一個序列化的類中顯式指定 serialVersionUID 的值。因為不同的 jdk 編譯很可能會生成不同的 serialVersionUID 默認值,從而導致在反序列化時拋出 InvalidClassExceptions 異常。

  serialVersionUID 字段必須是 static final long 類型。

  我們來舉個例子:

  (1)有一個可序列化類 Person

  public class Person implements Serializable {

  private static final long serialVersionUID = 1L;

  private String name;

  private Integer age;

  private String address;

  // 構造方法、get、set 方法略

  }

  (2)開發過程中,對 Person 做了修改,增加了一個字段 email,如下:

  public class Person implements Serializable {

  private static final long serialVersionUID = 1L;

  private String name;

  private Integer age;

  private String address;

  private String email;

  // 構造方法、get、set 方法略

  }

  由於這個類和老版本不兼容,我們需要修改版本號:

  private static final long serialVersionUID = 2L;

  再次進行反序列化,則會拋出 InvalidClassException 異常。

  綜上所述,我們大概可以清楚:serialVersionUID 用於控制序列化版本是否兼容。若我們認為修改的可序列化類是向後兼容的,則不修改 serialVersionUID。

  4. 默認序列化機制

  如果僅僅只是讓某個類實現 Serializable 接口,而沒有其它任何處理的話,那麽就是使用默認序列化機制。

  使用默認機制,在序列化對象時,不僅會序列化當前對象本身,還會對其父類的字段以及該對象引用的其它對象也進行序列化。同樣地,這些其它對象引用的另外對象也將被序列化,以此類推。所以,如果一個對象包含的成員變量是容器類對象,而這些容器所含有的元素也是容器類對象,那麽這個序列化的過程就會較復雜,開銷也較大。

  註意:這裏的父類和引用對象既然要進行序列化,那麽它們當然也要滿足序列化要求:被序列化的類必須屬於 Enum、Array 和 Serializable 類型其中的任何一種。

  5. 非默認序列化機制

  在現實應用中,有些時候不能使用默認序列化機制。比如,希望在序列化過程中忽略掉敏感數據,或者簡化序列化過程。下面將介紹若幹影響序列化的方法。

  5.1. transient 關鍵字

  當某個字段被聲明為 transient 後,默認序列化機制就會忽略該字段。

  我們將 SerializeDemo01 示例中的內部類 Person 的 age 字段聲明為 transient,如下所示:

  public class SerializeDemo02 {

  static class Person implements Serializable {

  transient private Integer age = null;

  // 其他內容略

  }

  // 其他內容略

  }

  輸出:

  name: Jack, age: null, sex: MALE

  從輸出結果可以看出,age 字段沒有被序列化。

  5.2. Externalizable 接口

  無論是使用 transient 關鍵字,還是使用 writeObject()和 readObject()方法,其實都是基於 Serializable 接口的序列化。

  JDK 中提供了另一個序列化接口--Externalizable。

  可序列化類實現 Externalizable 接口之後,基於 Serializable 接口的默認序列化機制就會失效。

  我們來基於 SerializeDemo02 再次做一些改動,代碼如下:

  public class ExternalizeDemo01 {

  static class Person implements Externalizable {

  transient private Integer age = null;

  // 其他內容略

  private void writeObject(ObjectOutputStream out) throws IOException {

  out.defaultWriteObject();

  out.writeInt(age);

  }

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

  in.defaultReadObject();

  age = in.readInt();

  }

  @Override

  public void writeExternal(ObjectOutput out) throws IOException { }

  @Override

  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }

  }

  // 其他內容略

  }

  輸出:

  call Person()

  name: null, age: null, sex: null

  從該結果,一方面可以看出 Person 對象中任何一個字段都沒有被序列化。另一方面,如果細心的話,還可以發現這此次序列化過程調用了 Person 類的無參構造方法。

  Externalizable 繼承於 Serializable,它增添了兩個方法:writeExternal() 與 readExternal()。這兩個方法在序列化和反序列化過程中會被自動調用,以便執行一些特殊操作。當使用該接口時,序列化的細節需要由程序員去完成。如上所示的代碼,由於 writeExternal() 與 readExternal() 方法未作任何處理,那麽該序列化行為將不會保存/讀取任何一個字段。這也就是為什麽輸出結果中所有字段的值均為空。

  另外,若使用 Externalizable 進行序列化,當讀取對象時,會調用被序列化類的無參構造方法去創建一個新的對象;然後再將被保存對象的字段的值分別填充到新對象中。這就是為什麽在此次序列化過程中 Person 類的無參構造方法會被調用。由於這個原因,實現 Externalizable 接口的類必須要提供一個無參的構造方法,且它的訪問權限為 public。

  對上述 Person 類作進一步的修改,使其能夠對 name 與 age 字段進行序列化,但要忽略掉 gender 字段,如下代碼所示:

  public class ExternalizeDemo02 {

  static class Person implements Externalizable {

  transient private Integer age = null;

  // 其他內容略

  private void writeObject(ObjectOutputStream out) throws IOException {

  out.defaultWriteObject();

  out.writeInt(age);

  }

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

  in.defaultReadObject();

  age = in.readInt();

  }

  @Override

  public void writeExternal(ObjectOutput out) throws IOException {

  out.writeObject(name);

  out.writeInt(age);

  }

  @Override

  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {

  name = (String) in.readObject();

  age = in.readInt();

  }

  }

  // 其他內容略

  }

  輸出:

  call Person()

  name: Jack, age: 30, sex: null

  5.3. Externalizable 接口的替代方法

  實現 Externalizable 接口可以控制序列化和反序列化的細節。它有一個替代方法:實現 Serializable 接口,並添加 writeObject(ObjectOutputStream out) 與 readObject(ObjectInputStream in) 方法。序列化和反序列化過程中會自動回調這兩個方法。

  示例如下所示:

  public class SerializeDemo03 {

  static class Person implements Serializable {

  transient private Integer age = null;

  // 其他內容略

  private void writeObject(ObjectOutputStream out) throws IOException {

  out.defaultWriteObject();

  out.writeInt(age);

  }

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

  in.defaultReadObject();

  age = in.readInt();

  }

  // 其他內容略

  }

  // 其他內容略

  }

  輸出:

  name: Jack, age: 30, sex: MALE

  在 writeObject()方法中會先調用 ObjectOutputStream 中的 defaultWriteObject()方法,該方法會執行默認的序列化機制,如 5.1 節所述,此時會忽略掉 age 字段。然後再調用 writeInt() 方法顯示地將 age 字段寫入到 ObjectOutputStream 中。readObject() 的作用則是針對對象的讀取,其原理與 writeObject()方法相同。

  註意:writeObject()與 readObject()都是 private 方法,那麽它們是如何被調用的呢?毫無疑問,是使用反射。詳情可見 ObjectOutputStream 中的 writeSerialData 方法,以及 ObjectInputStream 中的 readSerialData 方法。

  5.4. readResolve() 方法

  當我們使用 Singleton 模式時,應該是期望某個類的實例應該是唯一的,但如果該類是可序列化的,那麽情況可能會略有不同。此時對第 2 節使用的 Person 類進行修改,使其實現 Singleton 模式,如下所示:

  public class SerializeDemo04 {

  enum Sex {

  MALE, FEMALE

  }

  static class Person implements Serializable {

  private static final long serialVersionUID = 1L;

  private String name = null;

  transient private Integer age = null;

  private Sex sex;

  static final Person instatnce = new Person(Tom, 31, Sex.MALE);

  private Person() {

  System.out.println(call Person());

  }

  private Person(String name, Integer age, Sex sex) {

  this.name = name;

  this.age = age;

  this.sex = sex;

  }

  public static Person getInstance() {

  return instatnce;

  }

  private void writeObject(ObjectOutputStream out) throws IOException {

  out.defaultWriteObject();

  out.writeInt(age);

  }

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

  in.defaultReadObject();

  age = in.readInt();

  }

  public String toString() {

  return name: + this.name + , age: + this.age + , sex: + this.sex;

  }

  }

  /**

  * 序列化

  */

  private static void serialize(String filename) throws IOException {

  File f = new File(filename); // 定義保存路徑

  OutputStream out = new FileOutputStream(f); // 文件輸出流

  ObjectOutputStream oos = new ObjectOutputStream(out); // 對象輸出流

  oos.writeObject(new Person(Jack, 30, Sex.MALE)); // 保存對象

  oos.close();

  out.close();

  }

  /**

  * 反序列化

  */

  private static void deserialize(String filename) throws IOException, ClassNotFoundException {

  File f = new File(filename); // 定義保存路徑

  InputStream in = new FileInputStream(f); // 文件輸入流

  ObjectInputStream ois = new ObjectInputStream(in); // 對象輸入流

  Object obj = ois.readObject(); // 讀取對象

  ois.close();

  in.close();

  System.out.println(obj);

  System.out.println(obj == Person.getInstance());

  }

  public static void main(String[] args) throws IOException, ClassNotFoundException {

  final String filename = d:/text.dat;

  serialize(filename);

  deserialize(filename);

  }

  }

  輸出:

  name: Jack, age: 30, sex: MALE

  false

  值得註意的是,從文件中獲取的 Person 對象與 Person 類中的單例對象並不相等。為了能在單例類中仍然保持序列的特性,可以使用 readResolve() 方法。在該方法中直接返回 Person 的單例對象。我們在 SerializeDemo04 示例的基礎上添加一個 readObject 方法, 如下所示:

  public class SerializeDemo05 {

  // 其他內容略

  static class Person implements Serializable {

  // 添加此方法

  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

  in.defaultReadObject();

  age = in.readInt();

  }

  // 其他內容略

  }

  // 其他內容略

  }

  輸出:

  name: Jack, age: 30, sex: MALE

  true

  6. 總結

  通過上面的內容,相各位已經了解了 Java 序列化的使用。這裏用一張腦圖來總結知識點。

  

技術分享圖片

?

技術分享:Java 序列化