1. 程式人生 > >java中的transient關鍵字詳解

java中的transient關鍵字詳解

目錄

  • 1、何謂序列化?
  • 2、為何要序列化?
  • 3、序列化與transient的使用
  • 4、java類中serialVersionUID作用
  • 5、transient關鍵字小結

前言
說實話學了一段時間java的朋友對於transient這個關鍵字依舊很陌生基本沒怎麼用過,但是transient關鍵字在java中卻起到了不可或缺的地位!如果要說講到,我覺得最可能出現的地方是IO流中物件流(也叫序列化流)的時候會講到!

相信很多人都是直到自己碰到才會關心這個關鍵字,記得博主第一次碰到transient

關鍵字是在閱讀JDK原始碼的時候。在學習java的過程中transient關鍵字少見的原因其實離不開它的作用:transient關鍵字的主要作用就是讓某些被transient關鍵字修飾的成員屬性變數不被序列化。實際上也正是因此,在學習過程中很少用得上序列化操作,一般都是在實際開發中!至於序列化,相信有很多小白童鞋一直迷迷糊糊或者沒有具體的概念,這都不是事,下面博主會很清楚的讓你記住啥是序列化,保證你這輩子忘不了(貌似有點誇張,有點裝b,感覺要被打)

@

1、何謂序列化?

說起序列化,隨之而來的另一個概念就是反序列化,小白童鞋不要慌,記住了序列化就相當於記住了反序列化,因為反序列化就是序列化反過來,所以博主建議只記住序列化概念即可,省的搞暈自己。

專業術語定義的序列化:

Java提供了一種物件序列化的機制。用一個位元組序列可以表示一個物件,該位元組序列包含該物件的資料、物件的型別和物件中儲存的屬性等資訊。位元組序列寫出到檔案之後,相當於檔案中持久儲存了一個物件的資訊。反之,該位元組序列還可以從檔案中讀取回來,重構物件,對它進行反序列化。物件的資料、物件的型別和物件中儲存的資料資訊,都可以用來在記憶體中建立物件。

宜春的術語定義序列化:

序列化: 位元組 ——> 物件

其實,我總結的就是上面的結論,如果不理解,直接參照專業術語的定義,理解之後就記住我的話就行了,記不住,請打死我(我踢m簡直就是個天才)

圖理解序列化:

啥?你不懂啥是位元組?其實,我在一篇IO流的文章裡就已經介紹了序列化,放心,絕對特別詳細~光看文章名字就知道了~

史上最騷最全最詳細的IO流教程,小白都能看懂!

2、為何要序列化?

從上一節提到序列化的概念,知道概念之後,我們就必須要知道 為何要序列化了。

講為何要序列化原因之前,博主我舉個栗子:

就像你去街上買菜,一般操作都是用塑料袋給包裝起來,直到回家要做菜的時候就把菜給拿出來。而這一系列操作就像極了序列化和反序列化!

Java中物件的序列化指的是將物件轉換成以位元組序列的形式來表示,這些位元組序列包含了物件的資料和資訊,一個序列化後的物件 可以被寫到資料庫或檔案中,也可用於 網路傳輸,一般當我們使用 快取cache(記憶體空間不夠有可能會本地儲存到硬碟)或 遠端呼叫rpc(網路傳輸)的時候,經常需要讓我們的實體類實現Serializable介面,目的就是為了讓其可序列化。

  • 在開發過程中要使用transient關鍵字修飾的栗子:

如果一個使用者有一些密碼等資訊,為了安全起見,不希望在網路操作中被傳輸,這些資訊對應的變數就可以加上transient關鍵字。換句話說,這個欄位的生命週期僅存於呼叫者的記憶體中而不會寫到磁盤裡持久化。

  • 在開發過程中不需要transient關鍵字修飾的栗子:

1、類中的欄位值可以根據其它欄位推匯出來。
2、看具體業務需求,哪些欄位不想被序列化;

不知道各位有木有想過為什麼要不被序列化呢?其實主要是為了節省儲存空間。優化程式!

PS:記得之前看HashMap原始碼的時候,發現有個欄位是用transient修飾的,我覺得還是有道理的,確實沒必要對這個modCount欄位進行序列化,因為沒有意義,modCount主要用於判斷HashMap是否被修改(像put、remove操作的時候,modCount都會自增),對於這種變數,一開始可以為任何值,0當然也是可以(new出來、反序列化出來、或者克隆clone出來的時候都是為0的),沒必要持久化其值。

當然,序列化後的最終目的是為了反序列化,恢復成原先的Java物件,要不然序列化後幹嘛呢,就像買菜一樣,用塑料袋包裹最後還是為了方便安全到家再去掉塑料袋,所以序列化後的位元組序列都是可以恢復成Java物件的,這個過程就是反序列化。

