transient關鍵字作用是什么


1.從Serilizable說到transient

我們知道,如果一個對象需要序列化,那么需要實現Serilizable接口,那么這個類的所有非靜態屬性,都會被序列化。

注意:上面說的是非靜態屬性,因為靜態屬性是屬於類的,而不是屬於類對象的,而序列化是針對類對象的操作,所以這個根本不會序列化。下面我們可以實驗一下:
實體類Teacher.class:

import java.io.Serializable;

class Teacher implements Serializable {
    public int age;
    public static String schoolName;

    public Teacher(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "age=" + age +
                '}';
    }
}

測試代碼SerialTest.java,基本思路就是初始化的時候,靜態屬性schoolName為"東方小學",序列化對象之后,將靜態屬性修改,然后,反序列化,發現其實靜態變量還是修改之后的,說明靜態變量並沒有被序列化。

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        Teacher.schoolName = "東方小學";
        serial();
        Teacher.schoolName = "西方小學";
        deserial();
        System.out.println(Teacher.schoolName);
    }
    // 序列化
    private static void serial(){
        try {
            Teacher teacher = new Teacher(9);
            FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
            ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(teacher);
            objectOutputStream.flush();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
    // 反序列化
    private static void deserial() {
        try {
            FileInputStream fis = new FileInputStream("Teacher.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            Teacher teacher = (Teacher) ois.readObject();
            ois.close();
            System.out.println(teacher.toString());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

輸出的結果,證明靜態變量沒有被序列化!!!

Teacher{age=9}
西方小學

2.序列化屬性對象的類需要實現Serilizable接口?

突然想到一個問題,如果有些屬性是對象,而不是基本類型,需不需要改屬性的類型也實現Serilizable呢?

問題的答案是:需要!!!

下面是實驗過程:

首先,有一個Teacher.java,實現了Serializable,里面有一個屬性是School類型:

import java.io.Serializable;

class Teacher implements Serializable {
    public int age;
    public School school;
  
    public Teacher(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "age=" + age +
                '}';
    }
}

School類型,不實現Serializable:

public class School {
    public String name;

    public School(String name) {
        this.name = name;
    }

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

測試代碼,我們只測試序列化:

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        serial();
    }

    private static void serial(){
        try {
            Teacher teacher = new Teacher(9);
            teacher.school = new School("東方小學");
            FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
            ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(teacher);
            objectOutputStream.flush();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

會發現報錯了,報錯的原因是:School不能被序列化,也就是沒有實現序列化接口,所以如果我們想序列化一個對象,那么這個對象的屬性也必須是可序列化的,或者它是transient修飾的。

java.io.NotSerializableException: com.aphysia.transienttest.School
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at com.aphysia.transienttest.SerialTest.serial(SerialTest.java:18)
    at com.aphysia.transienttest.SerialTest.main(SerialTest.java:9)

當我們將School實現序列化接口的時候,發現一切就正常了...問題完美解決

3.不想被序列化的字段怎么辦?

但是如果有一個變量不是靜態變量,但是我們也不想序列化它,因為它可能是一些密碼等敏感的字段,或者它是不那么重要的字段,我們不希望增加報文大小,所以想在序列化報文中排除該字段。或者改字段存的是引用地址,不是真正重要的數據,比如ArrayList里面的elementData

這個時候就需要使用transient 關鍵字,將改字段屏蔽。

當我們用transient修飾School的時候:

import java.io.Serializable;

class Teacher implements Serializable {
    public int age;
    public transient School school;
    public Teacher(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "age=" + age +
                ", school=" + school +
                '}';
    }
}
import java.io.Serializable;

public class School implements Serializable {
    public String name;

    public School(String name) {
        this.name = name;
    }

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

執行下面序列化和反序列化的代碼:

import java.io.*;

public class SerialTest {
    public static void main(String[] args) {
        serial();
        deserial();
    }

    private static void serial(){
        try {
            Teacher teacher = new Teacher(9);
            teacher.school = new School("東方小學");
            FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
            ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(teacher);
            objectOutputStream.flush();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    private static void deserial() {
        try {
            FileInputStream fis = new FileInputStream("Teacher.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            Teacher teacher = (Teacher) ois.readObject();
            ois.close();
            System.out.println(teacher.toString());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

執行結果如下,可以看到School字段反序列化出來,其實是null,這也是transient起作用了。
但是注意,transient只能修飾變量,但是不能修飾類和方法,

Teacher{age=9,school=null}

4.ArrayList里面的elementData都被transient 關鍵字修飾了,為什么ArrayList還可以序列化呢?

這里提一下,既然transient修飾了ArrayList的數據節點,那么為什么序列化的時候我們還是可以看到ArrayList的數據節點呢?
這是因為序列化的時候:

如果僅僅實現了Serializable接口,那么序列化的時候,肯定是調用java.io.ObjectOutputStream.defaultWriteObject()方法,將對象序列化。然后如果是transient修飾了該屬性,肯定該屬性就不能序列化。
但是,如果我們雖然實現了Serializable接口,也transient修飾了該屬性,該屬性確實不會在默認的java.io.ObjectOutputStream.defaultWriteObject()方法里面被序列化了,但是ArrayList重寫一個writeObject()方法,這樣一來,序列化的時候調用的就是ArrayList重寫的writeObject()方法,而不是java.io.ObjectOutputStream.defaultWriteObject()的方法了。

下面的源碼是ObjectOutputStream.writeObject(Object obj),里面底層其實會有反射的方式調用到重寫的對象的writeObject()方法,這里不做展開。

    public final void writeObject(Object obj) throws IOException {
        // 如果可以被重寫,那么就會調用重寫的方法
        if (enableOverride) {
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }

ArrayList重寫的writeOject()方法如下:

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        // 調用默認的序列化對象的方法,實現常規的序列化操作
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // 此時elementData在默認序列化方法中沒有被序列化,單獨對elementData中的元素進行writeObject操作
        for (int i=0; i<size; i++) {
            // 序列化對象的值
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

我們可以看到,writeOject()里面其實在里面調用了默認的方法defaultWriteObject()defaultWriteObject()底層其實是調用改了writeObject0()方法。ArrayList重寫的writeOject()的思路主要是先序列化默認的,然后序列化數組大小,再序列化數組elementData里面真實的元素。這就達到了序列化元素真實內容的目的。

/**
     * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

ArrayList的反序列化操作入上面代碼所示。先調用defaultReadObject()進行默認的反序列化操作,對於之前writeOject()寫入的elementData中的真實元素,在for循環中挨個讀取出來,實現了elementData的反序列化,因此elementData雖然使用了transient關鍵字修飾,但是因為重寫了writeOject()和readObject()方法,所以最終還是實現了elementData屬性的序列化和反序列化。

同理,借用ArrayList的實現邏輯,可以實現帶有transient關鍵字的屬性的序列化和反序列化,下面構造一個簡單的實體類

class TransientTest implements Serializable {
    private static final long serialVersionUID = 233858934995755239L;
    private String name1;
    private transient String name2;

    public TransientTest(String name1, String name2) {
        this.name1 = name1;
        this.name2 = name2;
    }

    public String toString() {
        return String.format("TransientTest.toString(): name1=%s,name2=%s", name1, name2);
    }

    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.defaultWriteObject();
        s.writeObject(name2);
    }

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        name2 = String.valueOf(s.readObject());
    }

    private static void serial() {
        try {
            TransientTest teacher = new TransientTest("常規屬性", "transient修飾的屬性");
            System.out.println("序列化前:" + teacher.toString());
            FileOutputStream fileOutputStream = new FileOutputStream("Teacher.txt");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(teacher);
            objectOutputStream.flush();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    private static void deserial() {
        try {
            FileInputStream fis = new FileInputStream("Teacher.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            TransientTest teacher = (TransientTest) ois.readObject();
            ois.close();
            System.out.println("反序列化:" + teacher.toString());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        serial();
        deserial();
    }
}

結果如下:

序列化前:TransientTest.toString(): name1=常規屬性,name2=transient修飾的屬性
反序列化:TransientTest.toString(): name1=常規屬性,name2=transient修飾的屬性

5.除了transient,有沒有其他的方式,可以屏蔽屬性的反序列化?

且慢,問出這個問題,答案肯定是有的!!!那就是Externalizable接口。

具體情況:Externalizable意思就是,類里面有很多很多屬性,但是我只想要一部分,要屏蔽大部分,那么我不想在大部分的屬性前面加關鍵字transient,我只想標識一下自己序列化的字段,這個時候就需要使用Externalizable接口。

首先定義一個Person.java,里面有三個屬性

  • age:年齡
  • name:名字(被transient修飾)
  • score:分數

實現了Externalizable接口,就必須實現writeExternal()readExternal()方法。

  • writeExternal:將需要序列化的屬性進行自定義序列化
  • readExternal:將需要反序列化的屬性進行自定義反序列化
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable {
    public int age;
    public transient String name;
    public int score;

    // 必須實現無參構造器
    public Person() {
    }

    public Person(int age, String name, int score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        /*
         * 指定序列化時候寫入的屬性。這里不寫入score
         */
        out.writeObject(age);
        out.writeObject(name);
        out.writeObject(score);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        /*
         * 指定序列化時候寫入的屬性。這里仍然不寫入年齡
         */
        this.age = (int)in.readObject();
        this.name = (String)in.readObject();
    }

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

上面的代碼,我們可以看出,序列化的時候,將三個屬性都寫進去了,但是反序列化的時候,我們僅僅還原了兩個,那么我們來看看測試的代碼:

import java.io.*;

public class ExternalizableTest {
    public static void main(String[] args) {
        serial();
        deserial();
    }

    private static void serial(){
        try {
            Person person = new Person(9,"Sam",98);
            FileOutputStream fileOutputStream = new FileOutputStream("person.txt");
            ObjectOutputStream objectOutputStream= new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(person);
            objectOutputStream.flush();
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    private static void deserial() {
        try {
            FileInputStream fis = new FileInputStream("person.txt");
            ObjectInputStream ois = new ObjectInputStream(fis);
            Person person = (Person) ois.readObject();
            ois.close();
            System.out.println(person);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

測試結果如下,就可以發現其實前面兩個都反序列化成功了,后面那個是因為我們重寫的時候,沒有自定義該屬性的反序列化,所以沒有是正常的啦...

Person{age=9, name='Sam', score='0'}

如果細心點,可以發現,有一個字段是transient修飾的,不是說修飾了,就不會被序列化么,怎么序列化出來了。

沒錯,只要實現了Externalizable接口,其實就不會被transient左右了,只會按照我們自定義的字段進行序列化和反序列化,這里的transient是無效的...

站在巨人肩膀上摘蘋果

https://www.jianshu.com/p/31c77d833a8e

https://blog.csdn.net/u010188178/article/details/83581506


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM