面試官:告訴我為什麼static和transient關鍵字修飾的變數不能被序列化?

JavaBuild發表於2024-06-23

一、寫在開頭

在上一篇學習序列化的文章中我們提出了這樣的一個問題:

“如果在我的物件中,有些變數並不想被序列化應該怎麼辦呢?”

當時給的回答是:不想被序列化的變數我們可以使用transientstatic關鍵字修飾;transient 關鍵字的作用是阻止例項中那些用此關鍵字修飾的的變數序列化;當物件被反序列化時,被 transient 修飾的變數值不會被持久化和恢復;而static關鍵字修飾的變數並不屬於物件本身,所以也同樣不會被序列化!

當時沒有解釋具體為什麼static和transient 關鍵字修飾的變數就不能被序列化了,這個問題實際上在很多大廠的面試中都可能會被問及。我們今天在這篇中進行解釋吧。

二、案例演示

我們先透過一個實戰案例,去看一看用static和transient 關鍵字修飾後的變數,序列化與反序列化後的現象。

public class TestService {
    public static void main(String[] args) throws IOException {
        //初始化物件資訊
        Person person = new Person();
        person.setName("JavaBuild");
        person.setAge(30);
        System.out.println(person.getName()+" "+person.getAge());

        //序列化過程
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
            objectOutputStream.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
        person.par1 = "序列化後靜態欄位";
        //反序列化過程
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
            Person p = (Person) objectInputStream.readObject();
            System.out.println(p);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}
class Person implements Serializable{

    private static final long serialVersionUID = 8711922740433840551L;
    private String name;
    private int age;

    public static String par1 = "靜態欄位";
    transient String par2 = "臨時欄位";
    transient int high = 175;

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

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

在Person類中,我們定義了兩個正常的屬性,姓名與年齡,同時呢,我們也分別定義了一個靜態欄位和兩個臨時欄位,輸出結果為:

JavaBuild 30
Person{name='JavaBuild', age=30, par1=序列化後靜態欄位, high=0, par2='null'}

對於使用static關鍵字修飾的par1來說,在整個序列化過程中,它並未參與,原因是:我們在序列化與反序列化之間插入了屬性的重新賦值操作,最後輸出中列印出的是最新賦值,說明僅是呼叫了例項物件的屬性值,而不是反序列化的結果。

而對於transient 關鍵字修飾high和par2,在序列化時直接被忽略了。從輸出結果看就更加的明瞭了,int型別直接還原為預設值0,而String型別直接為null。

什麼原因呢?咱們繼續往下看。

三、原始碼分析

在之前的文章中,我們已經解釋過了,在序列化時Serializable只是作為一種標識介面,告訴程式我這個物件需要序列化,那麼真正的實現還要以來序列化流,比如寫出到檔案時,我們需要用到的ObjectOutputStream,它在序列化的時候會依次呼叫 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。

然後最後一步的defaultWriteFields()方法中,會去呼叫ObjectStreamClass物件,裡面有個方法為getDefaultSerialFields(),提供了可以被序列化的屬性值。

private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
    // 獲取該類中宣告的所有欄位
    Field[] clFields = cl.getDeclaredFields();
    ArrayList<ObjectStreamField> list = new ArrayList<>();
    int mask = Modifier.STATIC | Modifier.TRANSIENT;

    // 遍歷所有欄位,將非 static 和 transient 的欄位新增到 list 中
    for (int i = 0; i < clFields.length; i++) {
        Field field = clFields[i];
        int mods = field.getModifiers();
        if ((mods & mask) == 0) {
            // 根據欄位名、欄位型別和欄位是否可序列化建立一個 ObjectStreamField 物件
            ObjectStreamField osf = new ObjectStreamField(field.getName(), field.getType(), !Serializable.class.isAssignableFrom(cl));
            list.add(osf);
        }
    }

    int size = list.size();
    // 如果 list 為空,則返回一個空的 ObjectStreamField 陣列,否則將 list 轉換為 ObjectStreamField 陣列並返回
    return (size == 0) ? NO_FIELDS :
        list.toArray(new ObjectStreamField[size]);
}

這段原始碼中,定義一個mask標記變數,用於接收訪問修飾符中包含STATIC與TRANSIENT的屬性,並在後面的if判斷中,將這種mask的過濾掉,從而實現遍歷所有欄位,將非 static 和 transient 的欄位新增到 list 中。

而這段原始碼就證明了,為什麼在物件序列化過程中,static和transient不會被序列化!

四、總結

好啦,今天針對為什麼static和transient關鍵字修飾的變數不能被序列化進行了一個解釋,下次大家在面試的時候再被問道就可以這樣回答啦,不過,還有的BT面試官會問transient關鍵字修飾的變數真的不能被序列化嗎?這個問題咱們後面繼續討論哈。

相關文章