Java 的序列化 (Serialization) 教程

類別: IT
標籤: java

Java提供一種機制叫做序列化,通過有序的格式或者位元組序列持久化java物件,其中包含物件的資料,還有物件的型別,和儲存在物件中的資料型別。

所以,如果我們已經序列化了一個物件,那麼它可以被讀取並通過物件的型別和其他資訊進行反序列化,並最終獲取物件的原型。

ObjectInputStream 和 ObjectOutputStream物件是高階別的流物件,包含序列化和反序列化的方法。

ObjectOutputStream 擁有很多序列化物件的方法,最常用的是:

 private void writeObject(ObjectOutputStream os) throws IOException
    { 
        
    }
類似的 ObjectInputStream 提供如下方法:
    private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException
    {
        
    }


那麼哪裡會需要序列化呢?序列化通常在需要通過網路傳輸資料,或者儲存物件到檔案的場合使用。這裡說的資料是物件而不是文字。

現在的問題是,我們的網路架構和硬碟都只能識別二進位制和位元組,而不能識別Java物件。

序列化就是把Java物件中的value/states翻譯為位元組,以便通過網路傳輸或者儲存。另外,反序列化就是通過讀取位元組碼,並把它翻譯回java物件。

serialVersionUID概念

serialVersionUID 是用於保證同一個物件(在序列化中會被用到)可以在Deserialization過程中被載入。serialVersionUID 是用於物件的版本控制。你可以參考serialVersionUID in java serialization獲取更多資訊。

對於序列化:

步驟如下:

讓我們看一個列子:

在 src->org.arpit.javapostsforlearning 建立Employee.java

1.Employee.java 

package org.arpit.javapostsforlearning;
import java.io.Serializable;
public class Employee implements Serializable{

    int employeeId;
    String employeeName;
    String department;
    
    public int getEmployeeId() {
        return employeeId;
    }
    public void setEmployeeId(int employeeId) {
        this.employeeId = employeeId;
    }
    public String getEmployeeName() {
        return employeeName;
    }
    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }
    public String getDepartment() {
        return department;
    }
    public void setDepartment(String department) {
        this.department = department;
    }
}
就如你所見的,如果你需要序列化任何類,那麼你 必須實現 Serializable 介面 ,這個介面是標記介面(marker interface)。


Java中的標記介面(marker interface)就是一個沒有任何欄位或者方法的介面,簡單的來說,java中把空介面叫做標記介面(marker interface)

2.SerializeMain.java
package org.arpit.javapostsforlearning;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
 public class SerializeMain {

 /**
  * @author Arpit Mandliya
  */
 public static void main(String[] args) {

  Employee emp = new Employee();
  emp.setEmployeeId(101);
  emp.setEmployeeName("Arpit");
  emp.setDepartment("CS");
  try
  {
   FileOutputStream fileOut = new FileOutputStream("employee.ser");
   ObjectOutputStream outStream = new ObjectOutputStream(fileOut);
   outStream.writeObject(emp);
   outStream.close();
   fileOut.close();
  }catch(IOException i)
  {
   i.printStackTrace();
  }
 }
}

對於反序列化:

步驟是 

在包src->org.arpit.javapostsforlearning中,建立 DeserializeMain.java 

3.DeserializeMain.java
package org.arpit.javapostsforlearning;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializeMain {
 /**
  * @author Arpit Mandliya
  */
 public static void main(String[] args) {
  
  Employee emp = null;
       try
       {
          FileInputStream fileIn =new FileInputStream("employee.ser");
          ObjectInputStream in = new ObjectInputStream(fileIn);
          emp = (Employee) in.readObject();
          in.close();
          fileIn.close();
       }catch(IOException i)
       {
          i.printStackTrace();
          return;
       }catch(ClassNotFoundException c)
       {
          System.out.println("Employee class not found");
          c.printStackTrace();
          return;
       }
       System.out.println("Deserialized Employee...");
       System.out.println("Emp id: " + emp.getEmployeeId());
       System.out.println("Name: " + emp.getEmployeeName());
       System.out.println("Department: " + emp.getDepartment());
 }
}


4.執行:

