關於Java序列化的問題你真的會嗎?

宜信技術學院發表於2020-03-10

引言

在持久化資料物件的時候我們很少使用Java序列化,而是使用資料庫等方式來實現。但是在我看來,Java 序列化是一個很重要的內容,序列化不僅可以儲存物件到磁碟進行持久化,還可以透過網路傳輸。在平時的面試當中,序列化也是經常被談及的一塊內容。

談到序列化時,大家可能知道將類實現Serializable介面就可以達到序列化的目的,但當看到關於序列化的面試題時我們卻常常一臉懵逼。  

1)可序列化介面和可外部介面的區別是什麼?

2)序列化時,你希望某些成員不要序列化?該如何實現?

3)什麼是 serialVersionUID ?如果不定義serialVersionUID,會發生什麼?

是不是突然發現我們對這些問題其實都還存在很多疑惑?本文將總結一些Java序列化的常見問題,並且透過demo來進行測試和解答。

問題一:什麼是 Java 序列化?

序列化是把物件改成可以存到磁碟或透過網路傳送到其它執行中的 Java 虛擬機器的二進位制格式的過程,並可以透過反序列化恢復物件狀態。Java 序列化API給開發人員提供了一個標準機制:透過實現 java.io.Serializable 或者 java.io.Externalizable 介面,ObjectInputStream 及ObjectOutputStream 處理物件序列化。實現java.io.Externalizable 介面的話,Java 程式設計師可自由選擇基於類結構的標準序列化或是它們自定義的二進位制格式,通常認為後者才是最佳實踐,因為序列化的二進位制檔案格式成為類輸出 API的一部分,可能破壞 Java 中私有和包可見的屬性的封裝。

序列化到底有什麼用?

實現 java.io.Serializable。

定義使用者類:

