含義、意義使用場(chǎng)景
序列化:將對(duì)象寫入到IO流中
反序列化:從IO流中恢復(fù)對(duì)象
意義:序列化機(jī)制允許將實(shí)現(xiàn)序列化的Java對(duì)象轉(zhuǎn)換位字節(jié)序列,這些字節(jié)序列可以保存在磁盤上,或通過網(wǎng)絡(luò)傳輸,以達(dá)到以后恢復(fù)成原來的對(duì)象。序列化機(jī)制使得對(duì)象可以脫離程序的運(yùn)行而獨(dú)立存在。
使用場(chǎng)景:所有可在網(wǎng)絡(luò)上傳輸?shù)膶?duì)象都必須是可序列化的,比如RMI(remote method invoke,即遠(yuǎn)程方法調(diào)用),傳入的參數(shù)或返回的對(duì)象都是可序列化的,否則會(huì)出錯(cuò);所有需要保存到磁盤的java對(duì)象都必須是可序列化的。通常建議:程序創(chuàng)建的每個(gè)JavaBean類都實(shí)現(xiàn)Serializeable接口。
序列化的實(shí)現(xiàn)方式
如果需要將某個(gè)對(duì)象保存到磁盤上或者通過網(wǎng)絡(luò)傳輸,那么這個(gè)類應(yīng)該實(shí)現(xiàn)Serializable接口或者Externalizable接口之一。
Serializable
普通序列化
Serializable接口是一個(gè)標(biāo)記接口,不用實(shí)現(xiàn)任何方法。一旦實(shí)現(xiàn)了此接口,該類的對(duì)象就是可序列化的。
創(chuàng)建對(duì)象類
public class Person implements Serializable {
private String name;
private int age;
//我不提供無參構(gòu)造器
public Person(String name, int age) {
System.out.println("調(diào)用有參構(gòu)造方法");
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}
序列化步驟
步驟一:創(chuàng)建一個(gè)ObjectOutputStream輸出流;
步驟二:調(diào)用ObjectOutputStream對(duì)象的writeObject輸出可序列化對(duì)象
public class WriteObject {
public static void main(String[] args) {
try {
//創(chuàng)建一個(gè)ObjectOutputStream輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
//將對(duì)象序列化到文件s
Person person = new Person("9龍", 23);
oos.writeObject(person);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 輸出結(jié)果:
調(diào)用有參構(gòu)造方法
反序列化步驟
步驟一:創(chuàng)建一個(gè)ObjectInputStream輸入流;
步驟二:調(diào)用ObjectInputStream對(duì)象的readObject()得到序列化的對(duì)象。
我們將上面序列化到person.txt的person對(duì)象反序列化回來
public class WriteObject {
public static void main(String[] args) {
try {
//創(chuàng)建一個(gè)ObjectOutputStream輸出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"));
Person brady = (Person) ois.readObject();
System.out.println(brady);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//輸出結(jié)果:
Person{name='9龍', age=23}
輸出告訴我們,反序列化并不會(huì)調(diào)用構(gòu)造方法。反序列的對(duì)象是由JVM自己生成的對(duì)象,不通過構(gòu)造方法生成。
成員為引用的序列化
如果一個(gè)可序列化的類的成員不是基本類型,也不是String類型,那這個(gè)引用類型也必須是可序列化的;否則,會(huì)導(dǎo)致此類不能序列化。
看例子,我們新增一個(gè)Teacher類。將Person去掉實(shí)現(xiàn)Serializable接口代碼
public class Person {
// 省略。與上邊代碼相同
}
public class Teacher implements Serializable {
private String leval;
private Person person;
public Teacher(String leval, Person person) {
this.leval = leval;
this.person = person;
}
public static void main(String[] args) throws Exception {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("teacher.txt"))) {
Person person = new Person("路飛", 20);
Teacher teacher = new Teacher("雷利", person);
oos.writeObject(teacher);
}
}
}
控制臺(tái)輸出:
調(diào)用有參構(gòu)造方法
Exception in thread "main" java.io.NotSerializableException: com.jelly.java.serialize.Person
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.jelly.java.serialize.Teacher.main(Teacher.java:20)
因?yàn)镻erson類的對(duì)象是不可序列化的,這導(dǎo)致了Teacher的對(duì)象不可序列化
對(duì)同一個(gè)對(duì)象序列化多次
public static void writeMutObject() {
try {
//創(chuàng)建一個(gè)ObjectOutputStream輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
//將對(duì)象序列化到文件s
Person p1 = new Person("1龍", 23);
Person p2 = new Person("9龍", 30);
oos.writeObject(p1);
oos.writeObject(p2);
oos.writeObject(p2);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void readMutObject() {
try {
//創(chuàng)建一個(gè)ObjectOutputStream輸出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
Person p3 = (Person) ois.readObject();
System.out.println(p1);
System.out.println(p2);
System.out.println(p3);
System.out.println("p1==p2? :" + p1.equals(p2));
System.out.println("p2==p3? :" + p2.equals(p3));
System.out.println("p1==p3? :" + p1.equals(p3));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// writeMutObject();
readMutObject();
}
輸出
Person{name='1龍', age=23}
Person{name='9龍', age=30}
Person{name='9龍', age=30}
p1==p2? :false
p2==p3? :true
p1==p3? :false
從輸出結(jié)果可以看出,Java序列化同一對(duì)象,并不會(huì)將此對(duì)象序列化多次得到多個(gè)對(duì)象。
序列化算法
- 所有保存到磁盤的對(duì)象都有一個(gè)序列化編碼號(hào)
- 當(dāng)程序試圖序列化一個(gè)對(duì)象時(shí),會(huì)先檢查此對(duì)象是否已經(jīng)序列化過,只有此對(duì)象從未(在此虛擬機(jī))被序列化過,才會(huì)將此對(duì)象序列化為字節(jié)序列。
- 如果此對(duì)象已經(jīng)序列化過,則直接存儲(chǔ)對(duì)應(yīng)的編號(hào)即可。
序列化算法潛在問題
由于java序利化算法不會(huì)重復(fù)序列化同一個(gè)對(duì)象,只會(huì)記錄已序列化對(duì)象的編號(hào)。如果序列化一個(gè)可變對(duì)象(對(duì)象內(nèi)的內(nèi)容可更改)后,更改了對(duì)象內(nèi)容,再次序列化,并不會(huì)再次將此對(duì)象轉(zhuǎn)換為字節(jié)序列,而只是保存序列化編號(hào),那么就會(huì)造成數(shù)據(jù)丟失。
public static void execp() {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.txt"));
//將對(duì)象序列化到文件s
Person p1 = new Person("1龍", 23);
oos.writeObject(p1);
p1.setName("9龍");
oos.writeObject(p1);
oos.close();
oos.flush();
創(chuàng)建一個(gè)ObjectOutputStream輸出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.txt"));
Person pp1 = (Person) ois.readObject();
Person pp2 = (Person) ois.readObject();
System.out.println(pp1);
System.out.println(pp2);
} catch (Exception e) {
System.out.println("exception " + e);
}
}
public static void main(String[] args) {
execp();
}
// 輸出:
調(diào)用有參構(gòu)造方法
Person{name='1龍', age=23}
Person{name='1龍', age=23}
定制序列化方式
序列化時(shí)忽略字段 (transient)
有些時(shí)候,我們有這樣的需求,某些屬性不需要序列化。使用transient關(guān)鍵字選擇不需要序列化的字段。
使用transient修飾的屬性,java序列化時(shí),會(huì)忽略掉此字段,所以反序列化出的對(duì)象,被transient修飾的屬性是默認(rèn)值。對(duì)于引用類型,值是null;基本類型,值是0;boolean類型,值是false。
自定義序列化方法
使用transient雖然簡(jiǎn)單,但將此屬性完全隔離在了序列化之外。java提供了可選的??梢赃M(jìn)行控制序列化的方式,或者對(duì)序列化數(shù)據(jù)進(jìn)行編碼加密等。
private void writeObject (java.io.ObjectOutputStream out) throws IOException;
private void readObject (java.io.ObjectIutputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData () throws ObjectStreamException;
通過重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化,哪些屬性不需要。如果writeObject使用某種規(guī)則序列化,則相應(yīng)的readObject需要相反的規(guī)則反序列化,以便能正確反序列化出對(duì)象。
這里展示對(duì)名字進(jìn)行反轉(zhuǎn)加密。
public class Person implements Serializable {
private String name;
private int age;
//將名字反轉(zhuǎn)寫入二進(jìn)制流
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(new StringBuffer(this.name).reverse());
out.writeInt(age);
}
//將讀出的字符串反轉(zhuǎn)恢復(fù)回來
private void readObject(ObjectInputStream ins) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) ins.readObject()).reverse().toString();
this.age = ins.readInt();
}
}
當(dāng)序列化流不完整時(shí),readObjectNoData()方法可以用來正確地初始化反序列化的對(duì)象。例如,使用不同類接收反序列化對(duì)象,或者序列化流被篡改時(shí),系統(tǒng)都會(huì)調(diào)用readObjectNoData()方法來初始化反序列化的對(duì)象。
Externalizable
通過實(shí)現(xiàn)Externalizable接口,必須實(shí)現(xiàn)writeExternal、readExternal方法。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
public class ExPerson implements Externalizable {
private String name;
private int age;
//注意,必須加上pulic 無參構(gòu)造器
public ExPerson() {
}
public ExPerson(String name, int age) {
this.name = name;
this.age = age;
}
//將name反轉(zhuǎn)后寫入二進(jìn)制流
@Override
public void writeExternal(ObjectOutput out) throws IOException {
StringBuffer reverse = new StringBuffer(name).reverse();
System.out.println(reverse.toString());
out.writeObject(reverse);
out.writeInt(age);
}
//將讀取的字符串反轉(zhuǎn)后賦值給name實(shí)例變量
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.name = ((StringBuffer) in.readObject()).reverse().toString();
System.out.println(name);
this.age = in.readInt();
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ExPerson.txt")); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ExPerson.txt"))) {
oos.writeObject(new ExPerson("brady", 23));
ExPerson ep = (ExPerson) ois.readObject();
System.out.println(ep);
}
}
}
//輸出結(jié)果
ydarb
brady
ExPerson{name='brady', age=23}
注意:Externalizable接口不同于Serializable接口,實(shí)現(xiàn)此接口必須實(shí)現(xiàn)接口中的兩個(gè)方法實(shí)現(xiàn)自定義序列化,這是強(qiáng)制性的;特別之處是必須提供pulic的無參構(gòu)造器,因?yàn)樵诜葱蛄谢臅r(shí)候需要反射創(chuàng)建對(duì)象。
二者對(duì)比
| 實(shí)現(xiàn)Serializable接口 | 實(shí)現(xiàn)Externalizable接口 |
|---|---|
| 系統(tǒng)自動(dòng)存儲(chǔ)必要的信息 | 程序員決定存儲(chǔ)哪些信息 |
| Java內(nèi)建支持,易于實(shí)現(xiàn),只需要實(shí)現(xiàn)該接口即可,無需任何代碼支持 | 必須實(shí)現(xiàn)接口內(nèi)的兩個(gè)方法 |
| 性能略差 | 性能略好 |
序列化版本號(hào)serialVersionUID
我們知道,反序列化必須擁有class文件,但隨著項(xiàng)目的升級(jí),class文件也會(huì)升級(jí),序列化怎么保證升級(jí)前后的兼容性呢?
java序列化提供了一個(gè)
private static final long serialVersionUID = -81298930239;
的序列化版本號(hào),只有版本號(hào)相同,即使更改了序列化屬性,對(duì)象也可以正確被反序列化回來。
如果反序列化使用的class的版本號(hào)與序列化時(shí)使用的不一致,反序列化會(huì)報(bào)InvalidClassException異常。
序列化版本號(hào)指定
可以自由指定,如果不指定JVM會(huì)根據(jù)類信息自己計(jì)算一個(gè)版本號(hào),這樣隨著class的升級(jí),就無法正確反序列化;不指定版本號(hào)另一個(gè)明顯隱患是,不利于jvm間的移植,可能class文件沒有更改,但不同jvm可能計(jì)算的規(guī)則不一樣,這樣也會(huì)導(dǎo)致無法反序列化。
什么情況下需要修改serialVersionUID呢?
分三種情況。
- 如果只是修改了方法,反序列化不影響,則無需修改版本號(hào);
- 如果只是修改了靜態(tài)變量,瞬態(tài)變量(transient修飾的變量),反序列化不受影響,無需修改版本號(hào);
- 如果修改了非瞬態(tài)變量,則可能導(dǎo)致反序列化失敗。如果新類中實(shí)例變量的類型與序列化時(shí)類的類型不一致,則會(huì)反序列化失敗,這時(shí)候需要更改serialVersionUID。如果只是新增了實(shí)例變量,則反序列化回來新增的是默認(rèn)值;如果減少了實(shí)例變量,反序列化時(shí)會(huì)忽略掉減少的實(shí)例變量。
總結(jié)
- 所有需要網(wǎng)絡(luò)傳輸?shù)膶?duì)象都需要實(shí)現(xiàn)序列化接口,通過建議所有的javaBean都實(shí)現(xiàn)Serializable接口。
- 對(duì)象的類名、實(shí)例變量(包括基本類型,數(shù)組,對(duì)其他對(duì)象的引用)都會(huì)被序列化;方法、類變量、transient實(shí)例變量都不會(huì)被序列化。
- 如果想讓某個(gè)變量不被序列化,使用transient修飾。
- 序列化對(duì)象的引用類型成員變量,也必須是可序列化的,否則,會(huì)報(bào)錯(cuò)。
- 反序列化時(shí)必須有序列化對(duì)象的class文件。
- 當(dāng)通過文件、網(wǎng)絡(luò)來讀取序列化后的對(duì)象時(shí),必須按照實(shí)際寫入的順序讀取。
- 單例類序列化,需要重寫readResolve()方法;否則會(huì)破壞單例原則。
- 同一對(duì)象序列化多次,只有第一次序列化為二進(jìn)制流,以后都只是保存序列化編號(hào),不會(huì)重復(fù)序列化。
- 建議所有可序列化的類加上serialVersionUID 版本號(hào),方便項(xiàng)目升級(jí)。