Java序列化基本原理

序列化是一種對象持久化的手段概荷。普遍應(yīng)用在網(wǎng)絡(luò)傳輸秕岛、RPC、RMI等場景中误证。
序列化有多種協(xié)議(如Thrift继薛、protobuf、hessian愈捅、kryo等)遏考,本文主要說Jdk自帶的序列化。

什么是IO流蓝谨?

byte序列的讀寫灌具,Java中的IO流是實現(xiàn)輸入/輸出的基礎(chǔ).

Java將數(shù)據(jù)從源(文件、內(nèi)存譬巫、鍵盤咖楣、網(wǎng)絡(luò))讀入到內(nèi)存 中,形成了流芦昔,然后將這些流還可以寫到另外的目的地(文件诱贿、內(nèi)存、控制臺咕缎、網(wǎng)絡(luò))珠十,之所以稱為流,是因為這個數(shù)據(jù)序列在不同時刻所操作的是源的不同部分凭豪。按照不同的分類標(biāo)準(zhǔn)焙蹭,IO流分為不同類型。主要有以下幾種方式:按照數(shù)據(jù)流方向嫂伞、數(shù)據(jù)處理的單位和功能孔厉。

不管流的分類是多么的豐富和復(fù)雜,其根源來自于四個基本的類末早。這個四個類的關(guān)系如下:

字節(jié)(1 byte)流 字符(1 char)流
抽象基類 InputStream烟馅、OutputStream Reader说庭、Writer
文件 FileInputStream然磷、FileOutputStream FileReader、FileWriter
數(shù)組 ByteArrayInputStream刊驴、ByteArrayOutputStream CharArrayReader姿搜、CharArrayWriter
管道 PipedInputStream寡润、PipedOutputStream PipedReader、PipedWriter
字符串 StringReader舅柜、StringWriter
緩沖流 BufferedInputStream梭纹、BufferedOutputStream BufferedReader、BufferedWriter
轉(zhuǎn)換流 InputStreamReader致份、OutputStreamWriter
對象流 ObjectInputStream变抽、ObjectOutputStream
抽象基類 FilterInputStream、FilterOutputStream FilterReader氮块、FilterWriter
打印流 PrintStream PrintWriter
推回輸入流 PushbackInputStream PushbackReader
特殊流 DataInputStream绍载、DataOutputStream

序列化的目的:

1)永久的保存對象,保存對象的字節(jié)序列到本地文件中滔蝉;
2)通過序列化對象在網(wǎng)絡(luò)中傳遞對象击儡;
3)通過序列化對象在進程間傳遞對象。

幾個問題:

  • 怎么實現(xiàn)Java的序列化
  • 為什么實現(xiàn)了java.io.Serializable接口才能被序列化
  • transient的作用是什么
  • 怎么自定義序列化策略
  • 自定義的序列化策略是如何被調(diào)用的
  • ArrayList對序列化的實現(xiàn)有什么好處

怎么實現(xiàn)Java的序列化

只要一個類實現(xiàn)了java.io.Serializable接口蝠引,那么它就可以被序列化阳谍。

public class User implements Serializable{
    // 可序列化對象的版本  
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    public User(){}
...
}

// -------- test
   public static void main(String[] args) {
        //Initializes The Object
        User user = new User();
        user.setName("hollis");
        user.setAge(23);
     
        //Write Obj 
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(oos);
        }

        //Read Obj 
        File file = new File("tempFile");
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(file));
            User newUser = (User) ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(ois);
            try {
                FileUtils.forceDelete(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

序列化及反序列化相關(guān)知識

1、在Java中螃概,只要一個類實現(xiàn)了java.io.Serializable接口矫夯,那么它就可以被序列化。

2谅年、通過ObjectOutputStream和ObjectInputStream對對象進行序列化及反序列化

3茧痒、虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致融蹂,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID)

4旺订、序列化并不保存靜態(tài)變量。

5超燃、要想將父類對象也序列化区拳,就需要讓父類也實現(xiàn)Serializable 接口。

6意乓、Transient 關(guān)鍵字的作用是控制變量的序列化樱调,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中届良,在被反序列化后笆凌,transient 變量的值被設(shè)為初始值,如 int 型的是 0士葫,對象型的是 null乞而。

7、serialVersionUID :在對象進行序列化或者反序列化操作的時候慢显,要考慮JDK版本的問題爪模,如果序列化的JDK版本和反序列化的JDK版本不統(tǒng)一則就可能造成異常欠啤,所以在序列化操作中引入了一個serialVersionUID的常量,可以通過此常量來驗證版本的一致性屋灌,在進行反序列化時,JVM會將傳過來的字節(jié)流中的serialVersionUID與本地相應(yīng)實體的serialVersionUID進行比較洁段,如果相同就認(rèn)為是一致的,可以進行反序列化共郭,否則就拋出不一致的異常祠丝。

8、服務(wù)器端給客戶端發(fā)送序列化對象數(shù)據(jù)除嘹,對象中有一些數(shù)據(jù)是敏感的纽疟,比如密碼字符串等,希望對該密碼字段在序列化時憾赁,進行加密污朽,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時龙考,才可以對密碼進行讀取蟆肆,這樣可以一定程度保證序列化對象的數(shù)據(jù)安全。

為什么實現(xiàn)了java.io.Serializable接口才能被序列化

以ArrayList為例

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    transient Object[] elementData; // non-private to simplify nested class access
    private int size;
}

