Java序列化

概念

? ? ? ?Java中號稱一切皆是對象,在Java程序運(yùn)行過程中,都是借助對象來完成一系列我們想要的操作。但是對象它是存儲在內(nèi)存中的,如果我們機(jī)器關(guān)機(jī)了,這些對象也就不存在了。序列化就是將這些在內(nèi)存中運(yùn)行的對象的狀態(tài)信息轉(zhuǎn)換為一種可以存儲到磁盤上的過程,或者將對象的信息狀態(tài)進(jìn)行傳輸?shù)倪^程。在序列化期間,我們可以將當(dāng)前對象的狀態(tài)寫入到一個臨時的存儲區(qū)或者持久化到硬盤中,這樣對象的信息就可以長久存儲于硬盤介質(zhì)中,或者可以在網(wǎng)絡(luò)間進(jìn)行傳輸。

? ? ? ?有正向就有反向,對象信息持久化的過程是序列化過程,那么逆向就是反序列化過程,也就是說可以通過讀取已經(jīng)持久化到硬盤上的對象信息,將其重新轉(zhuǎn)變成對象應(yīng)用于應(yīng)用程序中。序列化和反序列化應(yīng)用很廣泛,我們常說的遠(yuǎn)程調(diào)用,Hibernate的持久化等等,這些都需要用到序列化和反序列化。

怎樣使用序列化

Serializable接口

首先最常見就是這個序列化標(biāo)識接口,說它是標(biāo)識是因為它的接口源碼中其實什么都沒有定義,純粹只是為了標(biāo)識而使用,一個類只有實現(xiàn)了這個接口,我們才能對它進(jìn)行序列化,否則是會報錯的。序列化和反序列化的過程需要用到ObjectInputStream和ObjectOutputStream。

public class User implements Serializable{
 private String name;
 private int age;
 private Data birthday;
 private static final long serialVersionUID = 1L;
 //...省略getter和setter
 @Override
 public String toString() {
 return "User{" +
 "name='" + name + "'" + 
 "age=" + age +
 ",birthday=" + birthday;
 }
}

? ? ? ?這里面的serialVersionUID主要是用于反序列化的時候做匹配用的,只有serialVersionUID的值一致,最后反序列化的時候才認(rèn)為它們是同一種類型,如果不定義,程序會默認(rèn)生成一個1L。在反序列化的時候,就是依據(jù)這個值來跟當(dāng)前虛擬機(jī)中的類中定義的數(shù)值比對,如果一致,才會認(rèn)為是同一個版本,否則即使其他部分完全一樣,也無法順序反序列化。

? ? ? ?然后對User做一個簡單的序列化demo,代碼如下:

public class Demo1 {
 public static void main(String[] args) {
   User user = new User();
   user.setName("Jack");
   user.setAge(20);
   user.setBirthday(new Date());

 //將對象序列化并寫入文件中
   ObjectOutputStream oos = null;
   try {
     oos = new ObjectOutputStream(new FileInputStream("D:\\tempFile"));
     oos.writeObject(user);
   } catch (Exception e) {
     e.printStackTrace();
   } finally {
     if (oos != null) {
     try {
       oos.close();
     } catch (Exception ex) {
       ex.printStackTrace();
     }
   }
 }

 //讀取tempFile中的內(nèi)容,將其再次轉(zhuǎn)換為內(nèi)存中的對象(反序列化)
   File file = new File("D:\\tempFile");
   ObjectInputStream ois = null;
   try {
     ois = new ObjectInputStream(new FileInputStream(file));
     User newUser = (User)ois.readObject();
     System.out.println(newUser);
   } catch (Exception e) {
     e.printStackTrace();
   } finally {
   if (ois != null) {
   try {
     ois.close();
   } catch (Exception ex) {
     ex.printStackTrace();
   }
   }
 }
 }
}

