最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java之標(biāo)準(zhǔn)序列化機(jī)制詳解

 更新時(shí)間:2026年05月18日 09:04:16   作者:吳聲子夜歌  
本文介紹了Java中的序列化機(jī)制,包括Serializable接口的實(shí)現(xiàn)、ObjectOutputStream和ObjectInputStream的用法、transient關(guān)鍵字和writeObject/readObject方法的的自定義序列化

1、基本用法

要讓一個(gè)類支持序列化,只需要讓這個(gè)類實(shí)現(xiàn)接口java.io.Serializable。Serializable沒有定義任何方法,只是一個(gè)標(biāo)記接口。

比如,對(duì)于前面章節(jié)提到的Student類,為支持序列化,可改為:

public class Student implements Serializable {
    //省略主體代碼
}

聲明實(shí)現(xiàn)了Serializable接口后,保存/讀取Student對(duì)象就可以使用ObjectOutput-Stream/ObjectInputStream流了。

ObjectOutputStream是OutputStream的子類,但實(shí)現(xiàn)了Object-Output接口。

ObjectOutput是DataOutput的子接口,增加了一個(gè)方法:

public void writeObject(Object obj) throws IOException

這個(gè)方法能夠?qū)?duì)象obj轉(zhuǎn)化為字節(jié),寫到流中。

ObjectInputStream是InputStream的子類,它實(shí)現(xiàn)了ObjectInput接口。

ObjectInput是DataInput的子接口,增加了一個(gè)方法:

public Object readObject() throws ClassNotFoundException, IOException

這個(gè)方法能夠從流中讀取字節(jié),轉(zhuǎn)化為一個(gè)對(duì)象。

使用這兩個(gè)流,保存學(xué)生列表的代碼就可以變?yōu)椋?/p>

public static void writeStudents(List<Student> students)
    throws IOException {
    ObjectOutputStream out = new ObjectOutputStream(
            new BufferedOutputStream(new FileOutputStream("students.dat")));
    try {
        out.writeInt(students.size());
        for(Student s : students) {
            out.writeObject(s);
        }
    } finally {
        out.close();
    }
}

而從文件中讀入學(xué)生列表的代碼可以變?yōu)椋?/p>

public static List<Student> readStudents() throws IOException,
        ClassNotFoundException {
    ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(
            new FileInputStream("students.dat")));
    try {
        int size = in.readInt();
        List<Student> list = new ArrayList<>(size);
        for(int i = 0; i < size; i++) {
            list.add((Student) in.readObject());
        }
        return list;
    } finally {
        in.close();
    }
}

實(shí)際上,只要List對(duì)象也實(shí)現(xiàn)了Serializable(ArrayList/LinkedList都實(shí)現(xiàn)了)?,上面代碼還可以進(jìn)一步簡化,讀寫只需要一行代碼,如下所示:

public static void writeStudents(List<Student> students)
    throws IOException {
    ObjectOutputStream out = new ObjectOutputStream(
            new BufferedOutputStream(new FileOutputStream("students.dat")));
    try {
        out.writeObject(students);
    } finally {
        out.close();
    }
}
public static List<Student> readStudents() throws IOException,
        ClassNotFoundException {
    ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(
            new FileInputStream("students.dat")));
    try {
        return (List<Student>) in.readObject();
    } finally {
        in.close();
    }
}

是不是很神奇?只要將類聲明實(shí)現(xiàn)Serializable接口,然后就可以使用ObjectOutput-Stream/ObjectInputStream直接讀寫對(duì)象了。

我們之前介紹的各種類,如String、Date、Double、ArrayList、LinkedList、HashMap、TreeMap等,都實(shí)現(xiàn)了Serializable。

2、復(fù)雜對(duì)象

上面例子中的Student對(duì)象是非常簡單的,如果對(duì)象比較復(fù)雜呢?比如:

  1. 如果a、b兩個(gè)對(duì)象都引用同一個(gè)對(duì)象c,序列化后c是保存兩份還是一份?在反序列化后還能讓a、b指向同一個(gè)對(duì)象嗎?答案是,c只會(huì)保存一份,反序列化后指向相同對(duì)象。
  2. 如果a、b兩個(gè)對(duì)象有循環(huán)引用呢?即a引用了b,而b也引用了a。這種情況Java也沒問題,可以保持引用關(guān)系。

這就是Java序列化機(jī)制的神奇之處,它能自動(dòng)處理引用同一個(gè)對(duì)象的情況,也能自動(dòng)處理循環(huán)引用的情況,具體例子我們就不介紹了。

3、定制序列化

默認(rèn)的序列化機(jī)制已經(jīng)很強(qiáng)大了,它可以自動(dòng)將對(duì)象中的所有字段自動(dòng)保存和恢復(fù),但這種默認(rèn)行為有時(shí)候不是我們想要的。

對(duì)于有些字段,它的值可能與內(nèi)存位置有關(guān),比如默認(rèn)的hashCode()方法的返回值,當(dāng)恢復(fù)對(duì)象后,內(nèi)存位置肯定變了,基于原內(nèi)存位置的值也就沒有了意義。還有一些字段,可能與當(dāng)前時(shí)間有關(guān),比如表示對(duì)象創(chuàng)建時(shí)的時(shí)間,保存和恢復(fù)這個(gè)字段就是不正確的。

還有一些情況,如果類中的字段表示的是類的實(shí)現(xiàn)細(xì)節(jié),而非邏輯信息,那默認(rèn)序列化也是不適合的。為什么不適合呢?因?yàn)樾蛄谢袷奖硎疽环N契約,應(yīng)該描述類的邏輯結(jié)構(gòu),而非與實(shí)現(xiàn)細(xì)節(jié)相綁定,綁定實(shí)現(xiàn)細(xì)節(jié)將使得難以修改,破壞封裝。

比如,我們?cè)谌萜黝愔薪榻B的LinkedList,它的默認(rèn)序列化就是不適合的。為什么呢?因?yàn)長inkedList表示一個(gè)List,它的邏輯信息是列表的長度,以及列表中的每個(gè)對(duì)象,但LinkedList類中的字段表示的是鏈表的實(shí)現(xiàn)細(xì)節(jié),如頭尾節(jié)點(diǎn)指針,對(duì)每個(gè)節(jié)點(diǎn),還有前驅(qū)和后繼節(jié)點(diǎn)指針等。

那怎么辦呢?Java提供了多種定制序列化的機(jī)制,主要的有兩種:

  • 一種是transient關(guān)鍵字
  • 另外一種是實(shí)現(xiàn)writeObject和readObject方法。

將字段聲明為transient,默認(rèn)序列化機(jī)制將忽略該字段,不會(huì)進(jìn)行保存和恢復(fù)。比如,類LinkedList中,它的字段都聲明為了transient,如下所示:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

聲明為了transient,不是說就不保存該字段了,而是告訴Java默認(rèn)序列化機(jī)制,不要自動(dòng)保存該字段了,可以實(shí)現(xiàn)writeObject/readObject方法來自己保存該字段。

類可以實(shí)現(xiàn)writeObject方法,以自定義該類對(duì)象的序列化過程,其聲明必須為:

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException

可以在這個(gè)方法中,調(diào)用ObjectOutputStream的方法向流中寫入對(duì)象的數(shù)據(jù)。比如, LinkedList使用如下代碼序列化列表的邏輯數(shù)據(jù):

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    s.defaultWriteObject();
    //寫元素個(gè)數(shù)
    s.writeInt(size);
    //循環(huán)寫每個(gè)元素
    for(Node<E> x = first; x ! = null; x = x.next)
        s.writeObject(x.item);
}

需要注意的是代碼:

s.defaultWriteObject();

