我們都知道,,Java序列化可以讓我們記錄下運(yùn)行時的對象狀態(tài)(對象實(shí)例域的值),,也就是我們經(jīng)常說的對象持久化
。這個過程其實(shí)是非常復(fù)雜的,,這里我們就好好理解一下Java的對象序列化,。
1、
首先我們要搞清楚,,Java對象序列化是將
對象的實(shí)例域數(shù)據(jù)(
包括private私有域)
進(jìn)行持久化存儲,。而并非是將整個對象所屬的類信息進(jìn)行存儲。
其實(shí)了解JVM的話,,我們就能明白這一點(diǎn)了,。實(shí)際上堆中所存儲的對象包含了實(shí)例域數(shù)據(jù)值以及指向類信息的地址,而對象所屬的類信息卻存放在方法區(qū)中,。當(dāng)我們要對持久層數(shù)據(jù)反序列化成對象的時候,,也就只需要將實(shí)例域數(shù)據(jù)值存放在新創(chuàng)建的對象中即可。
2,、
我們都知道凡要序列化的類都必須實(shí)現(xiàn)Serializable接口,。
但是不是所有類都可以序列化呢?當(dāng)然不是這樣,,想想看序列化可以讓我們輕而易舉的接觸到對象的私有數(shù)據(jù)域,,這是多么危險(xiǎn)的漏洞呀,!總結(jié)一下,JDK中有四種類型的類對象是絕對不能序列化的
,。
(1) 太依賴于底層實(shí)現(xiàn)的類(too closely tied to native code),。比如java.util.zip.Deflater。
(2) 對象的狀態(tài)依賴于虛擬機(jī)內(nèi)部和不停變化的運(yùn)行時環(huán)境,。比如java.lang.Thread, java.io.InputStream
(3) 涉及到潛在的安全性問題,。比如:java.lang.SecurityManager, java.security.MessageDigest
(4) 全是靜態(tài)域的類,沒有對象實(shí)例數(shù)據(jù),。要知道靜態(tài)域本身也是存儲在方法區(qū)中的,。
3、
自定義的類只要實(shí)現(xiàn)了Serializable接口,,是不是都可以序列化呢,?
當(dāng)然也不是這樣,看看下面的例子:
- class Employee implements Serializable{
- private ZipFile zf=null;
- Employee(ZipFile zf){
- this.zf=zf;
- }
- }
-
- ObjectOutputStream oout=
- new ObjectOutputStream(new FileInputStream(new File("aaa.txt")));
- oout.writeObject(new Employee(new ZipFile("c://.."));
我們會發(fā)現(xiàn)運(yùn)行之后拋出java.io.NotSerializableException
: java.util.zip.ZipFile
,。很明顯,,如果要對Employee對象序列化,就必須對其數(shù)據(jù)域ZipFile對象也進(jìn)行序列化,,而這個類在JDK中是不可序列化的,。因此,包含了不可序列化的對象域的對象也是不能序列化的,。
實(shí)際上,,這也并非不可能,我們在下面第6點(diǎn)會談到,。
4,、
可序列化的類成功序列化之后,是不是一定可以反序列化呢,?
(這里默認(rèn)在同一環(huán)境下,,而且類定義永遠(yuǎn)不會改變,即滿足兼容性,。在下面我們會討論序列化的不兼容性),。答案是不一定哦!我們還是看一個列子:
- //父類對象不能序列化
- class Employee{
- private String name;
- Employee(String n){
- this.name=n;
- }
- public String getName(){
- return this.name;
- }
- }
- //子類對象可以序列化
- class Manager extends Employee implements Serializable{
- private int id;
- Manager(String name, int id){
- super(name);
- this.id=id;
- }
- }
- //序列化與反序列化測試
- public static void main(String[] args) throws IOException, ClassNotFoundException{
- File file=new File("E:/aaa.txt");
- ObjectOutputStream oout=new ObjectOutputStream(new FileOutputStream(file));
- oout.writeObject(new Manager("amao",123));
- oout.close();
- System.out.println("序列化成功");
-
- ObjectInputStream oin=new ObjectInputStream(new FileInputStream(file));
- Object o=oin.readObject();
- oin.close();
- System.out.println("反序列化成功:"+((Manager) o).getName());
- }
程序的運(yùn)行結(jié)果是:打印出“序列化成功”之后拋出java.io.InvalidClassException:
Manager; Manager; no valid constructor,。
為什么會出現(xiàn)這種情況呢,?很顯然,序列化的時候只是將Manager類對象的數(shù)據(jù)域id寫入了文件,,但在反序列化的過程中,需要在堆中建立一個Manager新對象,。我們都知道任何一個類對象的建立都首先需要調(diào)用父類的構(gòu)造器對父類進(jìn)行初始化,,很可惜序列化文件中并沒有父類Employee的name數(shù)據(jù),,那么此時調(diào)用Employee(String)構(gòu)造器會因?yàn)闆]有數(shù)據(jù)而出現(xiàn)異常。既然沒有數(shù)據(jù),,那么可不可以調(diào)用無參構(gòu)造器呢,? 事實(shí)卻是如此,如果有Employee()無參構(gòu)造器的存在,,將不會拋出異常,,只是在執(zhí)行打印的時候出現(xiàn)--- “反序列化成功:null”。
總結(jié)一下:如果當(dāng)前類的所有超類中有一個類即不能序列化,,也沒有無參構(gòu)造器,。那么當(dāng)前類將不能反序列化。如果有無參構(gòu)造器,,那么此超類反序列化的數(shù)據(jù)域?qū)莕ull或者0,,false等等。
5,、
序列化的兼容性問題,!
類定義很有可能在不停的人為更新(比如JDK1.1到JDK1.2中HashTable的改變)。那么以前序列化的舊類對象很可能不能再反序列化成為新類對象,。這就是序列化的兼容性問題,,嚴(yán)格意義上來說改變類中除static
和transient以外的所有部分都會造成兼容性問題。而JDK采用了一種stream unique identifier (SUID)
來識別兼容性,。SUID是通過復(fù)雜的函數(shù)來計(jì)算的類名,,接口名,方法和數(shù)據(jù)域的
一個64位
hash值,。而這個值存儲在類中的靜態(tài)域內(nèi):
private static final long serialVersionUID = 3487495895819393L
只要稍微改動類的定義,,這個類的SUID就會發(fā)生變化,我們通過下面的程序來看看:
- //修改前的Employee
- class Employee implements Serializable{
- private String name;
- Employee(String n){
- this.name=n;
- }
- public String getName(){
- return this.name;
- }
- }
- //測試,,打印SUID=5135178525467874279L
- long serialVersionUID=ObjectStreamClass.lookup(Class.forName("Employee")).getSerialVersionUID();
- System.out.println(serialVersionUID);
-
- //修改后的Employee
- class Employee implements Serializable{
- private String name1; //注意,,這里略微改動一下數(shù)據(jù)域的名字
- Employee(String n){
- this.name1=n;
- }
- public String getName(){
- return this.name1;
- }
- }
- //測試,打印SUID=-2226350316230217613L
- long serialVersionUID=ObjectStreamClass.lookup(Class.forName("Employee")).getSerialVersionUID();
- System.out.println(serialVersionUID);
兩次測試的SUID都不一樣,,不過你可以試試如果name域是static或transient聲明的,,那么改變這個域名是不會影響SUID的。
很顯然,,JVM正是通過檢測新舊類SUID的不同,,來檢測出序列化對象與反序列化對象的不兼容。拋出 java.io.InvalidClassException:
Employee; local class incompatible:
很多時候,,類定義的改變勢在必行,,但又不希望出現(xiàn)序列化的不兼容性。我們就可以通過在類中顯示的定義serialVersionUID,并賦予一個明確的long值即可,。這樣會逃過JVM的默認(rèn)兼容性檢查,。但是如果數(shù)據(jù)域名的改變會導(dǎo)致反序列化后,改變的數(shù)據(jù)域只能得到默認(rèn)的null或者0或者false值,。
6,、
在上面第3點(diǎn)中談到了一個不能成功序列化的Employee的列子,原因就是包含了一個不能序列化的ZipFile對象引用的數(shù)據(jù)域,。但有時我們非常想將ZipFile所對應(yīng)的本地文件路徑進(jìn)行序列化,,是不是真的沒有辦法了呢?
這里我們就將一個非常有用的應(yīng)用,。
當(dāng)我們需要用writeObject(Object)方法對某個類對象序列化的時候,,會首先對這個類對象的所有超類按照繼承層次從高到低來寫出每個超類的數(shù)據(jù)域。誰能保證每個超類都實(shí)現(xiàn)了Serializable接口呢,? 其實(shí),,對于這些不能序列化的類,JVM會檢查這些類是否有這樣一個方法:
private void writeObject(ObjectOutputStream out)throws IOException
如果有,,JVM會調(diào)用這個方法仍然對該類的數(shù)據(jù)域進(jìn)行序列化,。我們來看看JDK的ObjectOutputStream類中對這一部分的實(shí)現(xiàn)(我這里只列出了源碼中的執(zhí)行過程):
- //下面的方法從上到下進(jìn)行調(diào)用
- writeObject(Object);
-
- //ObjectOutputStream的writeObject方法
- public final void writeObject(Object obj) throws IOException {
- writeObject0(obj, false);
- }
-
- //ObjectOutputStream, 底層寫入Object的實(shí)現(xiàn)
- private void writeObject0(Object obj, boolean unshared) {
- if (obj instanceof Serializable) {
- writeOrdinaryObject(obj, desc, unshared);
- }
-
- //ObjectOutputStream
- private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) {
- writeSerialData(obj, desc);
- }
-
- //ObjectOutputStream, 對超類到子類的每個可序列化的類,寫出數(shù)據(jù)域
- private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException{
- //如果類中有writeObject(ObjectOutputStream)方法,,則通過底層進(jìn)行調(diào)用
- if (slotDesc.hasWriteObjectMethod()) {
- slotDesc.invokeWriteObject(obj, this);
- }//如果沒有此方法,,則采用默認(rèn)的寫類數(shù)據(jù)域的方法。
- else {//這個方法會對可序列化的對象中的數(shù)據(jù)域進(jìn)行寫出,,但是如果這個數(shù)據(jù)域是不可序列化而且沒有writeObject(ObjectOutputStream)方法的類對象,,那么將拋出異常。
- defaultWriteFields(obj, slotDesc);
- }
- }
ObjectOutputStream中的writeSerialData()方法說明了JVM檢查writeObject(ObjectOutputStream out)
這個私有方法的潛在執(zhí)行機(jī)制,。這就是說,,我們可以通過構(gòu)造這個方法,使得原本不能序列化的類的部分?jǐn)?shù)據(jù)域可以序列化,。下面我們就開始對ZipFile進(jìn)行可序列化的改造吧,!
- //自定義的一個可序列化的ZipFile,當(dāng)然這個類不能繼承JDK中的ZipFile,,否則序列化將不可能完成,。
- class SerializableZipFile implements Serializable{
- public ZipFile zf;
- //包含一個ZipFile對象
- SerializableZipFile(String filename) throws IOException{
- zf=new ZipFile(filename);
- }
- //對ZipFile中的文件名進(jìn)行序列化,因?yàn)樗荢tring類型的
- private void writeObject(ObjectOutputStream out)throws IOException{
- out.writeObject(zf.getName());
- }
- //對應(yīng)的,,反序列化過程中JVM也會檢查類似的一個私有方法,。
- private void readObject(ObjectInputStream in)throws IOException,ClassNotFoundException{
- String filename=(String)in.readObject();
- zf=new ZipFile(filename);
- }
- }
- //測試
- public static void main(String[] args) throws IOException, ClassNotFoundException{
- //序列化
- File file=new File("E:/aaa.txt");
- ObjectOutputStream oout=new ObjectOutputStream(new FileOutputStream(file));
- oout.writeObject(new SerializableZipFile("e:/aaa.zip"));
- oout.close();
- System.out.println("序列化成功");
- //反序列化
- ObjectInputStream oin=new ObjectInputStream(new FileInputStream(file));
- Object o=oin.readObject();
- oin.close();
- System.out.println("反序列化成功:"+((SerializableZipFile) o).zf.getName());
- }
- //序列化成功
- //反序列化成功:e:\aaa.zip