? ? ? ?上面就是一個簡單的利用Java API進(jìn)行序列化的操作,如果User類沒有實現(xiàn)Serializable接口,再次運(yùn)行程序的時候會發(fā)現(xiàn),它在writeObject的時候會報錯:java.io.NotSerializableException。仔細(xì)想想應(yīng)該也能猜個大概:一定是writeObject方法中在進(jìn)行序列化的時候,對傳入的對象進(jìn)行了Serializable類型的判斷,只有實現(xiàn)了它才能繼續(xù)進(jìn)行序列化。

Externalizable接口

? ? ? ?這個接口其實是繼承了Serializable,它更加靈活一點,它里面定義了writeExternal和readExternal兩個方法分別用于序列化和反序列化使用。通過這兩個方法,我們可以自己決定需要序列化那些數(shù)據(jù)。如果對象中涉及到很少的屬性需要序列化,大多數(shù)屬性無需序列化,這種情況使用Externalizable接口是比較靈活的。當(dāng)然Serializable也同樣能實現(xiàn)只序列化出一部分屬性,只要將不需要序列化的屬性前面加一個trasient修飾符即可。

具體它的使用方式與前面Serializable接口的使用方式幾乎一樣,只是需要在實現(xiàn)了Externalizable接口的類中實現(xiàn)writeExternal和readExternal方法,在方法里面自己定義寫出的屬性,如:

public void writeExternal(ObjectOutput out) throws IOException {
 //這里只寫入了name和gender
 out.writeObject(name);
 out.writeObject(gender);
}
public void readExternal(ObjectInput in) throws IOException,
 ClassNotFoundException {
 //注意讀取的順序與write的順序保持一致
 name = (String)in.readObject();
 age = (String)in.readObject();
}

? ? ? ?以上這兩種序列化方式都是Java原生提供的序列化API,在一些特殊需求的場景中,可以借助于第三方包來實現(xiàn)序列化,例如:可以通過jackson包,利用ObjectMapper來實現(xiàn)JSON式的序列化;fastjson同樣也能實現(xiàn)json式序列化;還有比較有名的hessian序列化,hessian更加側(cè)重于數(shù)據(jù),有些場景下它的性能是比較好的,但是它也有有些不適用的場景,主要還是因為它的內(nèi)部原理特性決定的。

hessian序列化

? ? ? ?首先它的一個最大特色就是跨語言,hessian提供了一整套的byte[]的寫入規(guī)范。這樣其他語言在實現(xiàn)hessian序列化的時候就可以參照這套的標(biāo)準(zhǔn)規(guī)范,從而達(dá)到不同語言之間的兼容效果,因此hessian的序列化都是圍繞這byte數(shù)組來的。來看一下它的簡單使用:

<!-- 首先需要引入hessian包 -->
<!-- https://mvnrepository.com/artifact/com.caucho/hessian -->
<dependency>
 <groupId>com.caucho</groupId>
 <artifactId>hessian</artifactId>
 <version>4.0.38</version>
</dependency>
public static <T> byte[] serialize(T obj) {
   byte[] bytes = null;
   ByteArrayOutputStream bos = new ByteArrayOutputStream();
   HessianOutput hessianOutput = new HessianOutput(bos);
   try {
     //obj必須實現(xiàn)Serializable接口
     hessianOutput.writeObject(obj);
     bytes = bos.toByteArray();
   } catch (IOException e) {
     e.printStackTrace();
   }
   return bytes;
}
?
@SuppressWarnings("unchecked")
public static <T> T deserialize(byte[] data) {
   if (data == null) return null;
   ByteArrayInputStream bis = new ByteArrayInputStream(data);
   HessianInput hessianInput = new HessianInput(bis);
   Object object = null;
   try {
     object = hessianInput.readObject();
   } catch (IOException e) {
     e.printStackTrace();
   }
   return (T)object;
}