3、序列化與transient的使用

 1、需要做序列化的物件的類,必須實現序列化介面:Java.lang.Serializable 介面(一個標誌介面,沒有任何抽象方法),Java 中大多數類都實現了該介面,比如:StringInteger類等,不實現此介面的類將不會使任何狀態序列化或反序列化,會拋NotSerializableException異常 。

  2、底層會判斷,如果當前物件是 Serializable 的例項,才允許做序列化,Java物件 instanceof Serializable 來判斷。

  3、在 Java 中使用物件流ObjectOutputStream來完成序列化以及ObjectInputStream流反序列化   

  1. ==ObjectOutputStream:通過 writeObject()方法做序列化操作== 

  2. ==ObjectInputStream:通過 readObject() 方法做反序列化操作==

4、該類的所有屬性必須是可序列化的。如果有一個屬性不需要可序列化的,則該屬性必須註明是瞬態的,使用transient 關鍵字修飾。

由於位元組嘛所以肯定要涉及流的操作,也就是物件流也叫序列化流ObjectOutputstream,下面進行多種情況分析序列化的操作程式碼!

在這裡,我真的強烈建議看宜春部落格的讀者朋友,請試著去敲,切記一眼帶過或者複製過去執行就完事了,特別是小白童鞋,相信我!你一定會有不一樣的收穫。千萬不要覺得浪費時間,有時候慢就是快,宜春親身體會!

3.1、沒有實現Serializable介面進行序列化情況

package TransientTest;
import java.io.*;

class UserInfo {  //================================注意這裡沒有實現Serializable介面
    private String name;
    private transient String password;

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

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

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