這一行是必需的,它會(huì)調(diào)用默認(rèn)的序列化機(jī)制,默認(rèn)機(jī)制會(huì)保存所有沒聲明為transient的字段,即使類中的所有字段都是transient,也應(yīng)該寫這一行,因?yàn)镴ava的序列化機(jī)制不僅會(huì)保存純粹的數(shù)據(jù)信息,還會(huì)保存一些元數(shù)據(jù)描述等隱藏信息,這些隱藏的信息是序列化之所以能夠神奇的重要原因。

與writeObject對(duì)應(yīng)的是readObject方法,通過它自定義反序列化過程,其聲明必須為:

private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException

在這個(gè)方法中,調(diào)用ObjectInputStream的方法從流中讀入數(shù)據(jù),然后初始化類中的成員變量。比如,LinkedList的反序列化代碼為:

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    //讀元素個(gè)數(shù)
    int size = s.readInt();
    //循環(huán)讀入每個(gè)元素
    for (int i = 0; i < size; i++)
        linkLast((E)s.readObject());
}

注意代碼:

s.defaultReadObject();

這一行代碼也是必需的。

除了自定義writeObject/readObject方法,還有一些自定義序列化過程的機(jī)制:Exter-nalizable接口、readResolve方法和writeReplace方法,這些機(jī)制用得相對(duì)較少,我們就不介紹了。

4、序列化的基本原理

  1. 如果類的字段表示的就是類的邏輯信息,如上面的Student類,那就可以使用默認(rèn)序列化機(jī)制,只要聲明實(shí)現(xiàn)Serializable接口即可。
  2. 否則的話,如LinkedList,那就可以使用transient關(guān)鍵字,實(shí)現(xiàn)writeObject和read-Object自定義序列化過程。
  3. Java的序列化機(jī)制可以自動(dòng)處理如引用同一個(gè)對(duì)象、循環(huán)引用等情況。

序列化到底是如何發(fā)生的呢?關(guān)鍵在ObjectOutputStream的writeObject和ObjectInput-Stream的readObject方法內(nèi)。它們的實(shí)現(xiàn)都非常復(fù)雜,正因?yàn)檫@些復(fù)雜的實(shí)現(xiàn)才使得序列化看上去很神奇,我們簡單介紹其基本邏輯。

writeObject的基本邏輯是:

  1. 如果對(duì)象沒有實(shí)現(xiàn)Serializable,拋出異常NotSerializableException。
  2. 每個(gè)對(duì)象都有一個(gè)編號(hào),如果之前已經(jīng)寫過該對(duì)象了,則本次只會(huì)寫該對(duì)象的引用,這可以解決對(duì)象引用和循環(huán)引用的問題。
  3. 如果對(duì)象實(shí)現(xiàn)了writeObject方法,調(diào)用它的自定義方法。
  4. 默認(rèn)是利用反射機(jī)制,遍歷對(duì)象結(jié)構(gòu)圖,對(duì)每個(gè)沒有標(biāo)記為transient的字段,根據(jù)其類型,分別進(jìn)行處理,寫出到流,流中的信息包括字段的類型,即完整類名、字段名、字段值等。

readObject的基本邏輯是:

  • 不調(diào)用任何構(gòu)造方法;
  • 它自己就相當(dāng)于是一個(gè)獨(dú)立的構(gòu)造方法,根據(jù)字節(jié)流初始化對(duì)象,利用的也是反射機(jī)制;
  • 在解析字節(jié)流時(shí),對(duì)于引用到的類型信息,會(huì)動(dòng)態(tài)加載,如果找不到類,會(huì)拋出ClassNotFoundException。

5、版本問題

代碼是在不斷演化的,而序列化的對(duì)象可能是持久保存在文件上的,如果類的定義發(fā)生了變化,那持久化的對(duì)象還能反序列化嗎?

默認(rèn)情況下,Java會(huì)給類定義一個(gè)版本號(hào),這個(gè)版本號(hào)是根據(jù)類中一系列的信息自動(dòng)生成的。在反序列化時(shí),如果類的定義發(fā)生了變化,版本號(hào)就會(huì)變化,與流中的版本號(hào)就會(huì)不匹配,反序列化就會(huì)拋出異常,類型為java.io.InvalidClassException。