? ? ? ?主要的序列化和反序列化的過程都在這里了。需注意一點的是:這里一定要使用HessianOutput和HessianInput,因為在編寫代碼時會發(fā)現(xiàn),IDE還會提示一個Hessian2Output和Hessian2Input,這是兩個不同的版本,Hessian2的使用方法需要去查閱具體的文檔。另外經(jīng)過Hessian序列化出來的內(nèi)容要比原生的Java序列化暫用空間要少,這是因為Hessian內(nèi)部做了一些優(yōu)化操作。此外還有protobuf序列化,它是Google的一個序列化開發(fā)工具,如果有需要,可以到網(wǎng)上查閱相關(guān)信息,目前還沒遇到過使用protobuf的情況,這里就不贅述了。

? ? ? ?除此之外,還有其他的一些序列化方案(比如:avro,kryo等等),而且那些序列化方式我沒有用過,所以這里就不再一一分析使用了,如果有需要,可以網(wǎng)上搜索一下,基本資料比較齊全了。

? ? ? ?不同的序列化方式,各自的優(yōu)缺點都不同,但是可以確定的是:第三方的序列化工具都會對序列化的結(jié)果做一定的優(yōu)化,可以使得序列化的結(jié)果更加精簡,占用空間更少,但是這些都是有代價,更少的空間占用,就意味著更多的時間去解析。對于普通的序列化需求,Java的原生API已經(jīng)可以滿足,除非特別特殊的場景需要使用第三方工具的某些特性,否則沒必要糾結(jié)于到底哪種性能更好,只有哪種更合適而已。試想:如果出現(xiàn)一個很完美的序列化方案,其他的序列化方式早就被淘汰了,既然存在,就一定有獨到之處。

Java原生序列化原理

? ? ? ?在此之前首先看一個例子:Java中有一個類叫ArrayList,開發(fā)中我們會經(jīng)常遇到,稍微了解ArrayList源碼設(shè)計的都應(yīng)該知道,它的內(nèi)部實際上是維護(hù)了一個Object數(shù)組elementData作為數(shù)據(jù)的存儲容器。對于ArrayList對象的增刪改查實際上都是對這個elementData進(jìn)行操作得出的結(jié)果,而且ArrayList是實現(xiàn)了Serializable接口的,所以它是可以被序列化的,但是它的elementData這個屬性前面加了一個transient修飾符,前面介紹過,如果屬性中加了transient修飾符,在序列化的時候是會被忽略的,那么問題就來了:我們在正常序列化ArrayList對象以后,再對其進(jìn)行反序列化的時候,其實仍然可以得到它內(nèi)部存儲的那些數(shù)據(jù),這是怎么做到的?這里就牽扯到了序列化的內(nèi)部實現(xiàn)原理了,仔細(xì)查閱ArrayList源碼可以發(fā)現(xiàn),該類中定義了writeObject和readObject兩個私有方法,這兩個會在序列化的時候使用到。

? ? ? ?結(jié)合前面的代碼示例,可以看到Java原生序列化有兩個關(guān)鍵的類:ObjectOutputStream和ObjectInputStream。對于序列化,需要用到ObjectOutputStream的writeObject方法,這里直接給出源碼中writeObject內(nèi)部的調(diào)用關(guān)系:writeObject --》writeObject0 --》writeOrdinaryObject --》writeSerialData --》invokeWriteObject。(這里的調(diào)用關(guān)系主要給出的是對于一個普通對象序列化時的調(diào)用關(guān)系,有些特殊的類型,比如:String,Enum等可能中間有所不一樣)。下面是invokeWriteObject方法的源代碼

void invokeWriteObject(Object obj, ObjectOutputStream out)
 throws IOException, UnsupportedOperationException
{
   requireInitialized();
   if (writeObjectMethod != null) {
     try {
       writeObjectMethod.invoke(obj, new Object[]{ out });
     } catch (InvocationTargetException ex) {
       Throwable th = ex.getTargetException();
       if (th instanceof IOException) {
         throw (IOException) th;
       } else {
         throw MiscException(th);
       }
     } catch (IllegalAccessException ex) {
       // should not occur, as access checks have been suppressed
       throw new InternalError(ex);
     }
    } else {
     throw new UnsupportedOperationException();
   }
}