首先執行SerializeMain.java,然後執行 DeserializeMain.java,你會得到如下的結果:
Deserialized Employee...
Emp id: 101
Name: Arpit
Department: CS
就這樣,我們序列化了一個employee物件,並對它進行反序列化。這看起來和簡單,但是如果其中包含物件引用,繼承,那麼情況就會變得複雜。接下來讓我們一個接一個的看一下例子,看看如何在各種場合中實現序列化。



案例1 - 如果物件引用了其他物件,那該如何

我們已經看過最簡單的序列化例子,現在看看,如何處理物件中引用了其他物件的場合。我們該如何序列化?引用物件也會被序列化嗎?對的,你不需要顯式的序列化引用物件。當你序列化任何物件,如果它包含引用物件,那麼Java序列化會自動序列化該物件的整個物件圖。例如,Employee現在引用了一個address物件,並且Address也引用了其他物件(例如,Home),那麼當你序列化Employee物件的時候,所有其他引用物件,例如address和home將會被自動地被序列化。讓我們來建立Address類,並它Address的物件作為引用,新增到employee類中。

Employee.java: 

package org.arpit.javapostsforlearning;
import java.io.Serializable;

public class Employee implements Serializable{

 int employeeId;
 String employeeName;
 String department;
 Address address;
 
 public int getEmployeeId() {
  return employeeId;
 }
 public void setEmployeeId(int employeeId) {
  this.employeeId = employeeId;
 }
 public String getEmployeeName() {
  return employeeName;
 }
 public void setEmployeeName(String employeeName) {
  this.employeeName = employeeName;
 }
 public String getDepartment() {
  return department;
 }
 public void setDepartment(String department) {
  this.department = department;
 }
 public Address getAddress() {
  return address;
 }
 public void setAddress(Address address) {
  this.address = address;
 }
}

在 org.arpit.javapostsforlearning 包中,建立Address.java 
Address.java: 
package org.arpit.javapostsforlearning;
public class Address {

 int homeNo;
 String street;
 String city;
 public Address(int homeNo, String street, String city) {
  super();
  this.homeNo = homeNo;
  this.street = street;
  this.city = city;
 }
 public int getHomeNo() {
  return homeNo;
 }
 public void setHomeNo(int homeNo) {
  this.homeNo = homeNo;
 }
 public String getStreet() {
  return street;
 }
 public void setStreet(String street) {
  this.street = street;
 }
 public String getCity() {
  return city;
 }
 public void setCity(String city) {
  this.city = city;
 }
}
在包 org.arpit.javapostsforlearning中,建立SerializeDeserializeMain.java
SerializeDeserializeMain.java:
package org.arpit.javapostsforlearning;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeDeserializeMain {
 /**
  * @author Arpit Mandliya
  */
 public static void main(String[] args) {

  Employee emp = new Employee();
  emp.setEmployeeId(101);
  emp.setEmployeeName("Arpit");
  emp.setDepartment("CS");
  Address address=new Address(88,"MG road","Pune");
  emp.setAddress(address);
  //Serialize
  try
  {
   FileOutputStream fileOut = new FileOutputStream("employee.ser");
   ObjectOutputStream outStream = new ObjectOutputStream(fileOut);
   outStream.writeObject(emp);
   outStream.close();
   fileOut.close();
  }catch(IOException i)
  {
   i.printStackTrace();
  }

  //Deserialize
  emp = null;
  try
  {
   FileInputStream fileIn =new FileInputStream("employee.ser");
   ObjectInputStream in = new ObjectInputStream(fileIn);
   emp = (Employee) in.readObject();
   in.close();
   fileIn.close();
  }catch(IOException i)
  {
   i.printStackTrace();
   return;
  }catch(ClassNotFoundException c)
  {
   System.out.println("Employee class not found");
   c.printStackTrace();
   return;
  }
  System.out.println("Deserialized Employee...");
  System.out.println("Emp id: " + emp.getEmployeeId());
  System.out.println("Name: " + emp.getEmployeeName());
  System.out.println("Department: " + emp.getDepartment());
  address=emp.getAddress();
  System.out.println("City :"+address.getCity());
 }
}
執行它:
當你執行SerializeDeserializeMain.java。你會得到這樣的結果:

