序列化和反序列化2

文章1:https://www.hollischuang.com/archives/1150
文章2:http://www.hollischuang.com/archives/1140#What%20Serializable%20Did

這里通過(guò)分析數(shù)組列表的序列化來(lái)介紹Java序列化的相關(guān)內(nèi)容达吞。主要涉及到以下幾個(gè)問(wèn)題:序列化是一種對(duì)象持久化的手段。

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

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

瞬變的作用是什么

怎么自定義序列化策略

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

ArrayList對(duì)序列化的實(shí)現(xiàn)有什么好處

Java對(duì)象的序列化

Java平臺(tái)允許我們?cè)趦?nèi)存中創(chuàng)建可替換的Java對(duì)象本涕,但一般情況下烦感,只有當(dāng)JVM在運(yùn)行時(shí)铅檩,這些對(duì)象才可能存在仗颈,即刻盐,這些對(duì)象的生命周期不會(huì)比JVM的生命周期延長(zhǎng)尚困。但在現(xiàn)實(shí)應(yīng)用中蠢箩,就可能要求在JVM停止運(yùn)行之后才能保存(持久化)指定的對(duì)象,并在將來(lái)重新讀取被保存的對(duì)象尾组。Java對(duì)象序列化就能夠幫助我們實(shí)現(xiàn)該功能忙芒。

使用Java對(duì)象序列化示弓,在保存對(duì)象時(shí)讳侨,會(huì)把其狀態(tài)保存為一個(gè)字節(jié),在未來(lái)奏属,再將這些字節(jié)組裝成對(duì)象跨跨。必須注意地是,對(duì)象序列化保存的是對(duì)象的“狀態(tài)” 囱皿,即它的成員變量勇婴。可以可知嘱腥,對(duì)象序列化不會(huì)關(guān)注類中的靜態(tài)變量耕渴。

除了在持久化對(duì)象時(shí)會(huì)用到對(duì)象序列化之外,當(dāng)使用RMI(遠(yuǎn)程方法調(diào)用)齿兔,或在網(wǎng)絡(luò)中傳遞對(duì)象時(shí)橱脸,都會(huì)用到對(duì)象序列化础米。Java序列化API為處理對(duì)象序列化提供了一個(gè)標(biāo)準(zhǔn)機(jī)制,該API簡(jiǎn)單易用添诉。

如何對(duì)Java對(duì)象進(jìn)行序列化與反序列化

在Java中屁桑,只要一個(gè)類實(shí)現(xiàn)了java.io.Serializable接口,那么它就可以被序列化栏赴。這里先來(lái)一段代碼:

代碼1創(chuàng)建一個(gè)用戶類蘑斧,用于序列化及反序列化

package com.hollis;
import java.io.Serializable;
import java.util.Date;

/**
 * Created by hollis on 16/2/2.
 */
public class User implements Serializable{
    private String name;
    private int age;
    private Date birthday;
    private transient String gender;
    private static final long serialVersionUID = -6849794470754667710L;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", gender=" + gender +
                ", birthday=" + birthday +
                '}';
    }
}

代碼2對(duì)用戶進(jìn)行序列化及反序列化的演示

package com.hollis;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.util.Date;

/**
 * Created by hollis on 16/2/2.
 */
public class SerializableDemo {

    public static void main(String[] args) {
        //Initializes The Object
        User user = new User();
        user.setName("hollis");
        user.setGender("male");
        user.setAge(23);
        user.setBirthday(new Date());
        System.out.println(user);

        //Write Obj to File
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(oos);
        }

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

    }
}
//output 
//User{name='hollis', age=23, gender=male, birthday=Tue Feb 02 17:37:38 CST 2016}
//User{name='hollis', age=23, gender=null, birthday=Tue Feb 02 17:37:38 CST 2016}

序列化及反序列化相關(guān)知識(shí)
1,在Java中须眷,只要一個(gè)類實(shí)現(xiàn)了java.io.Serializable接口竖瘾,那么它就可以被序列化。

2柒爸,通過(guò)ObjectOutputStream和ObjectInputStream對(duì)對(duì)象進(jìn)行序列化及反序列化

3准浴,虛擬機(jī)是否允許反序列化,其他連續(xù)類路徑和功能代碼是否一致捎稚,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化ID是否一致(就是private static final long serialVersionUID)

4乐横,序列化并不保存靜態(tài)變量。

5今野,要想將父類對(duì)象也序列化葡公,就需要讓父類也實(shí)現(xiàn)Serializable接口。

