相信你們平常開發中,常常看到Java對象「implements Serializable」。那麼,它到底有什麼用呢?本文從如下幾個角度來解析序列這一塊知識點~html
Java對象是運行在JVM的堆內存中的,若是JVM中止後,它的生命也就戛然而止。java
打個比喻,做爲大城市漂泊的碼農,搬家是常態。當咱們搬書桌時,桌子太大了就通不過比較小的門,所以咱們須要把它拆開再搬過去,這個拆桌子的過程就是序列化。 而咱們把書桌復原回來(安裝)的過程就是反序列化啦。面試
序列化使得對象能夠脫離程序運行而獨立存在,它主要有兩種用途:segmentfault
好比 Web服務器中的Session對象,當有 10+萬用戶併發訪問的,就有可能出現10萬個Session對象,內存可能消化不良,因而Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內存中。數組
咱們在使用Dubbo遠程調用服務框架時,須要把傳輸的Java對象實現Serializable接口,即讓Java對象序列化,由於這樣才能讓對象在網絡上傳輸。bash
java.io.ObjectOutputStream
java.io.ObjectInputStream
java.io.Serializable
java.io.Externalizable
複製代碼
Serializable接口是一個標記接口,沒有方法或字段。一旦實現了此接口,就標誌該類的對象就是可序列化的。服務器
public interface Serializable {
}
複製代碼
Externalizable繼承了Serializable接口,還定義了兩個抽象方法:writeExternal()和readExternal(),若是開發人員使用Externalizable來實現序列化和反序列化,須要重寫writeExternal()和readExternal()方法網絡
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
複製代碼
表示對象輸出流,它的writeObject(Object obj)方法能夠對指定obj對象參數進行序列化,再把獲得的字節序列寫到一個目標輸出流中。併發
表示對象輸入流, 它的readObject()方法,從輸入流中讀取到字節序列,反序列化成爲一個對象,最後將其返回。框架
序列化如何使用?來看一下,序列化的使用的幾個關鍵點吧:
public class Student implements Serializable {
private Integer age;
private String name;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
複製代碼
把Student對象設置值後,寫入一個文件,即序列化,哈哈~
ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("D:\\text.out"));
Student student = new Student();
student.setAge(25);
student.setName("jayWei");
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();
複製代碼
看看序列化的可愛模樣吧,test.out文件內容以下(使用UltraEdit打開):
再把test.out文件讀取出來,反序列化爲Student對象
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student = (Student) objectInputStream.readObject();
System.out.println("name="+student.getName());
複製代碼
Serializable接口,只是一個空的接口,沒有方法或字段,爲何這麼神奇,實現了它就可讓對象序列化了?
public interface Serializable {
}
複製代碼
爲了驗證Serializable的做用,把以上demo的Student對象,去掉實現Serializable接口,看序列化過程怎樣吧~
序列化過程當中拋出異常啦,堆棧信息以下:
Exception in thread "main" java.io.NotSerializableException: com.example.demo.Student
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.example.demo.Test.main(Test.java:13)
複製代碼
順着堆棧信息看一下,原來有重大發現,以下~
序列化的方法就是writeObject,基於以上的demo,咱們來分析一波它的核心方法調用鏈吧~(建議你們也去debug看一下這個方法,感興趣的話)
writeObject直接調用的就是writeObject0()方法,
public final void writeObject(Object obj) throws IOException {
......
writeObject0(obj, false);
......
}
複製代碼
writeObject0 主要實現是對象的不一樣類型,調用不一樣的方法寫入序列化數據,這裏面若是對象實現了Serializable接口,就調用writeOrdinaryObject()方法~
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
......
//String類型
if (obj instanceof String) {
writeString((String) obj, unshared);
//數組類型
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
//枚舉類型
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
//Serializable實現序列化接口
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else{
//其餘狀況會拋異常~
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
......
複製代碼
writeOrdinaryObject()會先調用writeClassDesc(desc),寫入該類的生成信息,而後調用writeSerialData方法,寫入序列化數據
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
......
//調用ObjectStreamClass的寫入方法
writeClassDesc(desc, false);
// 判斷是否實現了Externalizable接口
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//寫入序列化數據
writeSerialData(obj, desc);
}
.....
}
複製代碼
writeSerialData()實現的就是寫入被序列化對象的字段數據
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
for (int i = 0; i < slots.length; i++) {
if (slotDesc.hasWriteObjectMethod()) {
//若是被序列化的對象自定義實現了writeObject()方法,則執行這個代碼塊
slotDesc.invokeWriteObject(obj, this);
} else {
// 調用默認的方法寫入實例數據
defaultWriteFields(obj, slotDesc);
}
}
}
複製代碼
defaultWriteFields()方法,獲取類的基本數據類型數據,直接寫入底層字節容器;獲取類的obj類型數據,循環遞歸調用writeObject0()方法,寫入數據~
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
// 獲取類的基本數據類型數據,保存到primVals字節數組
desc.getPrimFieldValues(obj, primVals);
//primVals的基本類型數據寫到底層字節容器
bout.write(primVals, 0, primDataSize, false);
// 獲取對應類的全部字段對象
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
// 獲取類的obj類型數據,保存到objVals字節數組
desc.getObjFieldValues(obj, objVals);
//對全部Object類型的字段,循環
for (int i = 0; i < objVals.length; i++) {
......
//遞歸調用writeObject0()方法,寫入對應的數據
writeObject0(objVals[i],
fields[numPrimFields + i].isUnshared());
......
}
}
複製代碼
static靜態變量和transient 修飾的字段是不會被序列化的,咱們來看例子分析一波~ Student類加了一個類變量gender和一個transient修飾的字段specialty
public class Student implements Serializable {
private Integer age;
private String name;
public static String gender = "男";
transient String specialty = "計算機專業";
public String getSpecialty() {
return specialty;
}
public void setSpecialty(String specialty) {
this.specialty = specialty;
}
@Override
public String toString() {
return "Student{" +"age=" + age + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", specialty='" + specialty + '\'' +
'}';
}
......
複製代碼
打印學生對象,序列化到文件,接着修改靜態變量的值,再反序列化,輸出反序列化後的對象~
序列化前Student{age=25, name='jayWei', gender='男', specialty='計算機專業'}
序列化後Student{age=25, name='jayWei', gender='女', specialty='null'}
複製代碼
對比結果能夠發現:
serialVersionUID 表面意思就是序列化版本號ID,其實每個實現Serializable接口的類,都有一個表示序列化版本標識符的靜態變量,或者默認等於1L,或者等於對象的哈希碼。
private static final long serialVersionUID = -6384871967268653799L;
複製代碼
serialVersionUID有什麼用?
JAVA序列化的機制是經過判斷類的serialVersionUID來驗證版本是否一致的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,若是相同,反序列化成功,若是不相同,就拋出InvalidClassException異常。
接下來,咱們來驗證一下吧,修改一下Student類,再反序列化操做
Exception in thread "main" java.io.InvalidClassException: com.example.demo.Student;
local class incompatible: stream classdesc serialVersionUID = 3096644667492403394,
local class serialVersionUID = 4429793331949928814
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1876)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1745)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2033)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1567)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:427)
at com.example.demo.Test.main(Test.java:20)
複製代碼
從日誌堆棧異常信息能夠看到,文件流中的class和當前類路徑中的class不一樣了,它們的serialVersionUID不相同,因此反序列化拋出InvalidClassException異常。那麼,若是確實須要修改Student類,又想反序列化成功,怎麼辦呢?能夠手動指定serialVersionUID的值,通常能夠設置爲1L或者,或者讓咱們的編輯器IDE生成
private static final long serialVersionUID = -6564022808907262054L;
複製代碼
實際上,阿里開發手冊,強制要求序列化類新增屬性時,不能修改serialVersionUID字段~
給Student類添加一個Teacher類型的成員變量,其中Teacher是沒有實現序列化接口的
public class Student implements Serializable {
private Integer age;
private String name;
private Teacher teacher;
...
}
//Teacher 沒有實現
public class Teacher {
......
}
複製代碼
序列化運行,就報NotSerializableException異常啦
Exception in thread "main" java.io.NotSerializableException: com.example.demo.Teacher
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at com.example.demo.Test.main(Test.java:16)
複製代碼
其實這個能夠在上小節的底層源碼分析找到答案,一個對象序列化過程,會循環調用它的Object類型字段,遞歸調用序列化的,也就是說,序列化Student類的時候,會對Teacher類進行序列化,可是對Teacher沒有實現序列化接口,所以拋出NotSerializableException異常。因此若是某個實例化類的成員變量是對象類型,則該對象類型的類必須實現序列化
子類Student實現了Serializable接口,父類User沒有實現Serializable接口
//父類實現了Serializable接口
public class Student extends User implements Serializable {
private Integer age;
private String name;
}
//父類沒有實現Serializable接口
public class User {
String userId;
}
Student student = new Student();
student.setAge(25);
student.setName("jayWei");
student.setUserId("1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\text.out"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();
//反序列化結果
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student1 = (Student) objectInputStream.readObject();
System.out.println(student1.getUserId());
//output
/**
* null
*/
複製代碼
從反序列化結果,能夠發現,父類屬性值丟失了。所以子類實現了Serializable接口,父類沒有實現Serializable接口的話,父類不會被序列化。
本文第六小節能夠回答這個問題,如回答Serializable關鍵字做用,序列化標誌啦,源碼中,它的做用啦~還有,能夠回答writeObject幾個核心方法,如直接寫入基本類型,獲取obj類型數據,循環遞歸寫入,哈哈~
能夠用transient關鍵字修飾,它能夠阻止修飾的字段被序列化到文件中,在被反序列化後,transient 字段的值被設爲初始值,好比int型的值會被設置爲 0,對象型初始值會被設置爲null。
Externalizable繼承了Serializable,給咱們提供 writeExternal() 和 readExternal() 方法, 讓咱們能夠控制 Java的序列化機制, 不依賴於Java的默認序列化。正確實現 Externalizable 接口能夠顯著提升應用程序的性能。
能夠看回本文第七小節哈,JAVA序列化的機制是經過判斷類的serialVersionUID來驗證版本是否一致的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較,若是相同,反序列化成功,若是不相同,就拋出InvalidClassException異常。
能夠的。咱們都知道,對於序列化一個對象需調用 ObjectOutputStream.writeObject(saveThisObject), 並用 ObjectInputStream.readObject() 讀取對象, 但 Java 虛擬機爲你提供的還有一件事, 是定義這兩個方法。若是在類中定義這兩種方法, 則 JVM 將調用這兩種方法, 而不是應用默認序列化機制。同時,能夠聲明這些方法爲私有方法,以免被繼承、重寫或重載。
static靜態變量和transient 修飾的字段是不會被序列化的。靜態(static)成員變量是屬於類級別的,而序列化是針對對象的。transient關鍵字修字段飾,能夠阻止該字段被序列化到文件中。