class User implements Serializable {
    private String username;
    private String passwd;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

我們把物件序列化,透過ObjectOutputStream儲存到txt檔案中,再透過ObjectInputStream讀取txt檔案,反序列化成User物件。

public class TestSerialize {
    public static void main(String[] args) {
        User user = new User();
        user.setUsername("hengheng");
        user.setPasswd("123456");
        System.out.println("read before Serializable: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());
        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
            os.writeObject(user); // 將User物件寫進檔案
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "/Users/admin/Desktop/test/user.txt"));
            user = (User) is.readObject(); // 從流中讀取User的資料
            is.close();
            System.out.println("\nread after Serializable: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

執行結果如下:

序列化前資料: 
username: hengheng
password: 123456
序列化後資料: 
username: hengheng
password: 123456

到這裡,我們大概知道了什麼是序列化。

問題二:序列化時,你希望某些成員不要序列化,該如何實現?

答案:宣告該成員為靜態或瞬態,在 Java 序列化過程中則不會被序列化。

  • 靜態變數:加static關鍵字。
  • 瞬態變數:  加transient關鍵字。

我們先嚐試把變數宣告為瞬態。

class User implements Serializable {
    private String username;
    private transient String passwd;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

在密碼欄位前加上了 transient關鍵字再執行。執行結果:

序列化前資料: 
username: hengheng
password: 123456
序列化後資料: 
username: hengheng
password: null

透過執行結果發現密碼沒有被序列化,達到了我們的目的。

再嘗試在使用者名稱前加 static關鍵字。

class User implements Serializable {
    private static String username;
    private transient String passwd;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }

執行結果:

序列化前資料: 
username: hengheng
password: 123456
序列化後資料: 
username: hengheng
password: null

我們發現執行後的結果和預期的不一樣,按理說username也應該變為null才對。是什麼原因呢?

原因是:反序列化後類中static型變數username的值為當前JVM中對應的靜態變數的值,而不是反序列化得出的。

我們來證明一下:

public class TestSerialize {
    public static void main(String[] args) {
        User user = new User();
        user.setUsername("hengheng");
        user.setPasswd("123456");
        System.out.println("序列化前資料: ");
        System.out.println("username: " + user.getUsername());
        System.err.println("password: " + user.getPasswd());
        try {
            ObjectOutputStream os = new ObjectOutputStream(
                    new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
            os.writeObject(user); // 將User物件寫進檔案
            os.flush();
            os.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        User.username = "小明";
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "/Users/admin/Desktop/test/user.txt"));
            user = (User) is.readObject(); // 從流中讀取User的資料
            is.close();
            System.out.println("\n序列化後資料: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
class User implements Serializable {
    public static String username;
    private transient String passwd;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
}

在反序列化前把靜態變數username的值改為『小明』。

User.username = "小明";

再執行一次:

序列化前資料: 
username: hengheng
password: 123456
序列化後資料: 
username: 小明
password: null

果然,這裡的username是JVM中靜態變數的值,並不是反序列化得到的值。

問題三:serialVersionUID有什麼用?

我們經常會在類中自定義一個serialVersionUID:

private static final long serialVersionUID = 8294180014912103005L

這個serialVersionUID有什麼用呢?如果不設定的話會有什麼後果?

serialVersionUID 是一個 private static final long 型 ID,當它被印在物件上時,它通常是物件的雜湊碼。serialVersionUID可以自己定義,也可以自己去生成。

不指定 serialVersionUID的後果是:當你新增或修改類中的任何欄位時,已序列化類將無法恢復,因為新類和舊序列化物件生成的 serialVersionUID 將有所不同。Java 序列化的過程是依賴於正確的序列化物件恢復狀態的,並在序列化物件序列版本不匹配的情況下引發 java.io.InvalidClassException 無效類異常。

舉個例子大家就明白了:

我們保持之前儲存的序列化檔案不變,然後修改User類。

class User implements Serializable {
    public static String username;
    private transient String passwd;
    private String age;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
}

加了一個屬性age,然後單另寫一個反序列化的方法:

public static void main(String[] args) {
        try {
            ObjectInputStream is = new ObjectInputStream(new FileInputStream(
                    "/Users/admin/Desktop/test/user.txt"));
            User user = (User) is.readObject(); // 從流中讀取User的資料
            is.close();
            System.out.println("\n修改User類之後的資料: ");
            System.out.println("username: " + user.getUsername());
            System.err.println("password: " + user.getPasswd());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

在這裡插入圖片描述

報錯了,我們發現之前的User類生成的serialVersionUID和修改後的serialVersionUID不一樣(因為是透過物件的雜湊碼生成的),導致了InvalidClassException異常。

自定義serialVersionUID:

class User implements Serializable {
    private static final long serialVersionUID = 4348344328769804325L;
    public static String username;
    private transient String passwd;
    private String age;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPasswd() {
        return passwd;
    }
    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }
    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
}

再試一下:

序列化前資料: 
username: hengheng
password: 123456
序列化後資料: 
username: 小明
password: null

執行結果無報錯,所以一般都要自定義serialVersionUID。

問題四:是否可以自定義序列化過程?

答案當然是可以的。

之前我們介紹了序列化的第二種方式:

實現Externalizable介面,然後重寫writeExternal() 和readExternal()方法,這樣就可以自定義序列化。

比如我們嘗試把變數設為瞬態。

public class ExternalizableTest implements Externalizable {
    private transient String content = "我是被transient修飾的變數哦";
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(content);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        content = (String) in.readObject();
    }
    public static void main(String[] args) throws Exception {
        ExternalizableTest et = new ExternalizableTest();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                new File("test")));
        out.writeObject(et);
        ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
                "test")));
        et = (ExternalizableTest) in.readObject();
        System.out.println(et.content);
        out.close();
        in.close();
    }
}

執行結果:

我是被transient修飾的變數哦

這裡實現的是Externalizable介面,則沒有任何東西可以自動序列化,需要在writeExternal方法中進行手工指定所要序列化的變數,這與是否被transient修飾無關。

透過上述介紹,是不是對Java序列化有了更多的瞭解?

作者:楊亨

來源:宜信技術學院


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2679384/,如需轉載,請註明出處,否則將追究法律責任。

相關文章