6条霜,瞬態(tài)關(guān)鍵字的作用是控制變量的序列化催什,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中宰睡,在被反序列化后蒲凶,瞬態(tài)變量的值被設(shè)置為初始值,如int型的是0拆内,對(duì)象型的是null旋圆。

7,服務(wù)器端給客戶端發(fā)送序列化對(duì)象數(shù)據(jù)麸恍,對(duì)象中有一些數(shù)據(jù)是敏感的灵巧,某些密碼字符串等,希望該密碼分區(qū)在序列化時(shí),進(jìn)行加密,而客戶端如果擁有解密的密鑰玄柠,僅在客戶端進(jìn)行反序列化時(shí),才可以對(duì)密碼進(jìn)行讀取敏弃,這樣可以一定程度保證序列化對(duì)象的數(shù)據(jù)安全。

ArrayList的序列化

在介紹ArrayList序列化之前噪馏,先來(lái)考慮一個(gè)問(wèn)題:

如何自定義的序列化和反序列化策略

帶著這個(gè)問(wèn)題麦到,我們來(lái)看java.util.ArrayList的源碼

代碼3

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實(shí)現(xiàn)了java.io.Serializable接口,那么我們就可以對(duì)它進(jìn)行序列化及反序列化隅要。因?yàn)閑lementData是transient的蝴罪,所以我們認(rèn)為這個(gè)成員變量不會(huì)被序列化而保留下來(lái)。我們寫一個(gè)Demo步清,驗(yàn)證一下我們的想法:

代碼4

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ù)組實(shí)際上elementData就是用來(lái)保存列表中的元素的要门。通過(guò)該屬性的聲明方式我們知道,他是無(wú)法通過(guò)序列化持久化下來(lái)的廓啊。那么為什么code 4的結(jié)果卻通過(guò)序列化和反序列化把List中的元素保留下來(lái)了呢欢搜?

writeObject和readObject方法
在ArrayList中定義了來(lái)個(gè)方法:writeObject和readObject。

這里先提出論文:

在序列化過(guò)程中谴轮,如果被序列化的類中定義了writeObject和readObject方法炒瘟,虛擬機(jī)會(huì)引發(fā)調(diào)用對(duì)象類里的writeObject和readObject方法,進(jìn)行用戶自定義的序列化和反序列化第步。

如果沒(méi)有這樣的方法疮装,則默認(rèn)調(diào)用的是ObjectOutputStream的defaultWriteObject方法以及ObjectInputStream的defaultReadObject方法。

用戶自定義的writeObject和readObject方法可以允許用戶控制序列化的過(guò)程粘都,例如可以在序列化的過(guò)程中動(dòng)態(tài)改變序列化的數(shù)值廓推。

來(lái)看一下這兩個(gè)方法的具體實(shí)現(xiàn):

代碼5

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

代碼6

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要用這種方式來(lái)實(shí)現(xiàn)序列化呢?

為什么瞬態(tài)

ArrayList實(shí)際上是動(dòng)態(tài)片段翩隧,每次在放滿以后自動(dòng)增長(zhǎng)設(shè)置的長(zhǎng)度值樊展,如果數(shù)組自動(dòng)增長(zhǎng)長(zhǎng)度設(shè)置為100,而實(shí)際只放了一個(gè)元素堆生,則會(huì)序列化99個(gè)null元素专缠。為了保證在序列化的時(shí)候不會(huì)將這么多null同時(shí)進(jìn)行序列化,ArrayList把元素?cái)?shù)組設(shè)置為瞬態(tài)淑仆。

為什么writeObject和readObject

但是涝婉,作為一個(gè)集合,在序列化過(guò)程中還必須保證其中的元素可以被持久化下來(lái)糯景。前面說(shuō)過(guò)嘁圈,為了防止一個(gè)包含大量空物體的序列被序列化省骂,以便優(yōu)化存儲(chǔ)蟀淮,所以,ArrayList使用transient來(lái)聲明elementData钞澳。 怠惶,所以,通過(guò)重新writeObject和readObject方法的方式把其中的元素保留下來(lái)轧粟。

writeObject方法把上面elementData的元素遍歷的保存到輸出流(ObjectOutputStream)中策治。

readObject方法從輸入流(ObjectInputStream)中識(shí)別對(duì)象并保存賦值到數(shù)組elementData中脓魏。

至此,我們先試著來(lái)回答剛剛提出的問(wèn)題:

如何自定義的序列化和反序列化策略

答:可以通過(guò)在被序列化的類中增加writeObject和readObject方法通惫。那么問(wèn)題又來(lái)了:

雖然ArrayList中寫了writeObject和readObject方法茂翔,但是這兩個(gè)方法并沒(méi)有顯示的被調(diào)用啊。

