Java 物件序列化 NIO NIO2 深度解析

布still發表於2017-02-04

物件序列化

物件序列化機制允許把記憶體中的Java物件轉換成與平臺無關的二進位制流,從而可以儲存到磁碟或者進行網路傳輸,其它程式獲得這個二進位制流後可以將其恢復成原來的Java物件。 序列化機制可以使物件可以脫離程式的執行而對立存在

序列化的含義和意義

序列化

序列化機制可以使物件可以脫離程式的執行而對立存在

序列化(Serialize)指將一個java物件寫入IO流中,與此對應的是,物件的反序列化(Deserialize)則指從IO流中恢復該java物件

如果需要讓某個物件可以支援序列化機制,必須讓它的類是可序列化(serializable),為了讓某個類可序列化的,必須實現如下兩個介面之一:

  • Serializable:標記介面,實現該介面無須實現任何方法,只是表明該類的例項是可序列化的
  • Externalizable

所有在網路上傳輸的物件都應該是可序列化的,否則將會出現異常;所有需要儲存到磁碟裡的物件的類都必須可序列化;程式建立的每個JavaBean類都實現Serializable;

使用物件流實現序列化

實現Serializable實現序列化的類,程式可以通過如下兩個步驟來序列化該物件:

1.建立一個ObjectOutputStream,這個輸出流是一個處理流,所以必須建立在其他節點流的基礎之上

// 建立個ObjectOutputStream輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));

2.呼叫ObjectOutputStream物件的writeObject方法輸出可序列化物件

// 將一個Person物件輸出到輸出流中
oos.writeObject(per);

定義一個NbaPlayer類,實現Serializable介面,該介面標識該類的物件是可序列化的

public class NbaPlayer implements java.io.Serializable
{
    private String name;
    private int number;
    // 注意此處沒有提供無引數的構造器!
    public NbaPlayer(String name, int number)
    {
        System.out.println("有引數的構造器");
        this.name = name;
        this.number = number;
    }

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // number的setter和getter方法
    public void setNumber(int number)
    {
        this.number = number;
    }
    public int getNumber()
    {
        return this.number;
    }
}

使用ObjectOutputStream將一個NbaPlayer物件寫入磁碟檔案

import java.io.*;