java.io.NotSerializableException: org.arpit.javapostsforlearning.Address
    at java.io.ObjectOutputStream.writeObject0(Unknown Source)
    at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source)
    at java.io.ObjectOutputStream.writeSerialData(Unknown Source)
    at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)
    at java.io.ObjectOutputStream.writeObject0(Unknown Source)
    at java.io.ObjectOutputStream.writeObject(Unknown Source) 
我們將解釋哪裡出錯了。我忘記了說,Address 類也必須是serializable。那麼Address類必須繼承serialzable介面。

Address.java:
import java.io.Serializable;

public class Address implements Serializable{

 int homeNo;
 String street;
 String city;
 public Address(int homeNo, String street, String city) {
  super();
  this.homeNo = homeNo;
  this.street = street;
  this.city = city;
 } 
 public int getHomeNo() {
  return homeNo;
 }
 public void setHomeNo(int homeNo) {
  this.homeNo = homeNo;
 }
 public String getStreet() {
  return street;
 }
 public void setStreet(String street) {
  this.street = street;
 }
 public String getCity() {
  return city;
 }
 public void setCity(String city) {
  this.city = city;
 }
}
再次執行:
當你再次執行SerializeDeserializeMain.java。你可以得到如下的結果

Deserialized Employee...
Emp id: 101
Name: Arpit
Department: CS
City :Pune 

案例2:如果我們不能訪問引用物件的原始碼(例如,你不能訪問上面的Address類的原始碼)

如果我們不能訪問到address類,那麼我們該如何在Address類中實現serializable 介面?是否有另外的途徑來實現呢?對的,你可以建立另外一個類,並繼承Address,然後讓它繼承serializable 介面,但是對於下面的情況,這個方案會失敗:

  • 如果引用類被定義為final
  • 如果引用類引用了另外一個非可序列化的物件

那麼,我們該如何序列化Employee物件?解決的辦法是,標記transient。如果你不需要序列化任何欄位,只需把它標記為transient。

transient Address address 
在Employee類中,標記了address為transient之後,執行程式。你會得到nullPointerException,因為在反序列化過程中,Address引用將會是null。



案例3 - 如果我仍然需要儲存引用物件的狀態呢?(例如address物件)

如果你在反序列化過程中,標記了address為transient,它將會返回null結果。但是如果你仍然需要儲存它的狀態,你就需要序列化address物件。 Java序列化提供一個機制,如果你有特定簽名的private方法,那麼它們就會在序列化和反序列化過程中被呼叫,所以我們將重寫Employee類的writeObject和readObject方法,然後它們就會在Employee物件序列化/反序列化過程中被呼叫。

Employee.java: 

package org.arpit.javapostsforlearning;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Employee implements Serializable{

 int employeeId;
 String employeeName;
 String department;
 transient Address address;

 public int getEmployeeId() {
  return employeeId;
 }
 public void setEmployeeId(int employeeId) {
  this.employeeId = employeeId;
 }
 public String getEmployeeName() {
  return employeeName;
 }
 public void setEmployeeName(String employeeName) {
  this.employeeName = employeeName;
 }
 public String getDepartment() {
  return department;
 }
 public void setDepartment(String department) {
  this.department = department;
 }
 public Address getAddress() {
  return address;
 }
 public void setAddress(Address address) {
  this.address = address;
 }

 private void writeObject(ObjectOutputStream os) throws IOException, ClassNotFoundException
 { 
  try {
   os.defaultWriteObject();
   os.writeInt(address.getHomeNo());
   os.writeObject(address.getStreet());
   os.writeObject(address.getCity());
  } 
  catch (Exception e) 
  { e.printStackTrace(); }
 }
 
 private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException
 {
  try {
   is.defaultReadObject();
   int homeNo=is.readInt();
   String street=(String) is.readObject();
   String city=(String) is.readObject();
   address=new Address(homeNo,street,city);

  } catch (Exception e) { e.printStackTrace(); }
 }
}
另外有一點需要牢記的,ObjectInputStream讀取資料的順序和ObjectOutputStream寫入資料的順序是一致的.


