一、寫在開頭
在上一篇學習序列化的文章中我們提出了這樣的一個問題:
“如果在我的物件中,有些變數並不想被序列化應該怎麼辦呢?”
當時給的回答是:不想被序列化的變數我們可以使用transient
或static
關鍵字修飾;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關鍵字修飾的變數真的不能被序列化嗎?這個問題咱們後面繼續討論哈。