可以知道ArrayList實現(xiàn)了java.io.Serializable接口晦款,那么我們就可以對它進行序列化及反序列化炎功。因為elementData是transient的,所以我們認(rèn)為這個成員變量不會被序列化而保留下來

public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<String> stringList = new ArrayList<String>();
        stringList.add("hello");
        stringList.add("world");
        stringList.add("hollis");
        stringList.add("chuang");
        System.out.println("init StringList" + stringList);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
        objectOutputStream.writeObject(stringList);

        IOUtils.close(objectOutputStream);
        File file = new File("stringlist");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        List<String> newStringList = (List<String>)objectInputStream.readObject();
        IOUtils.close(objectInputStream);
        if(file.exists()){
            file.delete();
        }
        System.out.println("new StringList" + newStringList);
    }
//init StringList[hello, world, hollis, chuang]
//new StringList[hello, world, hollis, chuang]

數(shù)組elementData其實就是用來保存列表中的元素的缓溅。通過該屬性的聲明方式我們知道蛇损,他是無法通過序列化持久化下來的。那么為什么code 4的結(jié)果卻通過序列化和反序列化把List中的元素保留下來了呢坛怪?

關(guān)鍵是重寫了writeObject和readObject方法

在序列化過程中淤齐,如果被序列化的類中定義了writeObject 和 readObject 方法,虛擬機會試圖調(diào)用對象類里的 writeObject 和 readObject 方法袜匿,進行用戶自定義的序列化和反序列化更啄。
如果沒有這樣的方法,則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法居灯。
用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程祭务,比如可以在序列化的過程中動態(tài)改變序列化的數(shù)值。

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

那么為什么ArrayList要用這種方式來實現(xiàn)序列化呢怪嫌?節(jié)省空間

why transient

ArrayList實際上是動態(tài)數(shù)組义锥,每次在放滿以后自動增長設(shè)定的長度值,如果數(shù)組自動增長長度設(shè)為100岩灭,而實際只放了一個元素拌倍,那就會序列化99個null元素。為了保證在序列化的時候不會將這么多null同時進行序列化川背,ArrayList把元素數(shù)組設(shè)置為transient。

why writeObject and readObject

前面說過,為了防止一個包含大量空對象的數(shù)組被序列化乘粒,為了優(yōu)化存儲诉稍,所以,ArrayList使用transient來聲明elementData缴允。 但是荚守,作為一個集合,在序列化過程中還必須保證其中的元素可以被持久化下來练般,所以矗漾,通過重寫writeObject 和 readObject方法的方式把其中的元素保留下來。

writeObject方法把elementData數(shù)組中的元素遍歷的保存到輸出流(ObjectOutputStream)中薄料。

readObject方法從輸入流(ObjectInputStream)中讀出對象并保存賦值到elementData數(shù)組中敞贡。

自定義的序列化策略是如何被調(diào)用的

對象的序列化過程通過ObjectOutputStream和ObjectInputputStream來實現(xiàn)的,那么帶著剛剛的問題摄职,我們來分析一下ArrayList中的writeObject 和 readObject 方法到底是如何被調(diào)用的呢誊役?

ObjectOutputStream的writeObject的調(diào)用棧:

writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject

void invokeWriteObject(Object obj, ObjectOutputStream out)
        throws IOException, UnsupportedOperationException
    {
        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 {
                    throwMiscException(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 });是關(guān)鍵,通過反射的方式調(diào)用writeObjectMethod方法谷市。

Serializable明明就是一個空的接口蛔垢,它是怎么保證只有實現(xiàn)了該接口的方法才能進行序列化與反序列化的呢?

            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());
                }
            }

在進行序列化操作時迫悠,會判斷要被序列化的類是否是Enum鹏漆、Array和Serializable類型,如果不是則直接拋出NotSerializableException

其他

路徑分隔符

在Windows下的路徑分隔符和Linux下的路徑分隔符是不一樣的创泄,當(dāng)直接使用絕對路徑時艺玲,跨平臺會暴出“No such file or diretory”的異常。

比如說要在temp目錄下建立一個test.txt文件鞠抑,在Windows下應(yīng)該這么寫:
File file1 = new File ("C:\tmp\test.txt");
在Linux下則是這樣的:
File file2 = new File ("/tmp/test.txt");

如果要考慮跨平臺板驳,則最好是這么寫:
File myFile = new File("C:" + File.separator + "tmp" + File.separator, "test.txt");

File類有幾個類似separator的靜態(tài)字段,都是與系統(tǒng)相關(guān)的碍拆,在編程時應(yīng)盡量使用若治。

  • public static final char separatorChar
    與系統(tǒng)有關(guān)的默認(rèn)名稱分隔符。此字段被初始化為包含系統(tǒng)屬性 file.separator 值的第一個字符感混。在 UNIX 系統(tǒng)上端幼,此字段的值為 '/';在 Microsoft Windows 系統(tǒng)上弧满,它為 ''婆跑。

  • public static final String separator
    與系統(tǒng)有關(guān)的默認(rèn)名稱分隔符,為了方便庭呜,它被表示為一個字符串滑进。此字符串只包含一個字符犀忱,即 separatorChar。

  • public static final char pathSeparatorChar
    與系統(tǒng)有關(guān)的路徑分隔符扶关。此字段被初始為包含系統(tǒng)屬性 path.separator 值的第一個字符阴汇。此字符用于分隔以路徑列表 形式給定的文件序列中的文件名。在 UNIX 系統(tǒng)上节槐,此字段為 ':'搀庶;在 Microsoft Windows 系統(tǒng)上,它為 ';'铜异。

  • public static final String pathSeparator
    與系統(tǒng)有關(guān)的路徑分隔符哥倔,為了方便,它被表示為一個字符串揍庄。此字符串只包含一個字符咆蒿,即 pathSeparatorChar。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚂子,一起剝皮案震驚了整個濱河市蜡秽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缆镣,老刑警劉巖芽突,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異董瞻,居然都是意外死亡寞蚌,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門钠糊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挟秤,“玉大人,你說我怎么就攤上這事抄伍∷腋眨” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵截珍,是天一觀的道長攀甚。 經(jīng)常有香客問我,道長岗喉,這世上最難降的妖魔是什么秋度? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮钱床,結(jié)果婚禮上荚斯,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好事期,可當(dāng)我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布滥壕。 她就那樣靜靜地躺著,像睡著了一般兽泣。 火紅的嫁衣襯著肌膚如雪绎橘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天撞叨,我揣著相機與錄音,去河邊找鬼浊洞。 笑死牵敷,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的法希。 我是一名探鬼主播枷餐,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼苫亦!你這毒婦竟也來了毛肋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤屋剑,失蹤者是張志新(化名)和其女友劉穎润匙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唉匾,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡孕讳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了巍膘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厂财。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖峡懈,靈堂內(nèi)的尸體忽然破棺而出璃饱,到底是詐尸還是另有隱情,我是刑警寧澤肪康,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布荚恶,位于F島的核電站,受9級特大地震影響磷支,放射性物質(zhì)發(fā)生泄漏裆甩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一齐唆、第九天 我趴在偏房一處隱蔽的房頂上張望嗤栓。 院中可真熱鬧,春花似錦、人聲如沸茉帅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堪澎。三九已至擂错,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間樱蛤,已是汗流浹背钮呀。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留昨凡,地道東北人爽醋。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像便脊,于是被迫代替她去往敵國和親蚂四。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容