Java序列化機制
序列化和反序列化
Java序列化是Java內(nèi)建的數(shù)據(jù)(對象)持久化機制很洋,通過序列化可以將運行時的對象數(shù)據(jù)按照特殊的格式存儲到文件中溶耘,以便在將來重建對象。
Java序列化是Java IO的一部分,需要ObjectStream提供支持。支撐序列化機制的基礎(chǔ)是運行期對對象的信息(如類信息)和數(shù)據(jù)(如成員域)的讀取和存儲肋坚,從而將數(shù)據(jù)的字節(jié)流寫入文件或字節(jié)數(shù)組中。反序列化過程則是讀取字節(jié)流中對象的描述信息和數(shù)據(jù)肃廓,通過指定類的構(gòu)造器和持久化的數(shù)據(jù)動態(tài)地重建出對象。
當然诲泌,對于一般對象而已盲赊,重建后的對象和被序列化到文件中的對象,不會是同一個了(地址)敷扫,因為可能都不在同一臺計算機中哀蘑,序列化中的“內(nèi)存地址”這個對象相等的標志,變成了對象的序列號葵第,這也是序列化的名稱由來绘迁。不過對于枚舉對象,因為在反序列化過程做了特殊的處理卒密,保證了對象的唯一性缀台。
需要注意的是,序列化是保存對象的狀態(tài)哮奇,和類狀態(tài)無關(guān)膛腐,所以它不會讀取靜態(tài)數(shù)據(jù)。
序列化和反序列化的序列號
序列號是關(guān)聯(lián)到對象的鼎俘,是內(nèi)部流處理的機制哲身,外部是不可見的。
- 序列化
- 序列化的每個對象對應(yīng)一個序列號贸伐,這個序列號是唯一對應(yīng)一個對象的勘天,相當于對象內(nèi)存地址的作用;
- 對于每個首次遇到的對象捉邢,會將其數(shù)據(jù)存儲到輸出流中脯丝;
- 對于相同的對象(這個相同的判斷是通過地址),會引用之前的對象的序列號歌逢,相當于引用一個地址巾钉;
- 反序列化(這個過程序列號就是對象的唯一標識符了)
- 對于首次遇到的序列號,會根據(jù)流中的信息構(gòu)建一個Java對象秘案;
- 對于與之前的序列號相同的序列號砰苍,直接引用之前構(gòu)建的對象(這里是對象的內(nèi)存地址)
序列化的版本號
序列化的版本號是在類中定義的private static final long serialVersionUID
潦匈,這個字段用來標示類的版本,它是數(shù)據(jù)域類型和方法簽名信息通過SHA算法取到的指紋赚导,采用了SHA碼的前8個字節(jié)茬缩,主要用在反序列化過程中的類的校驗。如果在反序列化過程中吼旧,當前類的serialVersionUID和序列化文件中的serialVersionUID不一致凰锡,那么就無法從流中構(gòu)建對象,并拋出異常圈暗。
那么什么時候這個serialVersionUID會變呢掂为?
主要有下面兩種情況
- 在實現(xiàn)Serializable接口時,沒有定義serialVersionUID屬性员串,但在序列化之后勇哗,修改了類結(jié)構(gòu);
- 手動修改了serialVersionUID的值寸齐;
serialVersionUID是用來應(yīng)對類在序列化之后類發(fā)生了變更的情況欲诺。對于第一個情況,在定義可序列化類時沒有定義serialVersionUID渺鹦,不會影響序列化過程扰法,因為在序列化過程中會自動生成一個寫入到流中,如果在之后沒有修改類的任何域或方法毅厚,反序列化是沒有問題的塞颁,(因為生成指紋的基礎(chǔ)沒變,自動生成的指紋即serialVersionUID是一致的)卧斟。但是如果在序列化之后修改了類殴边,反序列化就會失敗,除非手動加上serialVersionUID值和前面自動生成的值一致珍语。查看舊值的方法是:使用jdk的命令serialver <類名>
锤岸,能拿到他早期版本的版本號。
對于第二種情況板乙,一般而言是偷,除非是有此類需求,不然不會手動去破壞反序列化募逞。
相關(guān)的類蛋铆、接口
序列化標識接口
- java.io.Externalizable
- java.io.Serializable
序列化機制實現(xiàn)類
- java.io.ObjectInputStream
- java.io.ObjectOutputStream
實現(xiàn)Serializable接口或者Externalizable接口的才可以被序列化。Serializable接口沒有任何方法放接,是一個標記接口刺啦,Externalizable接口是Serializable接口的子接口,擁有兩個公共方法纠脾,用來自定義序列化過程玛瘸。
Serializable和Externalizable的區(qū)別
實現(xiàn)了Externalizable接口的類蜕青,需要實現(xiàn)接口的兩個抽象方法,readExternal和writeExternal糊渊,需要讀寫哪些數(shù)據(jù)都需要顯式的調(diào)用流的讀寫方法右核,也就是序列化的任務(wù)從Java內(nèi)建轉(zhuǎn)移到了開發(fā)者,提供了更大自由度的同時渺绒,也提高了復(fù)雜度贺喝。而且在Externalizable反序列化時,會調(diào)用類的public無參構(gòu)造器宗兼,如果類中沒有定義無參構(gòu)造器(沒有重載構(gòu)造器的話躏鱼,會有默認的無參構(gòu)造器的),會拋出異常殷绍。后面再詳細說明此接口的使用挠他。
對象流是序列化的核心,readObject和writeObject方法從文件中反序列化對象和將對象序列化到文件中篡帕。
使用默認的序列化
以下是實現(xiàn)了Serializable接口的類,main方法演示了對象的序列化和反序列化的使用贸呢。
public class SerializableUser implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private String name;
private int age;
private SerializableUser(String name, int age) {
this.name = name;
this.age = age;
}
public SerializableUser() {
}
@Override
public String toString() {
return "User -> name:" + name + " ,age=" + age;
}
public static void main(String[] args) throws IOException {
SerializableUser user = new SerializableUser("harry", 19);
SerialUtil.writeToFile("ser", user);
SerializableUser u = (SerializableUser) SerialUtil.readFromFile("ser");
System.out.println(u);
// Files.deleteIfExists(Paths.get("ser"));
}
}
// 工具類
public class SerialUtil {
public static void writeToFile(String path, Object obj) {
ObjectOutputStream oos;
try {
oos = new ObjectOutputStream(new FileOutputStream(path));
oos.writeObject(obj);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Object readFromFile(String path) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ois != null)
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
結(jié)果:
User -> name:harry ,age=19
序列化文件內(nèi)容:
aced 0005 7372 0026 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 5365
7269 616c 697a 6162 6c65 5573 6572 0000
0000 0000 0001 0200 0249 0003 6167 654c
0004 6e61 6d65 7400 124c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b78 7000 0000
1374 0005 6861 7272 79
自定義序列化
想要修改Serializable接口實現(xiàn)類的序列化邏輯镰烧,比如有些字段不需要寫入,或者自己想控制寫入的過程楞陷,如想給寫入文件的字節(jié)流加密等等怔鳖,就不能直接使用對象流提供的read和write方法了。
transient關(guān)鍵字
使用transient關(guān)鍵字修飾的字段固蛾,在序列化時會被忽略结执,不會寫入流中。transient意思是瞬時的艾凯,既然不是長久的献幔,就代表不要持久化。
重寫writeObject()和readObject()
在實現(xiàn)類中重寫writeObject和readObject方法可以改變序列化的方法調(diào)用趾诗,會調(diào)用類中重寫的方法蜡感,而不會再走對象流的方法,實現(xiàn)邏輯是在對象流的writeObject和readObject中通過反射判斷類中是否重寫了方法恃泪,然后反射調(diào)用郑兴。
一般重寫的邏輯是先調(diào)用流的defaultWriteObject和defaultReadObject方法,然后追加自己的寫入邏輯贝乎。這兩個default方法是原本對象流的writeObject和readObject方法會調(diào)用的情连,所以具備默認的讀寫作用±佬В可以參考ArrayList類的設(shè)計却舀,ArrayList內(nèi)部使用數(shù)組來存儲元素虫几,且數(shù)組是會動態(tài)擴容的,如果直接序列化數(shù)組禁筏,會序列化很多的空元素即null到流中持钉,既浪費空間也降低了效率,所以在類中將數(shù)組變量標注成transient篱昔,然后在重寫writeObject和readObect方法每强,將實際的元素寫入對象流和從對象流中讀出。
readResolve()
這個方法是應(yīng)對enum出現(xiàn)之前設(shè)計的枚舉的代替代碼和單例代碼的措施州刽。
看一段示例:
public class SimulateEnum {
/**
* 模擬一個枚舉類空执,在Java內(nèi)建枚舉出現(xiàn)之前的情況
*/
static class MyEnum implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private static int order = 0;
private int ordernum;
private MyEnum(String name) {
this.name = name;
this.ordernum = order++;
}
public String getName() {
return name;
}
public int getOrdernum() {
return ordernum;
}
public static final MyEnum A = new MyEnum("A");
public static final MyEnum B = new MyEnum("B");
public static final MyEnum C = new MyEnum("C");
}
public static void main(String[] args) throws IOException {
MyEnum a = MyEnum.C;
System.out.println(a.getName());
System.out.println(a.getOrdernum());
SerialUtil.writeToFile("enum", a);
MyEnum rebuilda = (MyEnum) SerialUtil.readFromFile("enum");
System.out.println(rebuilda.getName());
System.out.println(rebuilda.getOrdernum());
System.out.println(a == rebuilda);
Files.deleteIfExists(Paths.get("enum"));
}
}
在示例中我們模擬了一個枚舉的實現(xiàn),我們定義了私有的構(gòu)造器和幾個作為枚舉對象的靜態(tài)對象穗椅,我們期待的是枚舉是安全的辨绊,不會再生成除了定義的三個變量之外的其他變量,但是反序列化過程會破壞這種設(shè)計匹表∶趴溃看結(jié)果:
C
2
C
2
false
因為對象的反序列化會創(chuàng)建新的對象的,即使類的構(gòu)造器是私有的袍镀,這會破壞單例模式的設(shè)計默蚌。為了去保護在Java枚舉出現(xiàn)之前的模擬枚舉,對象留提供了一個解決措施——在類中實現(xiàn)readResolve方法苇羡。在readResolve方法中攔截新對象的生成绸吸,使之返回已有的對象。
我們在MyEnum類中加入以下方法:
private Object readResolve() throws Exception {
switch (name) {
case "A":
return A;
case "B":
return B;
case "C":
return C;
default:
throw new Exception();
}
}
再執(zhí)行返回的結(jié)果就是true了设江。
那么為什么在反序列真正的枚舉的時候就不用再考慮在類中實現(xiàn)readReslove方法呢锦茁,原因是ObjectStream對枚舉做了支持〔娲妫看一下調(diào)用棧码俩。
readObejct()-> readObject0()-> checkResolve(readEnum(unshared))->readEnum():
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
看一下源碼,實際上和我們自己寫的readResolve里面的意思是差不多的歼捏。
Externalizable接口
一個實現(xiàn)Externalizable接口的類表明它是可被外部化的握玛,也是可序列化,只是這個序列化的責任轉(zhuǎn)移給了外部控制甫菠。這個接口的定義:
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
兩個抽象方法給開發(fā)者自行實現(xiàn)序列化的處理挠铲。
示例:
public class ExternalizableUser implements Externalizable {
private String name;
private int age;
private ExternalizableUser(String name, int age) {
this.name = name;
this.age = age;
}
public ExternalizableUser() {
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
@Override
public String toString() {
return "User -> name:" + name + " ,age=" + age;
}
public static void main(String[] args) throws IOException {
ExternalizableUser user = new ExternalizableUser("harry",19);
SerialUtil.writeToFile("ext", user);
ExternalizableUser u = (ExternalizableUser) SerialUtil.readFromFile("ext");
System.out.println(u);
Files.deleteIfExists(Paths.get("ext"));
}
}
//User -> name:harry ,age=19
如果上面示例中的writeExternal和readExternal方法沒有實現(xiàn),那么返回的對象是一個空的對象寂诱,數(shù)據(jù)是默認值拂苹。
序列化的文件內(nèi)容是:
aced 0005 7372 0028 6a64 6b2e 7465 7374
2e73 6572 6961 6c69 7a61 626c 652e 4578
7465 726e 616c 697a 6162 6c65 5573 6572
c8f9 50ef 76db 2f15 0c00 0078 7074 0005
6861 7272 7977 0400 0000 1378
感興趣的同學可以去研究一下序列化文件的格式。
The end