在包org.arpit.javapostsforlearning 中建立Address.java
Address.java: 
package org.arpit.javapostsforlearning;
import java.io.Serializable;

public class Address {

 int homeNo;
 String street;
 String city;
 
 
 public Address(int homeNo, String street, String city) {
  super();
  this.homeNo = homeNo;
  this.street = street;
  this.city = city;
 }
 public int getHomeNo() {
  return homeNo;
 }
 public void setHomeNo(int homeNo) {
  this.homeNo = homeNo;
 }
 public String getStreet() {
  return street;
 }
 public void setStreet(String street) {
  this.street = street;
 }
 public String getCity() {
  return city;
 }
 public void setCity(String city) {
  this.city = city;
 }
}

在包org.arpit.javapostsforlearning中建立SerializeDeserializeMain.java
SerializeDeserializeMain.java:
package org.arpit.javapostsforlearning;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeDeserializeMain {
 /**
  * @author Arpit Mandliya
  */
 public static void main(String[] args) {

  Employee emp = new Employee();
  emp.setEmployeeId(101);
  emp.setEmployeeName("Arpit");
  emp.setDepartment("CS");
  Address address=new Address(88,"MG road","Pune");
  emp.setAddress(address);
  //Serialize
  try
  {
   FileOutputStream fileOut = new FileOutputStream("employee.ser");
   ObjectOutputStream outStream = new ObjectOutputStream(fileOut);
   outStream.writeObject(emp);
   outStream.close();
   fileOut.close();
  }catch(IOException i)
  {
   i.printStackTrace();
  }

  //Deserialize
  emp = null;
  try
  {
   FileInputStream fileIn =new FileInputStream("employee.ser");
   ObjectInputStream in = new ObjectInputStream(fileIn);
   emp = (Employee) in.readObject();
   in.close();
   fileIn.close();
  }catch(IOException i)
  {
   i.printStackTrace();
   return;
  }catch(ClassNotFoundException c)
  {
   System.out.println("Employee class not found");
   c.printStackTrace();
   return;
  }
  System.out.println("Deserialized Employee...");
  System.out.println("Emp id: " + emp.getEmployeeId());
  System.out.println("Name: " + emp.getEmployeeName());
  System.out.println("Department: " + emp.getDepartment());
  address=emp.getAddress();
  System.out.println("City :"+address.getCity());
 }
}

執行 :
當你執行SerializeDeserializeMain.java.你會得到如下的結果: 
Deserialized Employee...
Emp id: 101
Name: Arpit
Department: CS
City :Pune

現在我們就得到了address物件的狀態,就如它被序列化前的一樣。

序列化中的繼承:

現在我們看看繼承是如何影響序列化的。不管父類是不是可序列化,這將引出很多個例子。如果父類是非可序列化的,我們將如何處理,並且它是如何工作的。讓我們看看例子。

我們將建立一個Person.java,作為 Employee的父類。

案例4: 如果父類是可序列化的

如果父類可序列化,那麼所有的繼承類將是可序列化的。

案例5: 如果父類為非可序列化呢?

如果父類為非可序列化的 ,那麼我們的處理辦法會很不一樣。

  • 如果父類為非可序列化的,那麼它必然不會有引數建構函式。

Person.java 

package org.arpit.javapostsforlearning;
public class Person {
 
 String name="default";
 String nationality;
 
 public Person()
 {
  System.out.println("Person:Constructor");
 }

 public Person(String name, String nationality) {
  super();
  this.name = name;
  this.nationality = nationality;
 }
 
 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public String getNationality() {
  return nationality;
 }

 public void setNationality(String nationality) {
  this.nationality = nationality;
 }

}

在包org.arpit.javapostsforlearning 中建立Employee.java
Employee.java: 
package org.arpit.javapostsforlearning;
import java.io.Serializable;

public class Employee extends Person implements Serializable{

 int employeeId;
 String department;
 
 public Employee(int employeeId,String name,String department,String nationality)
 {
  super(name,nationality);
  this.employeeId=employeeId;
  this.department=department;
  System.out.println("Employee:Constructor");
 }
 
