Java基礎之序列化與反序列化詳解

1、什麼是序列化與反序列化?

序列化 (Serialization)是將對象的狀態信息轉換為可以存儲或傳輸的形式的過程。一般將一個對象存儲至一個儲存媒介,例如檔案或是記億體緩沖等。在網絡傳輸過程中,可以是字節或是XML等格式。而字節的或XML編碼格式可以還原完全相等的對象。這個相反的過程又稱為反序列化。

2、Java如何實現序列化和反序列化?

Java對象的序列化和反序列化

在Java中,我們可以通過多種方式來創建對象,並且隻要對象沒有被回收我們都可以復用該對象。但是,我們創建出來的這些Java對象都是存在於JVM的堆內存中的。隻有JVM處於運行狀態的時候,這些對象才可能存在。一旦JVM停止運行,這些對象的狀態也就隨之而丟失瞭。

但是在真實的應用場景中,我們需要將這些對象持久化下來,並且能夠在需要的時候把對象重新讀取出來。Java的對象序列化可以幫助我們實現該功能。

對象序列化機制(object serialization)是Java語言內建的一種對象持久化方式,通過對象序列化,可以把對象的狀態保存為字節數組,並且可以在有需要的時候將這個字節數組通過反序列化的方式再轉換成對象。對象序列化可以很容易的在JVM中的活動對象和字節數組(流)之間進行轉換。

在Java中,對象的序列化與反序列化被廣泛應用到RMI(遠程方法調用)及網絡傳輸中。

在Java中,隻要一個類實現瞭java.io.Serializable接口,那麼它就可以被序列化。這裡先來一段代碼:

code 1 創建一個User類,用於序列化及反序列化

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 +
                '}';
    }
}

code 2 對User進行序列化及反序列化的Demo 

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}

1、在Java中,隻要一個類實現瞭java.io.Serializable接口,那麼它就可以被序列化。

2、通過ObjectOutputStreamObjectInputStream對對象進行序列化及反序列化

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

4、序列化並不保存靜態變量。

5、要想將父類對象也序列化,就需要讓父類也實現Serializable 接口。

6、Transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。

7、服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,隻有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。

3、如何自定義序列化和反序列化呢?

code 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;
}

帶著這個問題,我們來看java.util.ArrayList的源碼從上面的代碼中可以知道ArrayList實現瞭java.io.Serializable接口,那麼我們就可以對它進行序列化及反序列化。因為elementData是transient的,所以我們認為這個成員變量不會被序列化而保留下來。我們寫一個Demo,驗證一下我們的想法:

code4

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]

瞭解ArrayList的人都知道,ArrayList底層是通過數組實現的。那麼數組elementData其實就是用來保存列表中的元素的。通過該屬性的聲明方式我們知道,他是無法通過序列化持久化下來的。那麼為什麼code 4的結果卻通過序列化和反序列化把List中的元素保留下來瞭呢? 

4、writeObject和readObject方法

在ArrayList中定義瞭來個方法: writeObjectreadObject

這裡先給出結論:

在序列化過程中,如果被序列化的類中定義瞭writeObject 和 readObject 方法,虛擬機會試圖調用對象類裡的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化。

如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。

用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。

那麼為什麼ArrayList要用這種方式來實現序列化呢? 

ArrayList實際上是動態數組,每次在放滿以後自動增長設定的長度值,如果數組自動增長長度設為100,而實際隻放瞭一個元素,那就會序列化99個null元素。為瞭保證在序列化的時候不會將這麼多null同時進行序列化,ArrayList把元素數組設置為transient。

前面說過,為瞭防止一個包含大量空對象的數組被序列化,為瞭優化存儲,所以,ArrayList使用transient來聲明elementData。 但是,作為一個集合,在序列化過程中還必須保證其中的元素可以被持久化下來,所以,通過重寫writeObject 和 readObject方法的方式把其中的元素保留下來。

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

readObject方法從輸入流(ObjectInputStream)中讀出對象並保存賦值到elementData數組中

至此,我們先試著來回答剛剛提出的問題:

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

答:可以通過在被序列化的類中增加writeObject 和 readObject方法。那麼問題又來瞭:

雖然ArrayList中寫瞭writeObject 和 readObject 方法,但是這兩個方法並沒有顯示的被調用啊。

那麼如果一個類中包含writeObject 和 readObject 方法,那麼這兩個方法是怎麼被調用的呢?

在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法時,會通過反射的方式調用。

5、serializable接口

類通過實現 java.io.Serializable 接口以啟用其序列化功能。未實現此接口的類將無法使其任何狀態序列化或反序列化。可序列化類的所有子類型本身都是可序列化的。序列化接口沒有方法或字段,僅用於標識可序列化的語義。

該接口並沒有字段和方法,為什麼隻有實現瞭該接口的對象才能被序列化呢?

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

序列化ID:

private static final long serialVersionUID = 1L;

序列化 ID在 IDE 下提供瞭兩種生成策略,一個設為固定的 1L,另一個是隨機生成一個不重復的 long 類型數據(實際上是使用 JDK 工具生成)。一般如果沒有特殊需求,用默認的 1L 就可以,這樣可以確保反序列化成功。因為不同的序列化id之間不能進行序列化和反序列化。

到此這篇關於Java基礎之序列化與反序列化詳解的文章就介紹到這瞭,更多相關Java序列化與反序列化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!