transient關鍵字和serialVersionUID

此文章很大部分轉載於Java的架構師技術棧微信公衆號,博主均測試經過加上本身理解寫出java

 

最近閱讀java集合的源碼,發現transient關鍵字,就瞭解了一下他的用法,transient關鍵字通常在實現Serializable接口的類中出現.以下:微信

 

 

 

1、初識transient關鍵字

其實這個關鍵字的做用很好理解,一句話:將不須要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會被序列化網絡

下面使用代碼去驗證一下:User類實現序列化接口,在age屬性前加上該關鍵字架構

public class User implements Serializable {
    private static final long serialVersionUID = 123456L;
    private transient int age;
    private String name;

    //getter和setter方法
    //toString方法
}

而後咱們在Test中去驗證一下:ide

public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        DeSerializeUser();
    }
    //序列化
    private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
        User user = new User();
        user.setName("Java的架構師技術棧");
        user.setAge(24);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt"));
        oos.writeObject(user);
        oos.close();
        System.out.println("添加transient關鍵字序列化age= "+user.getAge());
    }
    //反序列化
    private static void DeSerializeUser() throws IOException, ClassNotFoundException {
        File file = new File("D://Test/template.txt");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        System.out.println("添加了transient關鍵字反序列化:age= "+newUser);
    }

看一下結果:測試

 

 

 從上面的這張圖能夠看出,age屬性變爲了0,說明被transient關鍵字修飾以後沒有被序列化。this

2、深刻分析transient關鍵字

爲了更加深刻的去分析transient關鍵字,咱們須要帶着幾個問題去解讀:spa

(1)transient底層實現的原理是什麼?線程

(2)被transient關鍵字修飾過得變量真的不能被序列化嘛?3d

(3)靜態變量能被序列化嗎?被transient關鍵字修飾以後呢?

帶着這些問題一個一個來解決:

一、transient底層實現原理是什麼?

java的serialization提供了一個很是棒的存儲對象狀態的機制,說白了serialization就是把對象的狀態存儲到硬盤上 去,等須要的時候就能夠再把它讀出來使用。有些時候像銀行卡號這些字段是不但願在網絡上傳輸的,transient的做用就是把這個字段的生命週期僅存於調用者的內存中而不會寫到磁盤裏持久化,意思是transient修飾的age字段,他的生命週期僅僅在內存中,不會被寫到磁盤中。

二、被transient關鍵字修飾過得變量真的不能被序列化嘛?

想要解決這個問題,首先還要再重提一下對象的序列化方式:

Java序列化提供兩種方式。

- 一種是實現Serializable接口
- 另外一種是實現Exteranlizable接口。須要重寫writeExternal和readExternal方法,它的效率比Serializable高一些,而且能夠決定哪些屬性須要序列化(即便是transient修飾的),可是對大量對象,或者重複對象,則效率低。

從上面的這兩種序列化方式,我想你已經看到了,使用Exteranlizable接口實現序列化時,咱們本身指定那些屬性是須要序列化的,即便是transient修飾的。下面就驗證一下

首先咱們定義User1類:這個類是被Externalizable接口修飾的

public class User1 implements Externalizable{
    private transient String name;
   //getter、setter、toString方法
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
    }
    @Override
    public void readExternal(ObjectInput in) throws IOException {
        name = (String) in.readObject();
    }
}

而後咱們就能夠測試了

public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        DeSerializeUser();
    }
    //序列化
    private static void SerializeUser() throws FileNotFoundException, IOException {
        User1 user = new User1();
        user.setName("Java的架構師技術棧");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://template"));
        oos.writeObject(user);
        oos.close();
        System.out.println("使用Externalizable接口,"+ "添加了transient關鍵字序列化以前:"+user);
    }
    //反序列化
    private static void DeSerializeUser() throws IOException, ClassNotFoundException {
        File file = new File("D://template");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User1 newUser = (User1)ois.readObject();
        System.out.println("使用Externalizable接口,"+ " 添加了transient關鍵字序列化以後:"+newUser);
    }

上面,代碼分了兩個方法,一個是序列化,一個是反序列化。裏面的代碼和一開始給出的差很少,只不過,User1裏面少了age這個屬性。

而後看一下結果:

 

 

 結果驗證了咱們的猜測,也就是說,實現了Externalizable接口,哪個屬性被序列化使咱們手動去指定的,即便是transient關鍵字修飾也不起做用。

三、靜態變量能被序列化嗎?沒被transient關鍵字修飾以後呢?

這個我能夠提早先告訴結果,靜態變量是不會被序列化的,即便沒有transient關鍵字修飾。下面去驗證一下,而後再解釋緣由。

首先,在User類中對age屬性添加transient關鍵字和static關鍵字修飾。