 public int getEmployeeId() {
  return employeeId;
 }
 public void setEmployeeId(int employeeId) {
  this.employeeId = employeeId;
 }
 
 public String getDepartment() {
  return department;
 }
 public void setDepartment(String department) {
  this.department = department;
 }
}
在org.arpit.javapostsforlearning包中建立SerializeDeserializeMain.java
SerializeDeserializeMain.java:
package org.arpit.javapostsforlearning;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SerializeDeserializeMain {

 /**
  * @author Arpit Mandliya
  */
 public static void main(String[] args) {

  //Serialize
  Employee emp = new Employee(101,"Arpit","CS","Indian");
  System.out.println("Before serializing");
  System.out.println("Emp id: " + emp.getEmployeeId());
  System.out.println("Name: " + emp.getName());
  System.out.println("Department: " + emp.getDepartment());
  System.out.println("Nationality: " + emp.getNationality());
  System.out.println("************");
  System.out.println("Serializing");
  try
  {
   FileOutputStream fileOut = new FileOutputStream("employee.ser");
   ObjectOutputStream outStream = new ObjectOutputStream(fileOut);
   outStream.writeObject(emp);
   outStream.close();
   fileOut.close();
  }catch(IOException i)
  {
   i.printStackTrace();
  }

  //Deserialize
  System.out.println("************");
  System.out.println("Deserializing");
  emp = null;
  try
  {
   FileInputStream fileIn =new FileInputStream("employee.ser");
   ObjectInputStream in = new ObjectInputStream(fileIn);
   emp = (Employee) in.readObject();
   in.close();
   fileIn.close();
  }catch(IOException i)
  {
   i.printStackTrace();
   return;
  }catch(ClassNotFoundException c)
  {
   System.out.println("Employee class not found");
   c.printStackTrace();
   return;
  }
  System.out.println("After serializing");
  System.out.println("Emp id: " + emp.getEmployeeId());
  System.out.println("Name: " + emp.getName());
  System.out.println("Department: " + emp.getDepartment());
  System.out.println("Nationality: " + emp.getNationality());
 }
}
執行:

當你執行SerializeDeserializeMain.java後,你會得到如下的輸出,如果父類是非可序列化的,那麼在反序列化過程中,所有繼承於父類的例項變數值,將會通過呼叫非序列化建構函式來初始化。 這裡 name繼承於person,所以在反序列化過程中,name將會被初始化為預設值。

案例6 - 如果父類是可序列化,但你不需要繼承類為可序列化

如果你不希望繼承類為可序列化,那麼你需要實現 writeObject() 和readObject() 方法,並且需要丟擲NotSerializableException 異常。

案例7 - 可否序列化靜態變數?

不能。因為靜態變數是類級別的,不是物件級別的,當你序列化一個物件的時候,是不能序列化靜態變數。

總結:

  • 序列化是java物件的values/states轉化為位元組並在網路中傳輸或者儲存它的過程。另外反序列化是把位元組碼翻譯為對應的物件的過程。
  • 序列化的好處是,JVM的獨立性,也就是說,一個物件可以在一個平臺中被序列化,然後在另外一個不同的平臺反序列化。
  • 如果你需要序列化任何物件,你必須實現標記介面Serializable。
  • Java中的標記介面(Marker interface)就是沒有欄位或者方法的介面,或者更簡單的說,空介面
  • serialVersionUID 是用於保證同一個物件(在序列化中會被用到)可以在Deserialization過程中被載入。serialVersionUID 是用於物件的版本控制。
  • 當你需要序列化任何包含引用物件的物件,那麼Java會自動序列化該物件的整個物件圖。
  • 如果你不希望序列化某個欄位,你可以標記它為trasient
  • 如果父類為可序列化,那麼它的繼承類也將是可序列化的。
  • 如果父類為非可序列化,那麼在反序列化過程中,所有繼承於父類的例項變數值將會通過呼叫非可序列化的構造器來初始化。
  • 如果你需希望子類為可序列化的,那麼你需要實現writeObject() 和 readObject() 方法,並在這兩個方法中丟擲NotSerializableException異常
  • 你不能序列化靜態變數。


Java 的序列化 (Serialization) 教程原文請看這裡

推薦文章