public class WriteObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("object.txt")))
        {
            NbaPlayer player = new NbaPlayer("維斯布魯克", 0);
            // 將player物件寫入輸出流
            oos.writeObject(player);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

反序列化

從二進位制流中恢復Java物件,則需要使用反序列化,程式可以通過如下兩個步驟來序列化該物件:

1.建立一個ObjectInputStream輸入流,這個輸入流是一個處理流,所以必須建立在其他節點流的基礎之上

// 建立個ObjectInputStream輸出流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));

2.呼叫ObjectInputStream物件的readObject()方法讀取流中的物件,該方法返回一個Object型別的Java物件,可進行強制型別轉換成其真實的型別

// 從輸入流中讀取一個Java物件,並將其強制型別轉換為Person類
Person p = (Person)ois.readObject();

從object.txt檔案中讀取NbaPlayer物件的步驟

import java.io.*;
public class ReadObject
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            // 從輸入流中讀取一個Java物件,並將其強制型別轉換為NbaPlayer類
            NbaPlayer player = (NbaPlayer)ois.readObject();
            System.out.println("名字為:" + player.getName()
                + "\n號碼為:" + player.getNumber());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

反序列化讀取的僅僅是Java物件的資料,而不是Java類,因此採用反序列化恢復Java物件時,必須提供Java物件所屬的class檔案,否則會引發ClassNotFoundException異常;反序列化機制無須通過構造器來初始化Java物件

如果使用序列化機制向檔案中寫入了多個Java物件,使用反序列化機制恢復物件必須按照實際寫入的順序讀取。當一個可序列化類有多個父類時(包括直接父類和間接父類),這些父類要麼有無參的構造器,要麼也是可序列化的—否則反序列化將丟擲InvalidClassException異常。如果父類是不可序列化的,只是帶有無引數的構造器,則該父類定義的Field值不會被序列化到二進位制流中

物件引用的序列化

如果某個類的Field型別不是基本型別或者String型別,而是另一個引用型別,那麼這個引用型別必須是可序列化的,否則有用該型別的Field的類也是不可序列化的

public class AllStar implements java.io.Serializable
{
    private String name;
    private NbaPlayer player;
    public AllStar(String name, NbaPlayer player)
    {
        this.name = name;
        this.player = player;
    }
    // 此處省略了name和player的setter和getter方法

    // name的setter和getter方法
    public String getName()
    {
        return this.name;
    }

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

    // player的setter和getter方法
    public NbaPlayer getPlayer() 
    {
        return player;
    }

    public void setPlayer(NbaPlayer player) 
    {
        this.player = player;
    }
}

Java特殊的序列化演算法

  • 所有儲存到磁碟中的物件都有一個序列化編號
  • 當程式試圖序列化一個物件時,程式將先檢查該物件是否已經被序列化過,只有該物件從未(在本次虛擬中機)被序列化過,系統才會將該物件轉換成位元組序列並輸出
  • 如果某個物件已經序列化過,程式將只是直接輸出一個序列化編號,而不是再次重新序列化該物件
import java.io.*;
public class WriteAllStar
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectOutputStream輸出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("allStar.txt")))
        {
            NbaPlayer player = new NbaPlayer("詹姆斯哈登", 13);
            AllStar allStar1 = new AllStar("西部全明星", player);
            AllStar allStar2 = new AllStar("首發後衛", player);
            // 依次將四個物件寫入輸出流
            oos.writeObject(allStar1);
            oos.writeObject(allStar2);
            oos.writeObject(player);
            oos.writeObject(allStar2);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

4個寫入輸出流的物件,實際上只序列化了3個,而且序列的兩個AllStar物件的player引用實際是同一個NbaPlayer物件。以下程式讀取序列化檔案中的物件

import java.io.*;
public class ReadAllStar
{
    public static void main(String[] args)
    {
        try(
            // 建立一個ObjectInputStream輸出流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("allStar.txt")))
        {
            // 依次讀取ObjectInputStream輸入流中的四個物件
            AllStar star1 = (AllStar)ois.readObject();
            AllStar star2 = (AllStar)ois.readObject();
            NbaPlayer player = (NbaPlayer)ois.readObject();
            AllStar star3 = (AllStar)ois.readObject();
            // 輸出true
            System.out.println("star1的player引用和player是否相同:"
                + (star1.getPlayer() == player));
            // 輸出true
            System.out.println("star2的player引用和player是否相同:"
                + (star2.getPlayer() == player));
            // 輸出true
            System.out.println("star2和star3是否是同一個物件:"
                + (star2 == star3));
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

如果多次序列化同一個可變Java物件時,只有第一次序列化時才會把該Java物件轉換成位元組序列並輸出

當使用Java序列化機制序列化可變物件時,只有第一次呼叫WriteObject()方法來輸出物件時才會將物件轉換成位元組序列,並寫入到ObjectOutputStream;即使在後面程式中,該物件的例項變數發生了改變,再次呼叫WriteObject()方法輸出該物件時,改變後的例項變數也不會被輸出

import java.io.*;

public class SerializeMutable
{
    public static void main(String[] args)
    {

        try(
            // 建立一個ObjectOutputStream輸入流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("mutable.txt"));
            // 建立一個ObjectInputStream輸入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("mutable.txt")))
        {
            NbaPlayer player = new NbaPlayer("斯蒂芬庫裡", 30);
            // 系統會player物件轉換位元組序列並輸出
            oos.writeObject(player);
            // 改變per物件的name例項變數
            player.setName("塞斯庫裡");
            // 系統只是輸出序列化編號,所以改變後的name不會被序列化
            oos.writeObject(player);
            NbaPlayer player1 = (NbaPlayer)ois.readObject();    //①
            NbaPlayer player2 = (NbaPlayer)ois.readObject();    //②
            // 下面輸出true,即反序列化後player1等於player2
            System.out.println(player1 == player2);
            // 下面依然看到輸出"斯蒂芬庫裡",即改變後的例項變數沒有被序列化
            System.out.println(player2.getName());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

相關文章