那么如果一個(gè)類中包含writeObject和readObject方法履腋,那么這兩個(gè)方法是怎么被調(diào)用的呢珊燎?

ObjectOutputStream
從代碼4中,我們可以研磨遵湖,對(duì)象的序列化過(guò)程通過(guò)ObjectOutputStream和ObjectInputputStream來(lái)實(shí)現(xiàn)的悔政,那么帶著剛剛的問(wèn)題,我們來(lái)分析一下ArrayList中的writeObject和readObject方法到底是如何被調(diào)用的呢延旧?

為了節(jié)省篇幅谋国,此處稱為ObjectOutputStream的writeObject的調(diào)用棧:

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

這里看一下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)鍵,通過(guò)反射的方式調(diào)用writeObjectMethod方法迁沫。官方是這么解釋這個(gè)writeObjectMethod的:

類定義的writeObject方法芦瘾;如果沒(méi)有,則為null

在我們的示例中集畅,這個(gè)方法就是我們?cè)贏rrayList中定義的writeObject方法旅急。通過(guò)反射的方式被調(diào)用了。

至此牡整,我們先試著來(lái)回答剛剛提出的問(wèn)題:

如果一個(gè)類中包含writeObject和readObject方法藐吮,那么這兩個(gè)方法是怎么被調(diào)用的?

答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法時(shí)逃贝,會(huì)通過(guò)反射的方式調(diào)用谣辞。

至此,我們已經(jīng)介紹完了ArrayList的序列化方式沐扳。那么泥从,不知道有沒(méi)有人提出這樣的疑問(wèn):

Serializable明明就是一個(gè)空的接口,它是怎么保證只能實(shí)現(xiàn)該接口的方法才能進(jìn)行序列化與反序列化的呢沪摄?

Serializable接口的定義:

public interface Serializable {
}
讀者可以嘗試把代碼1中的繼承Serializable的代碼去掉躯嫉,再執(zhí)行代碼2,會(huì)拋出java.io.NotSerializableException杨拐。

其實(shí)這個(gè)問(wèn)題也很好回答祈餐,我們?cè)倩氐絼倓侽bjectOutputStream的writeObject的調(diào)用棧:

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

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

在進(jìn)行序列化操作時(shí),會(huì)判斷要被序列化的類是否為Enum哄陶,Array和Serializable類型帆阳,如果不是則直接拋出NotSerializableException。

總結(jié)
1屋吨,如果一個(gè)類想被序列化蜒谤,需要實(shí)現(xiàn)Serializable接口山宾。否則將引發(fā)NotSerializableException異常,這是因?yàn)轺⒒眨谛蛄谢僮鬟^(guò)程中序列類型進(jìn)行檢查资锰,要求被序列化的類必須屬于Enum,Array和Serializable類型其中的任何一種阶祭。

2台妆,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中胖翰。

3接剩,在類中增加writeObject和readObject方法可以實(shí)現(xiàn)自定義序列化策略

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市萨咳,隨后出現(xiàn)的幾起案子懊缺,更是在濱河造成了極大的恐慌,老刑警劉巖培他,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹃两,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡舀凛,警方通過(guò)查閱死者的電腦和手機(jī)俊扳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)猛遍,“玉大人馋记,你說(shuō)我怎么就攤上這事“每荆” “怎么了梯醒?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)腌紧。 經(jīng)常有香客問(wèn)我茸习,道長(zhǎng),這世上最難降的妖魔是什么壁肋? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任号胚,我火速辦了婚禮,結(jié)果婚禮上浸遗,老公的妹妹穿的比我還像新娘猫胁。我一直安慰自己,他們只是感情好乙帮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布杜漠。 她就那樣靜靜地躺著极景,像睡著了一般察净。 火紅的嫁衣襯著肌膚如雪驾茴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天氢卡,我揣著相機(jī)與錄音锈至,去河邊找鬼。 笑死译秦,一個(gè)胖子當(dāng)著我的面吹牛峡捡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播筑悴,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼们拙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了阁吝?” 一聲冷哼從身側(cè)響起砚婆,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎突勇,沒(méi)想到半個(gè)月后装盯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡甲馋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年埂奈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定躏。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡账磺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出痊远,到底是詐尸還是另有隱情绑谣,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布拗引,位于F島的核電站借宵,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏矾削。R本人自食惡果不足惜壤玫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哼凯。 院中可真熱鬧欲间,春花似錦、人聲如沸断部。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至她渴,卻和暖如春达址,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背趁耗。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工沉唠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人苛败。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓满葛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親罢屈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嘀韧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356