        UserInfo userInfo=new UserInfo("老王","123");
        System.out.println("序列化之前資訊:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt"));
            output.writeObject(new UserInfo("老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

執行結果

3.2、實現Serializable介面序列化情況

當我們加上實現Serializable介面再執行會發現,專案中出現的userinfo.txt檔案內容是這樣的:

其實這都不是重點,重點是序列化操作成功了!

3.3、普通序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable介面
    private String name;
    private String password;//都是普通屬性==============================

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

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

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程式設計師老王","123");
        System.out.println("序列化之前資訊:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程式設計師老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會丟擲ClassNotFoundException
            System.out.println("序列化之後資訊:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

執行結果:

序列化之前資訊:UserInfo{name='程式設計師老王', password='123'}
序列化之後資訊:UserInfo{name='程式設計師老王', password='123'}

3.4、transient序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable介面
    private String name;
    private transient String password; //特別注意:屬性由transient關鍵字修飾===========

    public UserInfo(String name,String psw) {
        this.name = name;
        this.password=psw;
    }

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

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程式設計師老王","123");
        System.out.println("序列化之前資訊:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程式設計師老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會丟擲ClassNotFoundException
            System.out.println("序列化之後資訊:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

執行結果:

序列化之前資訊:UserInfo{name='程式設計師老王', password='123'}
序列化之後資訊:UserInfo{name='程式設計師老王', password='null'}

特別注意結果,新增transient修飾的屬性值為預設值null!如果被transient修飾的屬性為int型別,那它被序列化之後值一定是0,當然各位可以去試試,這能說明什麼呢?說明被標記為transient的屬性在物件被序列化的時候不會被儲存(或者說變數不會持久化)

3.5、static序列化情況

package TransientTest;
import java.io.*;

class UserInfo implements Serializable{  //第一步實現Serializable介面
    private String name;
    private static String password; //特別注意:屬性由static關鍵字修飾==============

    public UserInfo(String name, String psw) {
        this.name = name;
        this.password=psw;
    }

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

public class TransientDemo {
    public static void main(String[] args) throws ClassNotFoundException {

        UserInfo userInfo=new UserInfo("程式設計師老王","123");
        System.out.println("序列化之前資訊:"+userInfo);

        try {
            ObjectOutputStream output=new ObjectOutputStream(new FileOutputStream("userinfo.txt")); //第二步開始序列化操作
            output.writeObject(new UserInfo("程式設計師老王","123"));
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream input=new ObjectInputStream(new FileInputStream("userinfo.txt"));//第三步開始反序列化操作
            Object o = input.readObject();//ObjectInputStream的readObject方法會丟擲ClassNotFoundException
            System.out.println("序列化之後資訊:"+o);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

執行結果:

序列化之前資訊:UserInfo{name='程式設計師老王', password='123'}
序列化之後資訊:UserInfo{name='程式設計師老王', password='123'}

這個時候,你就會錯誤的認為static修飾的也被序列化了,其實不然,實際上這裡很容易被搞暈!明明取出null(預設值)就可以說明不會被序列化,這裡明明沒有變成預設值,為何還要說static不會被序列化呢?

實際上,反序列化後類中static型變數name的值實際上是當前JVM中對應static變數的值,這個值是JVM中的並不是反序列化得出的。也就是說被static修飾的變數並沒有參與序列化!但是咱也不能口說無憑啊,是的,那我們就來看兩個程式對比一下就明白了!

第一個程式:這是一個沒有被static修飾的name屬性程式:

package Thread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserInfo implements Serializable {
    private String name;
    private transient String psw;

    public UserInfo(String name, String psw) {
        this.name = name;
        this.psw = psw;
    }

    public  String getName() {
        return name;
    }

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

    public String getPsw() {
        return psw;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    public String toString() {
        return "name=" + name + ", psw=" + psw;
    }
}
public class TestTransient {
    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("程式設計師老過", "456");
        System.out.println(userInfo);
        try {
            // 序列化,被設定為transient的屬性沒有被序列化
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.txt"));
            o.writeObject(userInfo);
            o.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        try {
            //在反序列化之前改變name的值 =================================注意這裡的程式碼
            userInfo.setName("程式設計師老改");
            // 重新讀取內容
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.txt"));
            UserInfo readUserInfo = (UserInfo) in.readObject();
            //讀取後psw的內容為null
            System.out.println(readUserInfo.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

執行結果:

name=程式設計師老過, psw=456
name=程式設計師老過, psw=null

從程式執行結果中可以看出,在反序列化之前試著改變name的值為程式設計師老改,結果是沒有成功的!

第二個程式:這是一個被static修飾的name屬性程式:

package Thread;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

class UserInfo implements Serializable {
    private static final long serialVersionUID = 996890129747019948L;
    private static String name;
    private transient String psw;

    public UserInfo(String name, String psw) {
        this.name = name;
        this.psw = psw;
    }

    public  String getName() {
        return name;
    }

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

    public String getPsw() {
        return psw;
    }

    public void setPsw(String psw) {
        this.psw = psw;
    }

    public String toString() {
        return "name=" + name + ", psw=" + psw;
    }
}
public class TestTransient {
    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo("程式設計師老過", "456");
        System.out.println(userInfo);
        try {
            // 序列化,被設定為transient的屬性沒有被序列化
            ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("UserInfo.txt"));
            o.writeObject(userInfo);
            o.close();
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        try {
            //在反序列化之前改變name的值
            userInfo.setName("程式設計師老改");
            // 重新讀取內容
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("UserInfo.txt"));
            UserInfo readUserInfo = (UserInfo) in.readObject();
            //讀取後psw的內容為null
            System.out.println(readUserInfo.toString());
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }
}

執行結果:

name=程式設計師老過, psw=456
name=程式設計師老改, psw=null

從程式執行結果中可以看出,在反序列化之前試著改變name的值為程式設計師老改,結果是成功的!現在對比一下兩個程式是不是就很清晰了?

static關鍵字修飾的成員屬性優於非靜態成員屬性載入到記憶體中,同時靜態也優於物件進入到記憶體中,被static修飾的成員變數不能被序列化,序列化的都是物件,靜態變數不是物件狀態的一部分,因此它不參與序列化。所以將靜態變數宣告為transient變數是沒有用處的。因此,反序列化後類中static型變數name的值實際上是當前JVM中對應static變數的值,這個值是JVM中的並不是反序列化得出的。

如果對static關鍵字還是不太清楚理解的童鞋可以參考這篇文章,應該算是不錯的:深入理解static關鍵字

3.6、final序列化情況

對於final關鍵字來講,final變數將直接通過值參與序列化,至於程式碼程式我就不再貼出來了,大家可以試著用final修飾驗證一下!

主要注意的是final 和transient可以同時修飾同一個變數,結果也是一樣的,對transient沒有影響,這裡主要提一下,希望各位以後在開發中遇到這些情況不會滿頭霧水!

4、java類中serialVersionUID作用

既然提到了transient關鍵字就不得不提到序列化,既然提到了序列化,就不得不提到serialVersionUID了,它是啥呢?基本上有序列化就會存在這個serialVersionUID。

serialVersionUID適用於Java的序列化機制。簡單來說,Java的序列化機制是通過判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體類的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是InvalidCastException,在開發中有時候可寫可不寫,建議最好還是寫上比較好。

5、transient關鍵字小結

1、變數被transient修飾,變數將不會被序列化
2、transient關鍵字只能修飾變數,而不能修飾方法和類。
3、被static關鍵字修飾的變數不參與序列化,一個靜態static變數不管是否被transient修飾,均不能被序列化。
4、final變數值參與序列化,final transient同時修飾變數,final不會影響transient,一樣不會參與序列化

第二點需要注意的是:本地變數是不能被transient關鍵字修飾的。變數如果是使用者自定義類變數,則該類需要實現Serializable介面

第三點需要注意的是:反序列化後類中static型變數的值實際上是當前JVM中對應static變數的值,這個值是JVM中的並不是反序列化得出的。

結語:被transient關鍵字修飾導致不被序列化,其優點是可以節省儲存空間。優化程式!隨之而來的是會導致被transient修飾的欄位會重新計算,初始化!

如果本文對你有一點點幫助,那麼請點個讚唄,謝謝~

若有不足或者不正之處,歡迎指正批評,感激不盡!如果有疑問歡迎留言,絕對第一時間回覆!

最後,歡迎各位關注宜春的公眾號,一起探討技術,嚮往技術,追求技術,說好了來了就是盆友喔...