? ? ? ?注意:核心是writeObjectMethod.invoke(obj, new Object[]{ out })這一句。這一句很明顯就是一個反射的語法,writeObjectMethod指的就是序列化對象所屬類中定義的writeObject方法對象。如果當(dāng)前對象所屬類中沒有定義writeObject這么一個方法,就會調(diào)用一個默認(rèn)的defaultWriteFields方法(走defaultWriteFields分支的判斷邏輯可在writeSerialData方法中查看)。

現(xiàn)在再來回想前面說過的:如果要對一個對象進(jìn)行序列化,該對象所屬的類必須實現(xiàn)Serializable接口,這是為什么?可以在writeObject0 方法中找到答案:

這是writeObject0 方法中的一段代碼:

...
if (obj instanceof String) {
 writeString((String) obj, unshared);
} else if (cl.isArray()) {
 writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
 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());
 }
}
...

? ? ? ?可以看到,第三個else if 判斷中對obj進(jìn)行了類型判斷,判斷其是否是Serializable類型,說明前面的猜想是對的。除此之外:它還進(jìn)行了String,數(shù)組以及枚舉類型的判斷。所以說如果待序列化的對象如果是上面的任何一種都可以進(jìn)行序列化,否則會拋出NotSerializableException。

? ? ? ?根據(jù)同樣的思路可以查閱ObjectInputStream源碼中的readObject方法調(diào)用鏈:readObject --》readObject0 --》 readOrdinaryObject --》readSerialData --》invokeReadObject。

? ? ? ?可以發(fā)現(xiàn):反序列化中的調(diào)用鏈跟序列化中的調(diào)用鏈非常相似。所以分析它的原理完全可以參照序列化中分析思路。它同樣在invokeReadObject方法內(nèi)部有一句核心的反射調(diào)用:readObjectMethod.invoke(obj, new Object[]{ in });利用反射調(diào)用readObject方法(如果對象所屬類自定義了該方法)。如果沒有,在readSerialData方法中就會直接走defaultReadFields方法,而不會走invokeReadObject方法了。而且在readObject0方法中也存在了大量的類型判斷,而且用的是switch-case,具體可以查閱源碼,這里就不再貼出代碼了。

? ? ? ?現(xiàn)在再來考慮:為什么ArrayList需要這么設(shè)計它的序列化方式,直接序列化elementData不是也可以?其實這是一個空間存儲效率的問題,因為在正常的使用場景中,elementData數(shù)組內(nèi)部其實都是存不滿的,可以說99%的情況都不會滿,而數(shù)組是固定長度的,剩余的空間其實存儲的都是null。如果直接序列化elementData中的內(nèi)容,就會發(fā)現(xiàn)有很多null也被序列化出來的,但是這些內(nèi)容是完全沒用的,這就浪費了效率和存儲空間。而ArrayList定義了writeObject方法,恰恰就解決了這個問題,在該方法中將elementData數(shù)組內(nèi)部真正有用的數(shù)據(jù)序列化出去,這樣既節(jié)約了空間,又提高了效率。

序列化的使用場景

持久化操作或網(wǎng)絡(luò)傳輸

? ? ? ?一個最常見的場景就是持久化操作,將對象作為一種數(shù)據(jù)存儲到某種介質(zhì)中一遍將來復(fù)用,這時就會使用到序列化技術(shù),不同的是實現(xiàn)序列化依賴的技術(shù)不同,但是核心仍然是序列化的概念。

