什么是java序列化
序列化:把對象轉(zhuǎn)換為字節(jié)序列的過程
反序列:把字節(jié)序列恢復(fù)為對象的過程
對象序列化機制(object serialization)是java語言內(nèi)建的一種對象持久化方式,通過對象序列化,可以將對象的狀態(tài)信息保存為字節(jié)數(shù)組,并且可以在有需要的時候?qū)⑦@個字節(jié)數(shù)組通過反序列化的方式轉(zhuǎn)換成對象,對象的序列化可以很容易的在JVM中的活動對象和字節(jié)數(shù)組(流)之間進行轉(zhuǎn)換。
簡單說:對象序列化成的字節(jié)序列會包含對象的類型信息、對象的數(shù)據(jù)等,說白了就是包含了描述這個對象的所有信息,能根據(jù)這些信息“復(fù)刻”出一個和原來一模一樣的對象。
在代碼運行的時候,我們可以看到很多的對象,可以是一個,也可以是一類對象的集合,很多的對象數(shù)據(jù),這些數(shù)據(jù)中,有些信息我們想讓他持久的保存起來,那么這個序列化,就是把內(nèi)存里面的這些對象給變成一連串的字節(jié)描述的過程。
常見的就是變成文件
什么情況需要使用 Java 序列化
想把的內(nèi)存中的對象狀態(tài)保存到一個文件中或者數(shù)據(jù)庫中時候;
想用套接字在網(wǎng)絡(luò)上傳送對象的時候;
想通過RMI(遠程方法調(diào)用)傳輸對象的時候。
為什么要序列化
Java對象是運行在JVM的堆內(nèi)存中的,如果JVM停止后,它的生命也就戛然而止。
如果想在JVM停止后,把這些對象保存到磁盤或者通過網(wǎng)絡(luò)傳輸?shù)搅硪贿h程機器,怎么辦呢?磁盤這些硬件可不認(rèn)識Java對象,它們只認(rèn)識二進制這些機器語言,所以我們就要把這些對象轉(zhuǎn)化為字節(jié)數(shù)組,這個過程就是序列化啦~
序列化和反序列化過程如下
因為在網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)只能是二進制。
序列化就是將對象轉(zhuǎn)換成二進制,反序列化就是講二進制轉(zhuǎn)化為對象的過程。
RPC 框架為什么需要序列化
因為網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)必須是二進制數(shù)據(jù),所以在 RPC 調(diào)用中,對入?yún)ο笈c返回值對象進行序列化與反序列化是一個必須的過程。
RPC 的通信流程
序列化用途
序列化使得對象可以脫離程序運行而獨立存在,它主要有兩種用途
序列化機制可以讓對象地保存到硬盤上,減輕內(nèi)存壓力的同時,也起了持久化的作用
比如 Web服務(wù)器中的Session對象,當(dāng)有 10+萬用戶并發(fā)訪問的,就有可能出現(xiàn)10萬個Session對象,內(nèi)存可能消化不良,于是Web容器就會把一些seesion先序列化到硬盤中,等要用了,再把保存在硬盤中的對象還原到內(nèi)存中。
序列化機制讓Java對象可以在網(wǎng)絡(luò)傳輸
我們在使用Dubbo遠程調(diào)用服務(wù)框架時,需要把傳輸?shù)腏ava對象實現(xiàn)Serializable接口,即讓Java對象序列化,因為這樣才能讓對象在網(wǎng)絡(luò)上傳輸。
實現(xiàn)對象序列化需要做哪些工作
JAVA提供了API實現(xiàn)了對象的序列化和反序列化的功能,使用這些API時需要遵守如下約定:
被序列化的對象類型需要實現(xiàn)序列化接口,此接口是標(biāo)志接口,沒有聲明任何的抽象方法,JAVA編譯器識別這個接口,自動的為這個類添加序列化和反序列化方法。
為了保持序列化過程的穩(wěn)定,建議在類中添加序列化版本號。
不想讓字段放在硬盤上就加transient
Java序列化常用API
java.io.ObjectOutputStream
java.io.ObjectInputStream
java.io.Serializable
java.io.Externalizable
Serializable 接口
Serializable接口是一個標(biāo)記接口,沒有方法或字段。一旦實現(xiàn)了此接口,就標(biāo)志該類的對象就是可序列化的。
public interface Serializable {
}
Externalizable 接口
Externalizable繼承了Serializable接口,還定義了兩個抽象方法:writeExternal()和readExternal(),如果開發(fā)人員使用Externalizable來實現(xiàn)序列化和反序列化,需要重寫writeExternal()和readExternal()方法
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
java.io.ObjectOutputStream類
表示對象輸出流,它的writeObject(Object obj)方法可以對指定obj對象參數(shù)進行序列化,再把得到的字節(jié)序列寫到一個目標(biāo)輸出流中。
java.io.ObjectInputStream
表示對象輸入流,它的readObject()方法,從輸入流中讀取到字節(jié)序列,反序列化成為一個對象,最后將其返回。
如何實現(xiàn)對象的序列化
- 對于要序列化對象的類要去實現(xiàn)Serializable接口或者Externalizable接口
- JDK提供的ObjectOutputStream類的writeObject方法,實現(xiàn)序列化
- JDK提供的ObjectInputStream類的readObject方法,實現(xiàn)反序列化
Serializable 實現(xiàn)序列化
public class TestBean implements Serializable {
private Integer id;
private String name;
private Date date;
//省去getter和setter方法和toString
}
序列化寫入文本,執(zhí)行后可以在test.txt文件中看到序列化內(nèi)容
public static void main(String[] args) {
TestBean testBean = new TestBean();
testBean.setDate(new Date());
testBean.setId(1);
testBean.setName("zll1");
//使用ObjectOutputStream序列化testBean對象并將其序列化成的字節(jié)序列寫入test.txt文件
try (FileOutputStream fileOutputStream = new FileOutputStream("D:\\test.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);) {
objectOutputStream.writeObject(testBean);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
反序列化:
public static void main(String[] args) {
try (FileInputStream fileInputStream = new FileInputStream("D:\\test.txt");
ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream)) {
TestBean testBean = (TestBean) objectInputStream.readObject();
System.out.println(testBean);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
輸出結(jié)果
TestBean{id=1, name='zll1', date=Fri Nov 27 14:52:48 CST 2020}
注意
- 一個對象要進行序列化,如果該對象成員變量是引用類型的,那這個引用類型也一定要是可序列化的,否則會報錯
- 同一個對象多次序列化成字節(jié)序列,這多個字節(jié)序列反序列化成的對象還是一個(使用==判斷為true)(因為所有序列化保存的對象都會生成一個序列化編號,當(dāng)再次序列化時回去檢查此對象是否已經(jīng)序列化了,如果是,那序列化只會輸出上個序列化的編號)
- 如果序列化一個可變對象,序列化之后,修改對象屬性值,再次序列化,只會保存上次序列化的編號(這是個坑注意下)
- 對于不想序列化的字段可以再字段類型之前加上transient關(guān)鍵字修飾(反序列化時會被賦予默認(rèn)值)
實現(xiàn)Externalizable接口
實現(xiàn)Externalizable接口必須重寫連個方法
- writeExternal(ObjectOutput out)
- readExternal(ObjectInput in)
writeObject 和 readObject 自定義序列化策略,代碼示例
public class TextBean implements Externalizable {
private Integer id;
private String name;
private Date date;
//可以自定義決定那些需要序列化
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id);
out.writeObject(name);
out.writeObject(date);
}
//可以自定義決定那些需要反序列化
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.id = in.readInt();
this.name = (String) in.readObject();
this.date = (Date) in.readObject();
}
//省去getter和setter方法和toString
}
序列化:
public static void main(String[] args) {
TextBean textBean = new TextBean();
textBean.setDate(new Date());
textBean.setId(1);
textBean.setName("zll1");
try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("D:\\externalizable.txt"))) {
outputStream.writeObject(textBean);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
反序列化:
public static void main(String[] args) {
try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\externalizable.txt"))) {
TextBean textBean = (TextBean) objectInputStream.readObject();
System.out.println(textBean);
//輸出結(jié)果:TextBean{id=1, name='zll1', date=Fri Nov 27 16:49:17 CST 2020}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
實現(xiàn)了 Serializable 接口是自動序列化的,實現(xiàn) Externalizable 則需要手動序列化,通過 writeExternal 和 readExternal 方法手動進行
注意:
- 序列化對象要提供無參構(gòu)造
- 如果序列化時一個字段沒有序列化,那反序列化是要注意別給為序列化的字段反序列化了
實現(xiàn)序列化代碼還可參考該博客說明,關(guān)于 serialVersionUID序列化中有為什么要加一串序列ID,下方博客有寫,為了解決序列化和反序列化的算法一致性,一般1L就可以。注意 靜態(tài)static的屬性,他不序列化
(47條消息) 什么是序列化,怎么序列化,為什么序列化,反序列化會遇到什么問題,如何解決。_老周聊架構(gòu)的博客-CSDN博客
什么是 transient?
被 transient 修飾的變量不能被序列化。
1)transient修飾的變量不能被序列化;
2)transient只作用于實現(xiàn) Serializable 接口;
3)transient只能用來修飾普通成員變量字段;
4)不管有沒有 transient 修飾,靜態(tài)變量都不能被序列化;
什么是serialVersionUID
serialVersionUID 表面意思就是序列化版本號ID,其實每一個實現(xiàn)Serializable接口的類,都有一個表示序列化版本標(biāo)識符的靜態(tài)變量,或者默認(rèn)等于1L,或者等于對象的哈希碼。
serialVersionUID的作用
先講述下序列化的過程:在進行序列化時,(JAVA的話,是JVM)會把當(dāng)前類的serialVersionUID寫入到字節(jié)序列中(也會寫入序列化的文件中),在反序列化時會將字節(jié)流中的serialVersionUID同本地對象中的serialVersionUID進行對比,一直的話進行反序列化,不一致則失敗報錯(報InvalidCastException異常)
故,JAVA序列化的機制是通過判斷類的serialVersionUID來驗證版本是否一致的。
阿里開發(fā)手冊,強制要求序列化類新增屬性時,不能修改serialVersionUID字段
serialVersionUID的生成有哪三種方式
private static final long serialVersionUID= XXXL :
- 顯式聲明:默認(rèn)的1L
- 顯式聲明:根據(jù)包名、類名、繼承關(guān)系、非私有的方法和屬性以及參數(shù)、返回值等諸多因素計算出的64位的hash值
- 隱式聲明:未顯式的聲明serialVersionUID時java序列化機制會根據(jù)Class自動生成一個serialVersionUID(最好不要這樣,因為如果Class發(fā)生變化,自動生成的serialVersionUID可能會隨之發(fā)生變化,導(dǎo)致匹配不上)
序列化類增加屬性時,最好不要修改serialVersionUID,避免反序列化失敗
IDEA中新建Class可以在類名上按alt+enter:
如果不顯示上圖提示,可以按照下面步驟設(shè)置:
注意
不同的serialVersionUID的值,會影響到反序列化,也就是數(shù)據(jù)的讀取,你寫1L,注意L大些。計算機是不區(qū)分大小寫的,但是,作為觀眾的我們,是要區(qū)分1和L的l,所以說,這個值,閑的沒事不要亂動,不然一個版本升級,舊數(shù)據(jù)就不兼容了,你還不知道問題在哪
如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現(xiàn)序列化
當(dāng)屬性是對象的時候,沒實現(xiàn)序列化接口,會產(chǎn)生異常Exception in thread “main” java.io.NotSerializableException: com....
代碼示例
給Student類添加一個Teacher類型的成員變量,其中Teacher是沒有實現(xiàn)序列化接口的
public class Student implements Serializable {
private Integer age;
private String name;
private Teacher teacher;
...
}
//Teacher 沒有實現(xiàn)
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)
其實這個可以在上小節(jié)的底層源碼分析找到答案,一個對象序列化過程,會循環(huán)調(diào)用它的Object類型字段,遞歸調(diào)用序列化的,也就是說,序列化Student類的時候,會對Teacher類進行序列化,但是對Teacher沒有實現(xiàn)序列化接口,因此拋出NotSerializableException異常。所以如果某個實例化類的成員變量是對象類型,則該對象類型的類必須實現(xiàn)序列化
子類實現(xiàn)了Serializable,父類沒有實現(xiàn)Serializable接口的話,父類不會被序列化。
子類Student實現(xiàn)了Serializable接口,父類User沒有實現(xiàn)Serializable接口
//父類實現(xiàn)了Serializable接口
public class Student extends User implements Serializable {
private Integer age;
private String name;
}
//父類沒有實現(xiàn)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();
//反序列化結(jié)果
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\text.out"));
Student student1 = (Student) objectInputStream.readObject();
System.out.println(student1.getUserId());
//output
/** * null */
從反序列化結(jié)果,可以發(fā)現(xiàn),父類屬性值丟失了。因此子類實現(xiàn)了Serializable接口,父類沒有實現(xiàn)Serializable接口的話,父類不會被序列化。
下面是摘自 jdk api 文檔里面關(guān)于接口 Serializable 的描述
類通過實現(xiàn) java.io.Serializable 接口以啟用其序列化功能。
未實現(xiàn)此接口的類將無法使其任何狀態(tài)序列化或反序列化。
可序列化類的所有子類型本身都是可序列化的。因為實現(xiàn)接口也是間接的等同于繼承。
序列化接口沒有方法或字段,僅用于標(biāo)識可序列化的語義。
關(guān)于 serialVersionUID 的描述
序列化運行時使用一個稱為 serialVersionUID 的版本號與每個可序列化類相關(guān)聯(lián),該序列號在反序列化過程中用于驗證序列化對象的發(fā)送者和接收者是否為該對象加載了與序列化兼容的類。
如果接收者加載的該對象的類的 serialVersionUID 與對應(yīng)的發(fā)送者的類的版本號不同,則反序列化將會導(dǎo)致 InvalidClassException。
可序列化類可以通過聲明名為 “serialVersionUID” 的字段(該字段必須是靜態(tài) (static)、最終 (final) 的 long 型字段)顯式聲明其自己的 serialVersionUID:
如果可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基于該類的各個方面計算該類的默認(rèn) serialVersionUID 值,如“Java? 對象序列化規(guī)范”中所述。
為什么要顯示聲明serialVersionUID 值
計算默認(rèn)的 serialVersionUID 對類的詳細(xì)信息具有較高的敏感性,根據(jù)編譯器實現(xiàn)的不同可能千差萬別,這樣在反序列化過程中可能會導(dǎo)致意外的 InvalidClassException。
因此,為保證 serialVersionUID 值跨不同 java 編譯器實現(xiàn)的一致性,序列化類必須聲明一個明確的 serialVersionUID 值。
還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(如果可能),原因是這種聲明僅應(yīng)用于直接聲明類 – serialVersionUID 字段作為繼承成員沒有用處。
數(shù)組類不能聲明一個明確的 serialVersionUID,因此它們總是具有默認(rèn)的計算值,但是數(shù)組類沒有匹配 serialVersionUID 值的要求。
JSON序列化和JDK序列化區(qū)別
對于對象轉(zhuǎn)化成json字符串和json字符串轉(zhuǎn)化成對象,也是屬于序列化和反序列化的范疇,相對于JDK提供的序列化機制,各有各的優(yōu)缺點:
- JDK序列化/反序列化:原生方法不依賴其他類庫、但是不能跨平臺使用、字節(jié)數(shù)較大
- json序列化/反序列化:json字符串可讀性高、可跨平臺使用無語言限制、擴展性好、但是需要第三方類庫、字節(jié)數(shù)較大
想了解json的使用可以看這里(https://mp.weixin.qq.com/s/S4R21FXSUPzpwBUv3uE6Xg)
常見的序列化
JDK原生序列化(前面已講過如何使用)
序列化的實現(xiàn)是由ObjectOutputStream完成,反序列化由ObjectInputStream完成。
JDK的序列化過程
序列化過程就是在讀取對象數(shù)據(jù)的時候,不斷的加入一些特殊分隔符,這些特殊分隔符在反序列化中使用。
頭部數(shù)據(jù)用來聲明序列化協(xié)議、版本,用于高版本向后兼容
對象數(shù)據(jù)主要包括類名、簽名、屬性名、屬性類型及屬性值、開頭結(jié)尾數(shù)據(jù)
存在對象引用、繼承的情況下,遞歸遍歷寫對象邏輯
任何一種序列化框架核心思想就是設(shè)計一個序列化協(xié)議,將對象的類型、屬性類型、屬性值按照固定的格式寫到二進制字節(jié)流中來完成序列化,再按照固定格式一一讀取對象的類型、屬性類型、屬性值,通過這些信息重建對象,完成反序列化
JSON序列化
JSON是典型的key-value形式,沒有數(shù)據(jù)類型,是一種文本型序列化框架
基于HTTP的RPC通信框架會采用JSON格式,存在以下問題
JSON序列化的額外空間開銷比較大,對于大數(shù)據(jù)量服務(wù)這意味著巨大的內(nèi)存和磁盤開銷,所以選擇JSON的時候,數(shù)據(jù)量要小。
JSON沒有類型,像Java這種強類型語言,需要反射統(tǒng)一解決,性能不太好
Hessian
Hession是動態(tài)類型、二進制、緊湊的、可跨語言移植的一種序列化框架。
Hessian協(xié)議要比JDK、JSON更加緊湊,性能要比JDK、JSON序列化高效很多,而且生成的字節(jié)數(shù)也更小。
Student student = new Student();
student.setNo(101);
student.setName("HESSIAN");
//把student對象轉(zhuǎn)化為byte數(shù)組
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bos);
output.writeObject(student);
output.flushBuffer();
byte[] data = bos.toByteArray();
bos.close();
//把剛才序列化出來的byte數(shù)組轉(zhuǎn)化為student對象
ByteArrayInputStream bis = new ByteArrayInputStream(data);
Hessian2Input input = new Hessian2Input(bis);
Student deStudent = (Student) input.readObject();
input.close();
System.out.println(deStudent);
存在問題:主要集中在對Java一些常見類型不支持
- linked系列:LinkedHashMap、LinkedHashSet不支持,可以通過CollectionDesric
- Locale類:通過擴展ContextSerializerFactory類修復(fù)
- Byte/Short反序列化為Integer
Protobuf
Google內(nèi)部的混合語言數(shù)據(jù)標(biāo)準(zhǔn),是一種輕便、高效的結(jié)構(gòu)化數(shù)據(jù)存儲格式,可以用于結(jié)構(gòu)化數(shù)據(jù)序列化,支持 Java、Python、C++、Go 等語言。
Protobuf 使用的時候需要定義 IDLProtobuf使用的時候需要定義IDL文件,使用不用語言的IDL編譯器,生成序列化工具類。
序列化體積比JSON、Hessian要小
IDL能清晰的描述語義,所以足以幫助并保證應(yīng)用程序之間的類型不會丟失,無需類似 XML 解析器;
序列化、反序列化速度很快,不需要反射獲取類型
消息格式升級和兼容性不錯,可以做到向后兼容。
// IDl 文件格式
synax = "proto3";
option java_package = "com.test";
option java_outer_classname = "StudentProtobuf";
message StudentMsg {
int32no=1;
//姓名
string name = 2;
}
StudentProtobuf.StudentMsg.Builder builder = StudentProtobuf.StudentMsg.newBuilder();
builder.setNo(103);
builder.setName("protobuf");
//把student對象轉(zhuǎn)化為byte數(shù)組
StudentProtobuf.StudentMsg msg = builder.build();
byte[] data = msg.toByteArray();
//把剛才序列化出來的byte數(shù)組轉(zhuǎn)化為student對象
StudentProtobuf.StudentMsg deStudent = StudentProtobuf.StudentMsg.parseFrom(dat
System.out.println(deStudent);
Protobuf 非常高效,但是對于具有反射和動態(tài)能力的語言來說,這樣用起來很費勁,這一點就不如 Hessian,比如用 Java 的話,這個預(yù)編譯過程不是必須的,可以考慮使用 Protostuff。
Protostuff
Protostuff 不需要依賴 IDL 文件,可以直接對 Java 領(lǐng)域?qū)ο筮M行反 / 序列化操作,在效率上跟 Protobuf 差不多,生成的二進制格式和 Protobuf 是完全相同的,可以說是一個 Java 版本的 Protobuf 序列化框架。但是
不支持null
不支持淡村的Map、List集合對象,需要包在對象里。
序列化底層
Serializable底層
Serializable接口,只是一個空的接口,沒有方法或字段,為什么這么神奇,實現(xiàn)了它就可以讓對象序列化了?
public interface Serializable {
}
為了驗證Serializable的作用,把實現(xiàn)序列化的對象,去掉實現(xiàn)Serializable接口,看序列化過程怎樣吧~
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;
}
}
序列化過程中拋出異常啦,堆棧信息如下:
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)
順著堆棧信息看一下,原來有重大發(fā)現(xiàn),如下~
**原來底層是這樣:**ObjectOutputStream 在序列化的時候,會判斷被序列化的Object是哪一種類型,String?array?enum?還是 Serializable,如果都不是的話,拋出 NotSerializableException異常。所以呀,Serializable真的只是一個標(biāo)志,一個序列化標(biāo)志~
writeObject(Object)
序列化的方法就是writeObject,基于以上的demo,我們來分析一波它的核心方法調(diào)用鏈吧~(建議大家也去debug看一下這個方法,感興趣的話)
writeObject直接調(diào)用的就是writeObject0()方法,
public final void writeObject(Object obj) throws IOException {
.....
writeObject0(obj, false);
.....
}
writeObject0 主要實現(xiàn)是對象的不同類型,調(diào)用不同的方法寫入序列化數(shù)據(jù),這里面如果對象實現(xiàn)了Serializable接口,就調(diào)用writeOrdinaryObject()方法~
private void writeObject0(Object obj, boolean unshared)
throws IOException {
//String類型
if (obj instanceof String) {
//數(shù)組類型
writeString((String) obj, unshared);
} else if (cl.isArray()) {
//枚舉類型
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
//Serializable實現(xiàn)序列化接口
writeEnum((Enum<?>) obj, desc, unshared);
} 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()會先調(diào)用writeClassDesc(desc),寫入該類的生成信息,然后調(diào)用writeSerialData方法,寫入序列化數(shù)據(jù)
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared) throws IOException {
.....
// 調(diào)用ObjectStreamClass的寫入方法
writeClassDesc(desc, false);
// 判斷是否實現(xiàn)了Externalizable接口
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
//寫入序列化數(shù)據(jù)
writeSerialData(obj, desc);
}
.....
}
writeSerialData()實現(xiàn)的就是寫入被序列化對象的字段數(shù)據(jù)
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
for (int i = 0; i < slots.length; i++) {
if (slotDesc.hasWriteObjectMethod()) {
//如果被序列化的對象自定義實現(xiàn)了writeObject()方法,則執(zhí)行這個代碼塊
slotDesc.invokeWriteObject(obj, this);
} else {
// 調(diào)用默認(rèn)的方法寫入實例數(shù)據(jù)
defaultWriteFields(obj, slotDesc);
}
}
}
defaultWriteFields()方法,獲取類的基本數(shù)據(jù)類型數(shù)據(jù),直接寫入底層字節(jié)容器;
獲取類的obj類型數(shù)據(jù),循環(huán)遞歸調(diào)用writeObject0()方法,寫入數(shù)據(jù)~
private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException {
// 獲取類的基本數(shù)據(jù)類型數(shù)據(jù),保存到primVals字節(jié)數(shù)組
desc.getPrimFieldValues(obj, primVals);
//primVals的基本類型數(shù)據(jù)寫到底層字節(jié)容器
bout.write(primVals, 0, primDataSize, false);
// 獲取對應(yīng)類的所有字段對象
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
// 獲取類的obj類型數(shù)據(jù),保存到objVals字節(jié)數(shù)組
desc.getObjFieldValues(obj, objVals);
//對所有Object類型的字段,循環(huán)
for (int i = 0; i < objVals.length; i++) {
//遞歸調(diào)用writeObject0()方法,寫入對應(yīng)的數(shù)據(jù)
writeObject0(objVals[i], fields[numPrimFields + i].isUnshared());
......
}
}
序列化注意事項
- static靜態(tài)變量和transient 修飾的字段是不會被序列化的
- serialVersionUID問題
- 如果某個序列化類的成員變量是對象類型,則該對象類型的類必須實現(xiàn)序列化
- 子類實現(xiàn)了序列化,父類沒有實現(xiàn)序列化,父類中的字段丟失問題
為什么static靜態(tài)變量和transient 修飾的字段是不會被序列化的
static靜態(tài)變量和transient 修飾的字段是不會被序列化的,我們來看例子分析一波~ Student類加了一個類變量gender和一個transient修飾的字段specialty
public class Student implements Serializable {
private Integer age;
private String name;
public static String gender = "男";
transient String specialty = "計算機專業(yè)";
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 + '\'' + '}';
}
......
}
打印學(xué)生對象,序列化到文件,接著修改靜態(tài)變量的值,再反序列化,輸出反序列化后的對象~
運行結(jié)果:
序列化前Student{age=25, name='jayWei', gender='男', specialty='計算機專業(yè)'}
序列化后Student{age=25, name='jayWei', gender='女', specialty='null'}
對比結(jié)果可以發(fā)現(xiàn):
- 1)序列化前的靜態(tài)變量性別明明是‘男’,序列化后再在程序中修改,反序列化后卻變成‘女’了,what?顯然這個靜態(tài)屬性并沒有進行序列化。其實,靜態(tài)(static)成員變量是屬于類級別的,而序列化是針對對象的~所以不能序列化哦。
- 2)經(jīng)過序列化和反序列化過程后,specialty字段變量值由’計算機專業(yè)’變?yōu)榭樟?,為什么呢?其實是因為transient關(guān)鍵字,它可以阻止修飾的字段被序列化到文件中,在被反序列化后,transient 字段的值被設(shè)為初始值,比如int型的值會被設(shè)置為 0,對象型初始值會被設(shè)置為null。
即:序列化并不保存靜態(tài)變量,Transient 關(guān)鍵字阻止該變量被序列化到文件中
序列化的底層是怎么實現(xiàn)的?
上文中已描述底層實現(xiàn),回答Serializable關(guān)鍵字作用,序列化標(biāo)志啦,源碼中,它的作用啦還有,可以回答writeObject幾個核心方法,如直接寫入基本類型,獲取obj類型數(shù)據(jù),循環(huán)遞歸寫入
序列化時,如何讓某些成員不要序列化?
可以用transient關(guān)鍵字修飾,它可以阻止修飾的字段被序列化到文件中,在被反序列化后,transient 字段的值被設(shè)為初始值,比如int型的值會被設(shè)置為 0,對象型初始值會被設(shè)置為null。
在 Java 中,Serializable 和 Externalizable 有什么區(qū)別
Externalizable繼承了Serializable,給我們提供 writeExternal() 和 readExternal() 方法, 讓我們可以控制 Java的序列化機制, 不依賴于Java的默認(rèn)序列化。正確實現(xiàn) Externalizable 接口可以顯著提高應(yīng)用程序的性能。
是否可以自定義序列化過程, 或者是否可以覆蓋 Java 中的默認(rèn)序列化過程?
可以的。我們都知道,對于序列化一個對象需調(diào)用 ObjectOutputStream.writeObject(saveThisObject), 并用 ObjectInputStream.readObject() 讀取對象, 但 Java 虛擬機為你提供的還有一件事, 是定義這兩個方法。如果在類中定義這兩種方法, 則 JVM 將調(diào)用這兩種方法, 而不是應(yīng)用默認(rèn)序列化機制。同時,可以聲明這些方法為私有方法,以避免被繼承、重寫或重載。
在 Java 序列化期間,哪些變量未序列化?
static靜態(tài)變量和transient 修飾的字段是不會被序列化的。
靜態(tài)(static)成員變量是屬于類級別的,而序列化是針對對象的。
transient關(guān)鍵字修字段飾,可以阻止該字段被序列化到文件中。
動態(tài)代理是什么?有哪些應(yīng)用?
動態(tài)代理是運行時動態(tài)生成代理類。
動態(tài)代理的應(yīng)用有 Spring AOP數(shù)據(jù)查詢、測試框架的后端 mock、rpc,Java注解對象獲取???
怎么實現(xiàn)動態(tài)代理?
JDK 原生動態(tài)代理和 cglib 動態(tài)代理。
JDK 原生動態(tài)代理是基于接口實現(xiàn)的,而 cglib 是基于繼承當(dāng)前類的子類實現(xiàn)的。
11、靜態(tài)編譯和動態(tài)編譯
**靜態(tài)編譯:**在編譯時確定類型,綁定對象
**動態(tài)編譯:**運行時確定類型,綁定對象
如何選擇序列化方式
- RPC序列化框架的性能和效率
- 空間開銷:序列化之后的二進制數(shù)據(jù)的體積大小
- 序列化協(xié)議的通用性和兼容性:多個語言,多個版本
- 序列化協(xié)議的安全性:JDK 原生序列化存在安全漏洞
首選的還是 Hessian 與 Protobuf,因為他們在性能、時間開銷、空間開銷、通用性、 兼容性和安全性上,都滿足了我們的要求。
Hessian 在使用上更加方便,在對象的兼 容性上更好;
Protobuf 則更加高效,通用性上更有優(yōu)勢。
RPC使用過程中注意哪些問題
對象要盡量簡單,沒有太多的依賴關(guān)系
入?yún)⒑头祷刂刁w積不要太大
盡量使用簡單的、常用的原生對象。
對象不要有復(fù)雜的繼承關(guān)系。
對象序列化網(wǎng)絡(luò)傳輸案例
來自于博文:(47條消息) JAVA 通過網(wǎng)絡(luò)傳輸對象(對象序列化)簡單示例_wjwisme的博客-CSDN博客
首先,要傳輸?shù)腟tudent類(實現(xiàn)Serializable接口):
public class Student implements Serializable{
private static final long serialVersionUID = 8683452581334592189L;
private String name;
private int age;
private int score;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "name:" + name + " age:" + age + " score:" + score;
}
}
Socket服務(wù)器類核心方法:
public static void openObjectServer(){
ServerSocket ss = null;
try {
ss = new ServerSocket(1111);
while(true){
final Socket socket = ss.accept();
new Runnable(){
public void run() {
try {
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
os.write("歡迎連接 服務(wù)器 一號!".getBytes());
ObjectInputStream ois = new ObjectInputStream(is);
Object object = ois.readObject();
//打印對象
System.out.println(object);
//關(guān)閉socket
socket.close();
}catch(Exception e){
e.printStackTrace();
}finally{
if(socket != null )
try {
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}.run();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally{
System.out.println("服務(wù)器關(guān)閉連接!");
try {
if(ss != null)
ss.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
說明:該服務(wù)器方法可以接受多個客戶端連接,一般網(wǎng)絡(luò)服務(wù)器編程核心思想應(yīng)該都是這樣,開啟后,支持多線程處理多個連接請求,互不影響。
Socket客戶端類:
public class ClientSocketClass {
public static void main(String[] args){
Socket socket = null;
try{
socket = new Socket(InetAddress.getByName("127.0.0.1"),1111);
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
Student student = new Student();
student.setAge(20);
student.setName("wjw");
student.setScore(100);
oos.writeObject(student);
}catch(Exception e){
e.printStackTrace();
}finally{
try {
if(socket != null)
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Java使用序列化實現(xiàn)深克隆
深拷貝的兩種實現(xiàn)方式
- 實現(xiàn)Cloneable接口,重寫Object類中clone()方法,實現(xiàn)層層克隆的方法。(clone()方法要求目標(biāo)類及其成員變量類都需要實現(xiàn)
java.lang.Cloneable
接口,并且覆寫java.lang.Object
的clone()
方法。) - 通過序列化(Serializable)的方法,將對象寫到流里,然后再從流中讀取出來。雖然這種方法效率很低,但是這種方法才是真正意義上的深度克隆。(序列化方法通過靜態(tài)方法實現(xiàn),其目標(biāo)類及其成員變量類都需要實現(xiàn)
java.lang.Serializable
接口)
性能對比:序列化克隆和Cloneable,Cloneable更快
Java實現(xiàn)深克隆的兩種方式 - luankun0214 - 博客園 (cnblogs.com)
由于序列化的方式實現(xiàn)深度克隆性能較差,還是推薦使用Cloneable接口的方式重寫clone方法,但是注意該方法需要注意克隆對象的對象(多層)。
使用Serializable方式實現(xiàn)深拷貝
需要被克隆的對象實現(xiàn)Serializable接口,克隆的過程相對比較通用
被克隆對象:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SerialCar implements Serializable {
private static final long serialVersionUID = -7308342867043888945L;
private String carType;
private String carName;
@Override
public String toString() {
return "SerialCar{" +
"carType='" + carType + '\'' +
", carName='" + carName + '\'' +
'}';
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SerialUser implements Serializable {
private static final long serialVersionUID = -2167957013938386204L;
private String username;
private SerialCar serialCar;
@Override
public String toString() {
return "SerialUser{" +
"username='" + username + '\'' +
", serialCar=" + serialCar +
'}';
}
}
import java.io.*;
public class TestDeepClone {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerialUser serialUser = new SerialUser("李四", new SerialCar("奔馳", "250"));
//在內(nèi)存中開辟一塊緩沖區(qū),將對象序列化成流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(serialUser);
//找到這一塊緩沖區(qū),將字節(jié)流反序列化成另一個對象
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
SerialUser cloneUser = (SerialUser)ois.readObject();
SerialCar cloneCar = cloneUser.getSerialCar();
cloneCar.setCarType("奧迪");
cloneCar.setCarName("A8");
System.out.println(serialUser);
System.out.println(cloneUser);
}
}
- 類需要實現(xiàn)
java.lang.Serializable
接口。否則代碼在運行時報錯。
解釋:
對象類Exam
需要實現(xiàn)java.lang.Serializable
接口,否則會在代碼執(zhí)行到os.writeObject(exam)
時拋出NotSerializableException
異常。
- 父類中的成員變量子類也需要實現(xiàn)
java.lang.Serializable
接口。否則在運行時報錯。
解釋:
當(dāng)父類中包含了成員變量子類時,如果只有父類實現(xiàn)java.lang.Serializable
接口,但是子類沒有實現(xiàn)java.lang.Serializable
接口,那么代碼執(zhí)行到os.writeObject(exam)
時還是會**拋出NotSerializableException
異常。
使用clone方式(淺拷貝)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Exam implements Cloneable {
private int examId;
private String examName;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
測試類
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Exam exam = new Exam(1, "語文考試");
Exam cloneExam = (Exam) exam.clone();
System.out.println(cloneExam != exam);
System.out.println(cloneExam.equals(exam));
}
}
控制臺輸出:
true
false
我們確實拷貝出了另一個對象。equals沒有覆寫,所以調(diào)用的是java.lang.Object
中的以下方法:
public boolean equals(Object obj) {
return (this == obj);
}
調(diào)用clone方法的前提
- 類
Exam
需要繼承java.lang.Cloneable
接口。否則代碼在運行時報錯。
解釋:
調(diào)用exam.clone()
的對象類Exam
需要繼承Cloneable
接口,否則會在代碼運行時拋出CloneNotSupportedException
異常
- 類
Exam
需要覆寫父類的clone()
方法。否則代碼在編譯時報錯。
解釋:
因為clone()
在java.lang.Object
中是protected
訪問控制。如果不覆寫,exam.clone()
這句代碼無法編譯通過。
clone方法的存在問題
閱讀
java.lang.Object
中的clone()
方法上的英文注釋時有這樣一段話:this method creates a new instance of the class of this object and initializes all its fields with exactly the contents of the corresponding fields of this object, as if by assignment; the contents of the fields are not themselves cloned. Thus, this method performs a “shallow copy” of this object, not a “deep copy” operation.
翻譯為:
該方法創(chuàng)建該對象類的新實例,并使用該對象相應(yīng)字段的內(nèi)容完全初始化其所有字段,就像通過賦值一樣; 字段的內(nèi)容本身不會被克隆。 因此,此方法執(zhí)行此對象的“淺復(fù)制”,而不是“深復(fù)制”操作。
代碼示例
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Teacher {
private String name;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Exam implements Cloneable {
private int examId;
private String examName;
private Teacher teacher;
@Override
public String toString() {
return "Exam{" +
"examId=" + examId +
", examName='" + examName + '\'' +
", teacher=" + teacher +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
測試類
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Exam exam = new Exam(1, "語文考試");
Teacher teacher = new Teacher("馬老師");
exam.setTeacher(teacher);
Exam cloneExam = (Exam) exam.clone();
System.out.println(cloneExam != exam);
System.out.println(cloneExam.equals(exam));
cloneExam.getTeacher().setName("Lily");
System.out.println(exam.toString());
System.out.println(cloneExam.toString());
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Exam exam = new Exam(1, "語文考試");
Exam cloneExam = (Exam) exam.clone();
System.out.println(cloneExam != exam);
System.out.println(cloneExam.equals(exam));
}
}
控制臺輸出:
true
false
Exam{examId=1, examName='語文考試', teacher=Teacher{name='Lily'}}
Exam{examId=1, examName='語文考試', teacher=Teacher{name='Lily'}}
原本只想將克隆出來的考試的監(jiān)考老師改為 Lily ,但是把原考試對象的監(jiān)考老師也修改了
使用clone方式實現(xiàn)“深拷貝”
覆寫考試類Exam.java
的clone()
方法
@Override
protected Object clone() throws CloneNotSupportedException {
Exam exam = (Exam) super.clone();
if (teacher != null) {
Teacher teacher = (Teacher) this.teacher.clone();
exam.setTeacher(teacher);
}
return exam;
}
解析
用上述方法,取代return super.clone()
的默認(rèn)實現(xiàn)。同時因為這里調(diào)用了teacher.clone()
,所以類Teacher
也要實現(xiàn)Cloneable
接口,覆寫clone()
方法。
改寫老師類Teacher.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Teacher implements Cloneable{
private String name;
@Override
public String toString() {
return "Teacher{" +
"name='" + name + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
控制臺輸出:
true
false
Exam{examId=1, examName=‘語文考試’, teacher=Teacher{name=‘馬老師’}}
Exam{examId=1, examName=‘語文考試’, teacher=Teacher{name=‘Lily’}}
使用泛型實現(xiàn)序列化深拷貝方法
public class Util {
private Util() {}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T obj) {
T cloneObj = null;
try {
//寫入字節(jié)流
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配內(nèi)存,寫入原始對象,生成新對象
ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新對象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
使用該方法可以在代碼編譯期檢查出沒有實現(xiàn)java.lang.Serializable
接口的對象。
作者留言
平時也經(jīng)常用到序列化,使用無非就是 implements Serializable ,又加了個UID而已.一直沒有去深入理解其含義,為了把JAVA基礎(chǔ)打牢,對這些知識點都做了深度學(xué)習(xí),看了很多篇博文,存檔在這只是為了個人學(xué)習(xí)筆記使用,可能還有些文獻沒有標(biāo)進來,實在是太多了,記不住看過哪篇,還請見諒!!!
參考文獻
(47條消息) 對象在網(wǎng)絡(luò)中如何傳輸之序列化_序列化和傳輸方式有關(guān)嗎_程序員面試那點事兒的博客-CSDN博客
(47條消息) 靜態(tài)變量能被序列化嗎?_靜態(tài)變量可以序列化嗎_碼上得天下的博客-CSDN博客
一文搞懂序列化與反序列化 - 知乎 (zhihu.com)
全方位解析Java的序列化 - 知乎 (zhihu.com)文章來源:http://www.zghlxwxcb.cn/news/detail-458604.html
一步步分析Java深拷貝的兩種方式-clone和序列化 - 極客子羽 - 博客園 (cnblogs.com)文章來源地址http://www.zghlxwxcb.cn/news/detail-458604.html
到了這里,關(guān)于從淺入深理解序列化和反序列化的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!