通常情況下,我們希望自定義這個(gè)版本號(hào),而非讓Java自動(dòng)生成,一方面是為了更好地控制,另一方面是為了性能,因?yàn)镴ava自動(dòng)生成的性能比較低。怎么自定義呢?在類中定義如下變量:

private static final long serialVersionUID = 1L;

在Java IDE如Eclipse中,如果聲明實(shí)現(xiàn)了Serializable而沒有定義該變量,IDE會(huì)提示自動(dòng)生成。這個(gè)變量的值可以是任意的,代表該類的版本號(hào)。在序列化時(shí),會(huì)將該值寫入流,在反序列化時(shí),會(huì)將流中的值與類定義中的值進(jìn)行比較,如果不匹配,會(huì)拋出InvalidClassException。

那如果版本號(hào)一樣,但實(shí)際的字段不匹配呢?Java會(huì)分情況自動(dòng)進(jìn)行處理,以盡量保持兼容性,大概分為三種情況:

  • 字段刪掉了:即流中有該字段,而類定義中沒有,該字段會(huì)被忽略;
  • 新增了字段:即類定義中有,而流中沒有,該字段會(huì)被設(shè)為默認(rèn)值;
  • 字段類型變了:對(duì)于同名的字段,類型變了,會(huì)拋出InvalidClassException。

6、序列化特點(diǎn)分析

序列化的主要用途有兩個(gè):一個(gè)是對(duì)象持久化;另一個(gè)是跨網(wǎng)絡(luò)的數(shù)據(jù)交換、遠(yuǎn)程過程調(diào)用。Java標(biāo)準(zhǔn)的序列化機(jī)制有很多優(yōu)點(diǎn),使用簡單,可自動(dòng)處理對(duì)象引用和循環(huán)引用,也可以方便地進(jìn)行定制,處理版本問題等,但它也有一些重要的局限性。

  1. Java序列化格式是一種私有格式,是一種Java特有的技術(shù),不能被其他語言識(shí)別,不能實(shí)現(xiàn)跨語言的數(shù)據(jù)交換。
  2. Java在序列化字節(jié)中保存了很多描述信息,使得序列化格式比較大。
  3. Java的默認(rèn)序列化使用反射分析遍歷對(duì)象結(jié)構(gòu),性能比較低。
  4. Java的序列化格式是二進(jìn)制的,不方便查看和修改。

由于這些局限性,實(shí)踐中往往會(huì)使用一些替代方案。在跨語言的數(shù)據(jù)交換格式中,ⅩML/JSON是被廣泛采用的文本格式,各種語言都有對(duì)它們的支持,文件格式清晰易讀。有很多查看和編輯工具,它們的不足之處是性能和序列化大小,在性能和大小敏感的領(lǐng)域,往往會(huì)采用更為精簡高效的二進(jìn)制方式,如ProtoBuf、Thrift、MessagePack等。

至此,關(guān)于Java的標(biāo)準(zhǔn)序列化機(jī)制就介紹完了。我們介紹了它的用法和基本原理,最后分析了它的特點(diǎn),它是一種神奇的機(jī)制,通過簡單的Serializable接口就能自動(dòng)處理很多復(fù)雜的事情,但它也有一些重要的限制,最重要的是不能跨語言。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

最新評(píng)論

璧山县| 银川市| 南宁市| 江津市| 澎湖县| 保山市| 黑龙江省| 深圳市| 六安市| 清水县| 城固县| 武乡县| 崇义县| 伽师县| 岳阳市| 元朗区| 伊通| 嘉兴市| 正安县| 郁南县| 金乡县| 巴塘县| 濮阳县| 周口市| 即墨市| 蓬安县| 五大连池市| 千阳县| 静宁县| 开封市| 阿鲁科尔沁旗| 涟源市| 寿光市| 渭源县| 恩平市| 朝阳区| 铜梁县| 抚顺市| 噶尔县| 濉溪县| 大关县|