Java序列化與反序列化的實例分析講解
序列化與反序列化
Java對象是有生命周期的,當(dāng)生命周期結(jié)束它就會被回收,但是可以通過將其轉(zhuǎn)換為字節(jié)序列永久保存下來或者通過網(wǎng)絡(luò)傳輸給另一方。
把對象轉(zhuǎn)換為字節(jié)序列的過程稱為對象的序列化;把字節(jié)序列恢復(fù)為對象的過程稱為對象的反序列化。
Serializable接口
一個類實現(xiàn)java.io.Serializable接口就可以被序列化或者反序列化。實際上,Serializable接口中沒有任何變量和方法,它只是一個標(biāo)識。如果沒有實現(xiàn)這個接口,在序列化或者反序列化時會拋出NotSerializableException異常。
下面是一個實現(xiàn)了Serializable接口的類以及它的序列化與反序列化過程。
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("test");
// 序列化,存儲對象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(test);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,從文本中取出對象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
Test1 test1 = (Test1) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test implements Serializable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
運行結(jié)果:
Test{name='test'}
serialVersionUID
private static final long serialVersionUID = -3297006450515623366L;
serialVersionUID是一個序列化版本號,實現(xiàn)Serializable接口的類都會有一個版本號。如果沒有自己定義,那么程序會默認(rèn)生成一個版本號,這個版本號是Java運行時環(huán)境根據(jù)類的內(nèi)部細(xì)節(jié)自動生成的。最好我們自己定義該版本號,否則當(dāng)類發(fā)生改變時,程序為我們自動生成的序列化版本號也會發(fā)生改變,那么再將原來的字節(jié)序列反序列化時就會發(fā)生錯誤。
下面是將Test1類加入一個變量age,此時再進(jìn)行反序列化的結(jié)果。可以看出,序列化版本號已發(fā)生改變,程序認(rèn)為不是同一個類,不能進(jìn)行反序列化。
java.io.InvalidClassException: test.Test1; local class incompatible: stream classdesc serialVersionUID = 9097989105451761251, local class serialVersionUID = -7756223913249050270 at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:689) at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1903) at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1772) at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2060) at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1594) at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:430) at test.SerialTest.main(SerialTest.java:11)
為了提高serialVersionUID的獨立性和確定性,強烈建議在一個可序列化類中顯示地定義serialVersionUID,為他賦予明確的值。
那么在IDEA中,怎么手動生成呢?
在settings->Editor->Inspections下,搜索serial,開啟Serializable class without 'serialVersionUID'的拼寫檢查,然后將光標(biāo)放在實現(xiàn)Serializable的接口上,按住ALt+Enter鍵,選擇添加serialVersionUID即可。
Transient關(guān)鍵字
transient修飾類的變量,可以使變量不被序列化。反序列化時,被transient修飾的變量的值被設(shè)為初始值,如int類型被設(shè)為0,對象型被設(shè)為null。
ObjectOutputStream類和ObjectInputStream類
ObjectOutputStream的writeObject方法可以序列化對象,ObjectInputStream的readObject可以反序列化對象。ObjectOutputStream實現(xiàn)了接口ObjectOutput,所以可以進(jìn)行對象寫操作。ObjectInputStream實現(xiàn)了接口ObjectInput,所以可以對對象進(jìn)行讀操作。
靜態(tài)變量序列化
給Test類中增加一個靜態(tài)變量,賦值為12,然后在序列化之后修改其值為10,反序列化之后打印它的值。發(fā)現(xiàn)打印的值為10,之前的12并沒有被保存。
靜態(tài)變量是不參與序列化的,序列化只是用來保存對象的狀態(tài),而靜態(tài)變量屬于類的狀態(tài)。
父類序列化
讓Test繼承一個沒有實現(xiàn)Serializable接口的類,設(shè)置父類中變量的值,對Test類的實例進(jìn)行序列化與反序列化操作。
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("huihui");
test.setSex(12);
// 序列化,存儲對象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(test);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,從文本中取出對象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
Test test1 = (Test) ois.readObject();
System.out.println(test1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test extends TestFather implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
"sex='" + sex + '\'' +
'}';
}
}
class TestFather {
protected Integer sex;
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
@Override
public String toString() {
return "TestFather{" +
"sex='" + sex + '\'' +
'}';
}
}
運行結(jié)果:
Test{name='huihui'sex='null'}
發(fā)現(xiàn)雖然對sex進(jìn)行了復(fù)制,但是反序列化結(jié)果仍然為null。
現(xiàn)在讓TestFather類實現(xiàn)Serializable接口,運行結(jié)果如下。所以當(dāng)我們想要序列化父類的變量時,也需要讓父類實現(xiàn)Serializable接口。
Test{name='huihui'sex='12'}
同理,如果Test類中有任何變量是對象,那么該對象的類也需要實現(xiàn)Serializable接口。查看String源代碼,確實實現(xiàn)了Serializable接口。大家可以測試一下字段的類不實現(xiàn)Serializable接口的情況,運行會直接報java.io.NotSerializableException異常。
敏感字段加密
如果對于某些字段我們并不想直接暴露出去,需要對其進(jìn)行加密處理,那么就需要我們自定義序列化和反序列化方法。使用Serializable接口進(jìn)行序列化時,如果不自定義方法,則默認(rèn)調(diào)用ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。下面我們來嘗試一下自己實現(xiàn)序列化與反序列化過程。
class Test implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
private void writeObject(ObjectOutputStream out) {
try {
ObjectOutputStream.PutField putField = out.putFields();
System.out.println("原name:" + name);
// 模擬加密
name = "change";
putField.put("name", name);
System.out.println("加密后的name:" + name);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField getField = in.readFields();
Object object = getField.get("name", "");
System.out.println("要解密的name:" + object.toString());
name = "huihui";
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
運行結(jié)果:
原name:huihui
加密后的name:change
要解密的name:change
解密后的name:huihui
這種寫法重寫了writeObject方法和readObject方法,下面一種接口也可以實現(xiàn)相同的功能。
Externalizable接口
除了Serializable接口,Java還提供了一個Externalizable接口,它繼承了Serializable接口,提供了writeExternal和readExternal兩個方法,實現(xiàn)該接口的類必須重寫這兩個方法。同時還發(fā)現(xiàn),類還必須提供一個無參構(gòu)造方法,否則會報java.io.InvalidClassException異常。
先不深究為什么要加一個無參構(gòu)造方法,我們先試一下這個接口的序列化效果。將類Test改為如下所示:
class Test implements Externalizable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public Test() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
}
}
再次運行測試方法,發(fā)現(xiàn)輸出的name是null。在readObject處打斷點,發(fā)現(xiàn)會調(diào)用無參構(gòu)造方法。
name其實并沒有被序列化與反序列化,writeExternal方法和readExternal方法中是需要我們自己來實現(xiàn)序列化與反序列化的細(xì)節(jié)的。在反序列化時,會首先調(diào)用類的無參考構(gòu)造方法創(chuàng)建一個新對象,然后再填充每個字段。
我們對writeExternal方法和readExternal方法進(jìn)行重寫:
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
}
此時運行測試方法,發(fā)現(xiàn)Test類被正常序列化與反序列化。
序列化存儲規(guī)則
當(dāng)多次序列化一個對象時,是會序列化多次還是會序列化一次呢?
public class SerialTest {
public static void main(String[] args) {
Test test = new Test();
test.setName("huihui");
// 序列化,存儲對象到文本
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("test"));
// 兩次寫入文件
oos.writeObject(test);
oos.flush();
System.out.println(new File("test").length());
oos.writeObject(test);
oos.flush();
System.out.println(new File("test").length());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (oos != null) {
oos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化,從文本中取出對象
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("test"));
// 讀取兩個對象
Test test1 = (Test) ois.readObject();
Test test2 = (Test) ois.readObject();
System.out.println(test1 == test1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (ois != null) {
ois.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class Test implements Serializable {
private static final long serialVersionUID = 4335715933640891747L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
'}';
}
}
運行結(jié)果:
73
78
true
可以發(fā)現(xiàn),當(dāng)?shù)诙螌懭雽ο髸r,文件的長度僅僅增加了5個字節(jié),并且在判等時,兩個引用指向同一地址。
Java序列化機制為了節(jié)省磁盤空間,具有特定的存儲規(guī)則,當(dāng)寫入文件為同一對象時,并不會再將對象的內(nèi)容進(jìn)行存儲,而只是再次存儲一份引用。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
相關(guān)文章
Java高并發(fā)編程之CAS實現(xiàn)無鎖隊列代碼實例
這篇文章主要介紹了Java高并發(fā)編程之CAS實現(xiàn)無鎖隊列代碼實例,在多線程操作中,我們通常會添加鎖來保證線程的安全,那么這樣勢必會影響程序的性能,那么為了解決這一問題,于是就有了在無鎖操作的情況下依然能夠保證線程的安全,需要的朋友可以參考下2023-12-12
Spring Security如何優(yōu)雅的增加OAuth2協(xié)議授權(quán)模式
這篇文章主要介紹了Spring Security如何優(yōu)雅的增加OAuth2協(xié)議授權(quán)模式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
SpringApplicationRunListener監(jiān)聽器源碼詳解
這篇文章主要介紹了SpringApplicationRunListener監(jiān)聽器源碼詳解,springboot提供了兩個類SpringApplicationRunListeners、SpringApplicationRunListener(EventPublishingRunListener),spring框架還提供了一個ApplicationListener接口,需要的朋友可以參考下2023-11-11
springboot之security?FilterSecurityInterceptor的使用要點記錄
這篇文章主要介紹了springboot之security?FilterSecurityInterceptor的使用要點記錄,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12
apollo更改配置刷新@ConfigurationProperties配置類
這篇文章主要為大家介紹了apollo更改配置刷新@ConfigurationProperties配置類示例解析,apollo更改配置刷新@ConfigurationProperties配置類2023-04-04
詳解關(guān)于springboot-actuator監(jiān)控的401無權(quán)限訪問
本篇文章主要介紹了詳解關(guān)于springboot-actuator監(jiān)控的401無權(quán)限訪問,非常具有實用價值,有興趣的可以了解一下2017-09-09
SpringBoot+SseEmitter和Vue3+EventSource實現(xiàn)實時數(shù)據(jù)推送
本文主要介紹了SpringBoot+SseEmitter和Vue3+EventSource實現(xiàn)實時數(shù)據(jù)推送,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03
java中 String和StringBuffer的區(qū)別實例詳解
這篇文章主要介紹了java中 String和StringBuffer的區(qū)別實例詳解的相關(guān)資料,一個小的例子,來測試String和StringBuffer在時間和空間使用上的差別,需要的朋友可以參考下2017-04-04

