注意:本文語言環(huán)境為:max os x 10 ,jdk 1.8
前言
對于一個對象實例囤热,如果我想在系統(tǒng)重啟后锹雏,仍能重現(xiàn)磷籍。那么這個時候通過數(shù)據(jù)庫持久化可以將對象實例存儲到數(shù)據(jù)庫內(nèi)即横,在需要的時候再取出來袄秩,進行對象的復(fù)現(xiàn)阵翎。但是如果我想在進行遠程調(diào)用或者跨平臺進行數(shù)據(jù)傳輸時,對方接收到的對象實例和我傳輸?shù)氖且恢碌闹纾敲催@個時候如果實現(xiàn)呢郭卫?請看下面這段代碼:
public class Logon {
Date date = new Date();
String username;
public Logon(String name, String pwd) {
username = name;
password = pwd;
}
}
如果在進行數(shù)據(jù)傳輸時,不能保證對象的實例是同一個背稼,對于Logon類而言贰军,那么date屬性的值就不能保證是一致的。
如果我想保持對象實例的一致性,那么怎么做呢词疼?實現(xiàn)序列化接口即可俯树。
public class Logon implements Serializable {
Date date = new Date();
String username;
public Logon(String name, String pwd) {
username = name;
password = pwd;
}
}
1、標記接口
看到這贰盗,我們就有一個疑問:為嘛序列化接口是一個空接口许饿,憑聲明一個空接口就可以進行對象實例的持久化?
我們來看jdk源碼內(nèi)的序列化接口聲明:
public interface Serializable {
}
這種接口也稱為標記接口(類似的還有cloneable)舵盈,只需要類實現(xiàn)該接口即可陋率,jvm底層自動會幫你完成這個接口的特殊功能(在有的情況下)。
2秽晚、序列化接口實現(xiàn)原理
但是到這瓦糟,我們上面的疑問其實還沒解答啊。
序列化接口作為一種將對象轉(zhuǎn)換為流數(shù)據(jù)的方法赴蝇,必須通過ObjectOutputStream/ObjectInputStream對象進行流的傳輸菩浙。在進行流傳輸時,ObjectOutputStream/ObjectInputStream會調(diào)用其對應(yīng)的writeObject/readObject方法進行對象的序列化操作(具體的代碼我就不貼了扯再,太長了)芍耘。
這個時候就有一個問題, 我就不想用系統(tǒng)自帶的序列化方法熄阻,我想自己自定義斋竞,怎么辦?
好辦秃殉,在類內(nèi)添加如下方法坝初。
private void writeObject(java.io.ObjectOutputStream s)
throws IOException;
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException;
寫完這個代碼之后,心里虛啊钾军,這里有幾個奇怪的地方鳄袍。
1)既然是實現(xiàn)接口的功能,為嘛這個訪問權(quán)限是private
2)方法寫完了之后吏恭,在這個類內(nèi)都沒有調(diào)用的地方拗小。
這2個問題,其實我也不知道怎么解釋(坐等高人……)樱哼。其實哀九,在看源碼之后(代碼在手,天下我有)搅幅,可以給出如下的解釋(還是debug靠譜):
首先阅束,在這個類內(nèi)沒有地方調(diào)用,是因為這2個方法是在ObjectOutputStream/ObjectInputStream內(nèi)調(diào)用的茄唐,是由這2個類內(nèi)的writeObject/readObject調(diào)用對象內(nèi)覆寫的writeObject/readObject方法進行對象的序列化操作息裸。
至于private訪問權(quán)限,有萬能的反射。
來呼盆,我們看ObjectOutputStream的代碼:
private void writeSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
//判斷是否覆寫了writeObject方法
if (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
//反射調(diào)用覆寫的writeObject方法
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
//調(diào)用默認的writeObject方法
defaultWriteFields(obj, slotDesc);
}
}
}
3年扩、自定義序列化的另一種方式
上面不是提到了自定義序列化方式嗎,除了覆寫2個方法外宿亡,還有一種方式常遂,就是實現(xiàn)Externalizable接口纳令。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
4挽荠、transient關(guān)鍵字
上面介紹的都是自定義序列化方法,但是對于一個成員變量平绩,如果我不想對其進行序列化操作圈匆,這個時候如果僅采用自定義序列化,那么肯定要完全重寫ObjectOutputStream/ObjectInputStream內(nèi)的序列化/反序列化代碼捏雌,否則一旦調(diào)用defaultWriteObject/defaultReadObject跃赚,那么成員變量就自動進行了序列化的操作。
此時transient關(guān)鍵字就發(fā)揮作用了性湿。這個關(guān)鍵字會告訴defaultWriteObject方法纬傲,這個成員變量不用進行序列化了。如果一個成員變量被transient修飾符修飾了肤频,但是又想進行序列化怎么辦叹括?就重寫該類的writeObject /readObject方法吧,在這2個方法內(nèi)自定義序列化與反序列化宵荒。
5汁雷、序列化與反序列化
對于上面的Logon類,生成一個對象實例报咳,并序列化后侠讯,再新增一個屬性password(可以將序列化完成的結(jié)果持久化到硬盤),變成如下類:
public class Logon implements Serializable {
private Date date = new Date();
private String username;
private String password;
}
那么再進行反序列化調(diào)用readObject時暑刃,會發(fā)生什么呢厢漩?
會拋出ClassNotFoundException。為什么會這樣呢岩臣?因為序列化實際上會自動生成一個版本信息溜嗜,類似于這樣:
public class Logon implements Serializable {
private static final long serialVersionUID = -5664935080424674771L;
private Date date = new Date();
private String username;
private String password;
}
那如果沒生成serialVersionUID屬性會怎樣?那么此時序列化機制會根據(jù)對象內(nèi)的成員屬性自動生成一個序列化版本號(猜測類似于hashcode)婿脸,當對象的成員變量有變更時粱胜,那么serialVersionUID就會不一致,反序列化時就會認為這2個不是一個類狐树,拋ClassNotFoundException也在情理之中了焙压。
如何避免這種情況呢?正常來說,最好在實現(xiàn)序列化接口時涯曲,手動生成一個版本信息野哭。那么即使對象的成員變量有變更,在進行反序列化時也會將未變更的成員變量進行自動反序列化幻件。
還有一種情況拨黔,就是父類實現(xiàn)了序列化接口,但是子類沒有绰沥。那么如果父類有變更篱蝇,子類的序列化版本會發(fā)生變化嗎?不會!(原因徽曲,我也不知道零截,這么亂,我也是醉了)
如果系統(tǒng)是使用maven進行版本管理秃臣,如果成員變更涧衙,但是序列化版本沒變,那么此時可以考慮進行版本升級奥此,避免不同版本之間進行錯誤的序列化操作弧哎。
6、其他情況
對于static修飾的成員變量稚虎,也是不會進行序列化的撤嫩。因為所謂的序列化,是對對象的實例而言祥绞,而一個類的static成員變量非洲,是一個類共享的,不屬于任何一個對象實例蜕径,因而不會將static成員變量進行序列化两踏。如果想序列化static成員變量,那么就需要自定義序列化與反序列化兜喻。
7梦染、實例
上面講了這么多,我們來段代碼分析分析朴皆,加深一下印象帕识。還是對hashmap源碼進行分析(為嘛我就跟hashmap過不去呢,哈哈)遂铡。
先對writeObject進行分析:
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
//獲取當前實例的位桶大小
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
//調(diào)用ObjectOutputStream的默認序列化方法
s.defaultWriteObject();
//序列化位桶
s.writeInt(buckets);
//序列化當前對象的大小
s.writeInt(size);
//序列化每個entry內(nèi)的數(shù)據(jù)肮疗,即每個位桶內(nèi)的K-V
internalWriteEntries(s);
}
其中:
//瞬態(tài)的size
transient int size;
//獲取當前實例的位桶容量
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
//遍歷所有位桶,逐個序列化位桶內(nèi)的K-V
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
從上面代碼可以看出扒接,hashmap將位桶的數(shù)據(jù)用transient進行修飾:
transient Node<K,V>[] table;
進行這樣修飾之后伪货,也不是說在進行序列化時忽略掉们衙,而是基于一種優(yōu)化思想:在進行序列化時,如果該位桶有數(shù)據(jù)碱呼,我就進行序列化蒙挑,如果沒有數(shù)據(jù),那么我就不進行序列化愚臀。
再對readObject進行分析:
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
//調(diào)用ObjectInputStream內(nèi)的默認方法進行反序列化
s.defaultReadObject();
//類似于進行反構(gòu)造
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " + loadFactor);
//對于這個丟棄處理表示很不解
s.readInt();
//讀取需進行反序列化的實例大小
int mappings = s.readInt();
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " + mappings);
else if (mappings > 0) {
// range of 0.25...4.0
//隨機生成一個類似于負載因子的數(shù)
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
//生成位桶大小
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
//生成閾值
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
//遍歷忆蚀,逐個反序列化
//從這也可以看出,在進行反序列化的時候姑裂,不保證每個位桶內(nèi)的數(shù)據(jù)順序與原來的保持一致
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
8馋袜、小結(jié)
隨著技術(shù)的發(fā)展,如今的序列化已經(jīng)不限于java原生的序列化機制了.如果想了解javaweb中的json序列化炭分,可以看這個文章了解一下桃焕,不過目前的使用已經(jīng)遠超這篇文章的敘述了:
http://www.ibm.com/developerworks/cn/web/wa-lo-json/?ca=drs-tp3308
注:部分代碼與思想來自于Thinking in java(fourth edition)