java序列化機制詳解
java序列化是將java對象保存在文件或者通過網絡傳輸的機制,通過實現接口Serializable或者Externalizable標識該類的對象可以序列化和反序列化。
如果希望保存java對象的狀態並在以後的某個時刻在內存中重建該對象,我們可以通過java序列化的機制實現。
Serializable關鍵字
標識類對象是可序列化的只要實現Serializable接口即可,該接口不包括任何字段和方法,它只是一個空的接口。如以下代碼片斷,Student類實現了該接口並自動獲得了序列化的能力。
//實現了Serializable接口的類自動擁有序列化的能力
public class Student implements Serializable {
}
對象的狀態是由成員變量決定的,所以序列化保存的是成員變量的值。static修飾的是類變量,序列化的時候static變量不會保存,另外,我們還可以通過java提供的transient關鍵字來顯示標識變量不需要序列化。因此java序列化的是除了static和transient修飾的其它成員變量。
ObjectOutputSteram和ObjectInputStream
如何使用java的序列化機制呢?
java提供了ObjectOutputStream和ObjectInputStream兩個對象輸入輸出流來實現序列化。假設Student類需要序列化,類的定義如下:
import java.io.Serializable;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午7:30
*/
public class Student implements Serializable {
private Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
下面代碼是將Student對象序列化的例子
該例中將對象序列化到了文件student.out中,通過ObjectOutputStream將對象jack寫到文件輸出流中。接着通過ObjectInputStream將jack對象反序列化並強制轉換爲Student類。(注意此時需要捕獲ClassNotFoundException類,假如此時JVM找不到Student Class對象,將會拋出該異常。)
import java.io.*;
/**
* <p>文件描述: 對象序列化Demo</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午7:34
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
/**
* 對象輸出流,具體輸出流是文件輸出
*/
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
/**
* 通過ObjectOutputStream將對象寫入到文件輸出流
*/
oos.writeObject(jack);
oos.close();
/**
* 對象輸入流,具體輸入是文件輸入流
*/
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
/**
* 通過ObjectInputStream將對象從文件輸入流讀入
*/
Student stu = (Student) ois.readObject();
ois.close();
System.out.println(stu);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
輸出:
Student{age=23, name='jack'}
若Student類未實現Serializable接口,將會拋出運行時異常 java.io.NotSerializableException。
transient關鍵字
若將Student的age字段設置爲transient的,反序化後age字段將是空值:
import java.io.Serializable;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午7:40
*/
public class Student implements Serializable{
//transient修飾的字段默認不會被序列化
private transient Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo運行後的輸出爲:
Student{age=null, name='jack'}
可以看到輸出後的age爲null,transient修飾的字段將不會被序列化。
static修飾的成員變量
前面我們說過java序列化包括的是除了static和transient修飾的其它成員變量。假設將Student的name字段設置爲static,看下Demo輸出結果:
import java.io.Serializable;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午7:55
*/
public class Student implements Serializable{
private Integer age;
private static String name = "student";
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo輸出結果爲:
Student{age=23, name='jack'}
看到反序列化後name的值是”jack”而不是”student”,這是爲何?
這正是因爲不會序列化static修飾的變量而導致的結果。序列化後的文件其實name字段值爲空,反序列化後去方法區查找靜態變量name的值,這個name值正是之前Demo類通過Student的構造函數將name值設置爲”jack”的值,所以最終反序列化後的name值爲”jack”。
也就是說這個”name”值不是通過反序列化得到的,而是通過去方法區拿到的全局的值。
如果讀者對上例有疑惑,可以繼續看下面的例子:
import java.io.*;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午20:30
*/
public class Student implements Serializable{
private Integer age;
private static String name = "student";
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
//增加了設置靜態變量的方法
public static void setName(String name) {
Student.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
import java.io.*;
/**
* <p>文件描述: </p>
*
* @Author luanmousheng
* @Date 17/7/1 下午20:52
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
oos.writeObject(jack);
oos.close();
//反序列化前將靜態變量設置爲"tom"
Student.setName("tom");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
Student stu = (Student) ois.readObject();
ois.close();
System.out.println(stu);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Demo輸出結果爲:
Student{age=23, name='tom'}
輸出結果的name值爲”tom”,而不是寫入時的”jack”。因爲在反序列化之前已經將靜態變量name的值設置爲”tom”,反序列化後會去取這個靜態變量的值。:smile:
writeObject()和readObject()方法
如果沒有爲類添加這兩個方法,序列化和反序列化是通過ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法進行的。
如果爲類添加writeObject()和readObject()方法,將不會再調用ObjectOutputStream的defaultWriteObject()和ObjectInputStream的defaultReadObject()方法,我們可以更加精確的操作對象的序列化。
例如如果將Student類的age字段設置爲transient了,此時我們又想去序列化該字段,那麼我們通過這兩個方法來實現:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午21:12
*/
public class Student implements Serializable{
private transient Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
//默認的序列化
oos.defaultWriteObject();
oos.writeInt(age);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//默認的反序列化
ois.defaultReadObject();
this.age = ois.readInt();
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
此時Demo的輸出結果將會輸出age的值:
Student{age=23, name='jack'}
注意,這兩個方法都是private的。方法中首先調用了序列化的默認方法,之後纔是自定義的邏輯。還要注意的是序列化的順序和反序列化的順序必須一致。否則會讀出未知的結果。例如將Student的age和name都設置爲transient,在writeObject和readObject方法中調整寫入和讀出的順序:
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午21:29
*/
public class Student implements Serializable{
private transient Integer age;
private transient String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
//默認的序列化
oos.defaultWriteObject();
oos.writeInt(age);
oos.writeUTF(name);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
//默認的序列化
ois.defaultReadObject();
//此處讀入的順序和寫入的順序不一致
this.name = ois.readUTF();
this.age = ois.readInt();
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo輸出結果讓人莫名其妙:
Student{age=1507332, name=''}
此處如果將oos.writeUTF方法改爲oos.writeObject、ois.readUTF方法改爲ois.readObject,會拋出異常 OptionalDataException
Externalizable關鍵字
實現了該接口的類基於之前Serializable的序列化機制就會失效,Externalizable接口已經實現了Serializable。看下面的例子:
import java.io.*;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午21:53
*/
public class Student implements Externalizable{
private Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
將Student改爲上面的代碼,運行Demo類的輸出結果爲:
java.io.InvalidClassException: com.lms.serializable.Student; no valid constructor
異常拋出,提示沒有可用的構造器,其實就是沒有默認的構造函數。實現了Externalizable接口的類對象,反序列化的時候會先通過無參的構造函數new一個對象,然後將各個字段的值賦值給該對象,Student沒有默認的構造函數,所以上例拋出異常。
爲Student添加默認構造函數後的Demo輸出結果爲:
Student{age=null, name='null'}
可以看出,所有字段爲空,這是因爲實現的Externalizable接口的對象序列化需要開發者自己去保證,也就是需要實現writeExternal和readExternal方法,實現了writeExternal和readExternal方法後的Student類如下:
import java.io.*;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午22:14
*/
public class Student implements Externalizable{
private Integer age;
private String name;
public Student() {
}
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(age);
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
age = in.readInt();
name = (String) in.readObject();
}
}
Demo輸出結果爲:
Student{age=23, name='jack'}
可以看出正確輸出了結果。
單例的序列化
如果一個類是單例,反序列化後是同一個對象嗎?我們將Student改爲單例模式的(Student用單例模式確實有點牽強),看下例:
import java.io.*;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午22:40
*/
public class Student implements Serializable{
private Integer age;
private String name;
private static class Instance {
private static Student jack = new Student(23, "jack");
}
private Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public static Student getInstance() {
return Instance.jack;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo類修改爲:
import java.io.*;
/**
* <p>文件描述:Demo類 </p>
*
* @Author luanmousheng
* @Date 17/7/1 下午23:00
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
//取單例
Student jack = Student.getInstance();
oos.writeObject(jack);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
Student stu = (Student) ois.readObject();
ois.close();
System.out.println(stu);
//看反序列化後是否是同一個對象
System.out.println(jack == stu);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Demo輸出結果爲:
Student{age=23, name='jack'}
false
可以看出,反序列化後不是同一個對象。那要怎麼解決這個問題呢?可以在單例類Student中加個私有方法readResolve()方法。
import java.io.*;
/**
* <p>文件描述: 需要序列化的類需要實現Serializable接口</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午23:20
*/
public class Student implements Serializable{
private Integer age;
private String name;
private static class Instance {
private static Student jack = new Student(23, "jack");
}
private Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public static Student getInstance() {
return Instance.jack;
}
//增加readResolve方法,直接返回單例
private Object readResolve() {
return Instance.jack;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
Demo輸出結果爲:
Student{age=23, name='jack'}
true
可以看到,反序列化後的對象和序列化之前的對象是同一個對象。
無論是實現Serializable或者Externalizable接口的對象,反序列化的時候都會調用readResolve方法,因此這裏的readResolve方法直接返回了單例,而不是返回反序列化後的對象,保證了單例的邏輯。
序列化ID
實現Serializable接口的類需要生成對應的序列化id(serialVersionUID)。如:
private static final long serialVersionUID = -5881678404557689785L;
如果沒有顯示爲類添加該字段(IDE可以設置成自動生成),java將會根據字節碼文件動態生成序列化ID。每次重新編譯生成的序列化ID都不一樣。所以前面的Student例子都不是完美的,沒有生成序列化ID。
如果顯示爲類添加該字段,那麼不管編譯多少次,只要沒有修改該字段的值,該字段就不會變。
爲何需要序列化ID?
如果一個類X在多個客戶端使用,其中一個客戶端A收到來自客戶端B的類X對象x。客戶端A通過網絡收到對象x的二進制序列化文件後會進行反序列化操作。首先客戶端A會取出對象x的序列化ID,並將該ID和客戶端A本地類X Class對象的序列化ID進行比較,如果不相等那就會提示序列化失敗,如果相等那就可以正常進行序列化。本人所在的公司現在使用的RPC框架、阿里開源的dubbo所使用的網絡傳輸對象就是實現了Serializable接口。
父類和子類實現序列化的問題
- 父類實現了序列化接口,子類自動擁有序列化功能,不需要顯示的實現序列化接口
- 父類沒有實現序列化接口,子類實現了序列化接口。序列化子類對象時不會去序列化父類的對象。因爲必須有父對象纔有子對象,反序列化時會調用父類的無參默認構造函數,因此父類必須要有無參默認構造函數,反序列化後父類對象字段值都是其類型默認值(基本類型是0值,引用類型是null)
序列化多次相同對象的問題
如果序列化兩個相同的對象,java做了特殊處理,看下例:
import java.io.*;
/**
* <p>文件描述: Demo類</p>
*
* @Author luanmousheng
* @Date 17/7/1 下午23:40
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
//第一次序列化對象
oos.writeObject(jack);
oos.flush();
//第一次序列化對象後的文件大小
System.out.println(new File("student.out").length());
//第二次序列化同一個對象
oos.writeObject(jack);
//第二次序列化對象後的文件大小
System.out.println(new File("student.out").length());
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
//第一次反序列化對象
Student stu1 = (Student) ois.readObject();
//第二次反序列化對象
Student stu2 = (Student) ois.readObject();
ois.close();
//比較是否同一個對象
System.out.println(stu1 == stu2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
Demo輸出結果爲:
189
194
true
可以看到兩次反序列化後的對象是同一個對象。
第一次序列化後的文件大小爲189,第二次序列化後的文件大小爲194,只增加了5字節,這顯示是java做了優化導致的。兩個相同對象當然不需要冗餘存儲,這增加的5字節是一些引用信息,這些引用指向同一個對象,反序列化後重建引用關係。
另外需要注意的是,如果在第二次序列化同一個對象之前,改變了這個對象某些字段的值,因爲虛擬機發現之前已經將該對象序列化過了,這時只會存儲寫的引用,不會實際去寫,所以第二次序列化之前對這些字段的改變實際上是沒有效果的。
例如:
import java.io.*;
/**
* <p>文件描述:Demo類 </p>
*
* @Author luanmousheng
* @Date 17/7/1 下午23:54
*/
public class SerializableDemo {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.out"));
Student jack = new Student(23, "jack");
//第一次序列化對象
oos.writeObject(jack);
oos.flush();
//第一次序列化對象後的文件大小
System.out.println(new File("student.out").length());
//第二次序列化之前將name值改爲"lucy"
jack.setName("lucy");
//第二次序列化同一個對象
oos.writeObject(jack);
//第二次序列化對象後的文件大小
System.out.println(new File("student.out").length());
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.out"));
//第一次反序列化對象
Student stu1 = (Student) ois.readObject();
//第二次反序列化對象
Student stu2 = (Student) ois.readObject();
ois.close();
//比較是否同一個對象
System.out.println(stu1);
System.out.println(stu2);
System.out.println(stu1 == stu2);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
輸出結果爲:
189
194
Student{age=23, name='jack'}
Student{age=23, name='jack'}
true
可以發現輸出的兩個對象值完全相同,第二次反序列化之前將值改爲”lucy”是無效的。