? ? ? ?同樣的對于需要進(jìn)行網(wǎng)絡(luò)減傳輸?shù)?,也需要對其進(jìn)行序列化,否則對象只能存在與當(dāng)前虛擬機(jī)的內(nèi)存中,無法通過夸虛擬機(jī),跨網(wǎng)絡(luò)的數(shù)據(jù)通信,比如:現(xiàn)在常用的一些消息中間件,核心功能就是消息的傳遞,而且是不在不同環(huán)境下的消息傳遞,需要大量的數(shù)據(jù)在網(wǎng)絡(luò)中傳輸,如果沒有序列化,這些傳輸是完全無法完成的。

? ? ? ?這個是所有使用場景的核心,其它的應(yīng)用場景基本不會逃出這持久化和網(wǎng)絡(luò)傳輸這兩個概念。

RPC框架

? ? ? ?也就是遠(yuǎn)程調(diào)用框架,例如dubbo,本質(zhì)上說,遠(yuǎn)程調(diào)用就必須將對象序列化后通過網(wǎng)絡(luò)傳輸?shù)竭h(yuǎn)程調(diào)用機(jī)器上,這樣遠(yuǎn)程調(diào)用的機(jī)器無需實現(xiàn)具體的對象,僅僅通過網(wǎng)絡(luò),就可以完成一系列原本無法在本地完成的工作。而且RPC的調(diào)用方式極大的保護(hù)了代碼的安全,發(fā)放給調(diào)用端的可能僅僅是定義的一些接口,但是在調(diào)用端使用的時候,它的具體實現(xiàn)的類的對象則是由網(wǎng)絡(luò)傳輸提供,這樣避免了代碼的泄露風(fēng)險。

? ? ? ?其實RPC的序列化核心場景就是:網(wǎng)絡(luò)傳輸。

緩存

? ? ? ?現(xiàn)在比較熱門的話題之一,現(xiàn)在系統(tǒng)中幾乎必不可少的一個模塊,為了提高系統(tǒng)的響應(yīng)速度,我們可能會用到大量的緩存,有些緩存是純內(nèi)存級別的,就是程序關(guān)閉后,數(shù)據(jù)丟失。但是現(xiàn)在一些緩存框架,如:Redis等,可以實現(xiàn)定期的持久化,用以保證程序出現(xiàn)問題時不會完全丟失數(shù)據(jù),可能只是丟失很少一部分?jǐn)?shù)據(jù)。這個持久化的過程就需要涉及到序列化。

? ? ? ?緩存序列化核心場景:持久化操作。

其他

單例中的序列化安全問題

? ? ? ?面試中常常被問到的一個問題就是:單例模式的幾種實現(xiàn)方式。這里可以強(qiáng)烈推薦H大的 單例模式的七種實現(xiàn)。但是大多的單例模式設(shè)計方案都無法保證序列化下的安全,換句話說:在將對象序列化以后,再次進(jìn)行反序列化,可以繞過單例模式的檢查機(jī)制,從而導(dǎo)致內(nèi)存中存在兩個甚至更多個相同類型的對象,這對于單例模式來說是破壞性的,因為單例的原則就是:運(yùn)行期間,內(nèi)存中只會存在一個對象。常用的一種解決方案就是使用readResolve()方法來避免此事發(fā)生(可網(wǎng)上搜索相關(guān)用法,比較簡單)。

? ? ? ?在七種單例模式的寫法當(dāng)中,有一種采用枚舉的方式,它是《Effective Java》作者Josh Bloch提倡使用的一種方式,雖然在實際應(yīng)用中很少使用這種方式(因為枚舉方式的應(yīng)用場景非常狹小,很多場景下是不能使用的)。但是Josh Bloch既然提倡使用,肯定是有一定道理的。

寫法簡單

首先枚舉單例的寫法非常簡單,三行代碼即可搞定:

public enum Singleton{
 INSTANCE;
}

枚舉類型可以自己處理序列化