public class User2 implements Serializable {
    private static final long serialVersionUID = 12345L;
    private static transient int age;
    private String name;

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

 

而後,在Test類中去測試

public class Test2 {
    public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        DeSerializeUser();
    }
    //序列化
    private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
        User2 user = new User2();
        user.setName("Java的架構師技術棧");
        //序列化以前靜態變量age年齡是24.
        user.setAge(24);
        //將數據24寫入磁盤
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://template"));
        oos.writeObject(user);
        oos.close();
        //再讀取,經過getAge()打印新的值
        System.out.println("static、transient關鍵字修飾age以前:"+user.getAge());
        //如今把年齡改爲18,此時18只存在內存
        user.setAge(18);
    }
    //反序列化
    private static void DeSerializeUser() throws IOException, ClassNotFoundException {
        File file = new File("D://template");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User2 newUser = (User2)ois.readObject();
        System.out.println("使用Externalizable接口,"+ " 添加了transient關鍵字序列化以後:"+newUser);
    }
}

最後,看看結果

 

 

 

結果已經很明顯了。如今解釋一下,爲何會是這樣,其實在前面已經提到過了。由於靜態變量在全局區,原本流裏面就沒有寫入靜態變量,我打印靜態變量固然會去全局區查找,而咱們的序列化是寫到磁盤上的,因此JVM查找這個靜態變量的值,是從全局區查找的,而不是磁盤上。user.setAge(18);年齡改爲18以後,被寫到了全局區,其實就是方法區,只不過被全部的線程共享的一塊空間。所以能夠總結一句話:

靜態變量不論是不是transient關鍵字修飾,都不會被序列化

3、transient關鍵字總結

java 的transient關鍵字爲咱們提供了便利,你只須要實現Serilizable接口,將不須要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。像銀行卡、密碼等等這些數據。這個須要根據業務狀況了。

 

四.serialVersionUID

serialVersionUID適用於Java的序列化機制。簡單來講,Java的序列化機制是經過判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,若是相同就認爲是一致的,能夠進行反序列化,不然就會出現序列化版本不一致的異常,便是InvalidCastException

具體的序列化過程是這樣的:序列化操做的時候系統會把當前類的serialVersionUID寫入到序列化文件中,當反序列化時系統會去檢測文件中的serialVersionUID,判斷它是否與當前類的serialVersionUID一致,若是一致就說明序列化類的版本與當前類版本是同樣的,能夠反序列化成功,不然失敗。

下面咱們測試一下 serialVersionUID = 123456L;

public class User implements Serializable {
    private static final long serialVersionUID = 123456L;
    private transient int age;
    private String name;

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

序列化到磁盤

public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        //DeSerializeUser();
    }
    //序列化
    private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
        User user = new User();
        user.setName("Java的架構師技術棧");
        user.setAge(24);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt"));
        oos.writeObject(user);
        oos.close();
        System.out.println("添加transient關鍵字序列化age= "+user.getAge());
    }

看下結果

 

能夠看到,成功了,如今咱們修改一下serialVersionUID = 12345L;

 

 

public class User implements Serializable {
    private static final long serialVersionUID = 12345L;
    private transient int age;
    private String name;

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

再反序列試一下

public static void main(String[] args) throws Exception, IOException {
        //SerializeUser();
        DeSerializeUser();
    }
    //序列化
    private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
        User user = new User();
        user.setName("Java的架構師技術棧");
        user.setAge(24);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt"));
        oos.writeObject(user);
        oos.close();
        System.out.println("添加transient關鍵字序列化age= "+user.getAge());
    }
    //反序列化
    private static void DeSerializeUser() throws IOException, ClassNotFoundException {
        File file = new File("D://Test/template.txt");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        System.out.println("添加了transient關鍵字反序列化:age= "+newUser);
    }

運行,看結果

 

 

 出現了序列化版本不一致的異常,便是InvalidCastException

 

當實現java.io.Serializable接口的類沒有顯式地定義一個serialVersionUID變量時候,Java序列化機制會根據編譯的Class自動生成一個serialVersionUID做序列化版本比較用,

serialVersionUID註釋掉,去除transient關鍵字

測試一下:

public class User implements Serializable {
    //private static final long serialVersionUID = 12345L;
    private int age;
    private String name;

    /*public static long getSerialVersionUID() {
        return serialVersionUID;
    }*/

    public int getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

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

測試代碼以下:

public class Test {
    public static void main(String[] args) throws Exception, IOException {
        SerializeUser();
        DeSerializeUser();
    }
    //序列化
    private static void SerializeUser() throws FileNotFoundException, IOException, ClassNotFoundException {
        User user = new User();
        user.setName("Java的架構師技術棧");
        user.setAge(24);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://Test/template.txt"));
        oos.writeObject(user);
        oos.close();
        System.out.println("添加transient關鍵字序列化age= "+user.getAge());
    }
    //反序列化
    private static void DeSerializeUser() throws IOException, ClassNotFoundException {
        File file = new File("D://Test/template.txt");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        User newUser = (User)ois.readObject();
        System.out.println("添加了transient關鍵字反序列化:age= "+newUser);
    }
}

運行,看一下結果

能夠看到,序列化與反序列化都很正常

這種狀況下,若是Class文件(類名,方法明等)沒有發生變化(增長空格,換行,增長註釋等等),就算再編譯屢次,serialVersionUID也不會變化的。

 

若是咱們不但願經過編譯來強制劃分軟件版本,即實現序列化接口的實體可以兼容先前版本,就須要顯式地定義一個名爲serialVersionUID,類型爲long的變量,不修改這個變量值的序列化實體均可以相互進行串行化和反串行化。

相關文章
相關標籤/搜索