? ? ? ?前面在分析序列化的時候,可以發(fā)現(xiàn),在writeObject0方法中其實是有枚舉類型的判斷的。而且Java的規(guī)范中明確說明了:每一個枚舉類型及其定義的枚舉變量在JVM中都是唯一的,在枚舉類型的序列化和反序列化上,Java做了特殊的規(guī)定:在對枚舉對象進(jìn)行序列化的時候,Java僅僅是將枚舉對象的name屬性輸出,在反序列化的時候,通過java.lang.Enum的valueOf方法,根據(jù)name查找對應(yīng)的枚舉對象;同時編譯器不允許任何對該序列化過程的定制。禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve方法。

? ? ? ?簡單來說就是:Java對枚舉類型的序列化和反序列化有特殊規(guī)定,不允許定制,那些傳統(tǒng)的方法全部失效,而且序列化寫出的時候只有一個name屬性,反序列化的時候,在虛擬機(jī)中通過name查找相應(yīng)的枚舉對象。而且枚舉對象在虛擬機(jī)內(nèi)部是唯一的。

? ? ? ?所以說枚舉類型的安全性保障是JVM提供的,它是非??煽康摹?/p>

枚舉實例的創(chuàng)建是線程安全的

現(xiàn)在以前面的Singleton的枚舉寫法為例,使用cfr工具對其進(jìn)行反編譯可以看到:

//使sugarenums參數(shù)設(shè)置為false,可以得到較為詳細(xì)的編譯結(jié)果
root@ubuntu:/usr/local/workspace/demo3# java -jar ../cfr_0_132.jar Singleton.class --sugarenums false
/*
 * Decompiled with CFR 0_132.
 */
public final class Singleton extends Enum<Singleton> {
 public static final /* enum */ Singleton INSTANCE = new Singleton();
 private static final /* synthetic */ Singleton[] $VALUES;
?
 public static Singleton[] values() {
 return (Singleton[])$VALUES.clone();
 }
?
 public static Singleton valueOf(String string) {
 return Enum.valueOf(Singleton.class, string);
 }
?
 private Singleton() {
 super(string, n);
 }
?
 static {
 $VALUES = new Singleton[]{INSTANCE};
 }
}

? ? ? ?可以看到,Singleton實際上是繼承了Enum類,并且將其定義成成了final類型,無法被繼承了。它的內(nèi)部幾乎都是static屬性的,static類型的屬性會在類被加載之后被初始化。當(dāng)一個Java類第一次被真正使用到的時候,會對它的靜態(tài)資源(static修飾)進(jìn)行初始化以及Java類的加載,這個過程是線程安全的。這個線程安全是虛擬機(jī)保證的,所以說它同樣是可靠的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Java序列化機(jī)制 序列化和反序列化 Java序列化是Java內(nèi)建的數(shù)據(jù)(對象)持久化機(jī)制,通過序列化可以將運(yùn)行時...
    0x70e8閱讀 506評論 0 1
  • 1. 對象序列化定義 Java平臺允許我們在內(nèi)存中創(chuàng)建可復(fù)用的Java對象,但一般情況下,只有當(dāng)JVM處于運(yùn)行時,...
    mance閱讀 297評論 0 0
  • 正如前文《Java序列化心得(一):序列化設(shè)計和默認(rèn)序列化格式的問題》中所提到的,默認(rèn)序列化方法存在各種各樣的問題...
    登高且賦閱讀 8,751評論 0 19
  • 如果你只知道實現(xiàn) Serializable 接口的對象,可以序列化為本地文件。那你最好再閱讀該篇文章,文章對序列化...
    jiangmo閱讀 560評論 0 2
  • 序言 將 Java 對象序列化為二進(jìn)制文件的 Java 序列化技術(shù)是 Java 系列技術(shù)中一個較為重要的技術(shù)點,在...
    martin6699閱讀 384評論 0 1

友情鏈接更多精彩內(nèi)容