第七十五條 考慮使用自定義的序列化形式
序列化使用起來比價方便,但有一些常見的細節需要注意,比如說定義 serialVersionUID 值,關鍵字 transient 的用法,下面就用例子來說明
定義一個bean,實現序列化的介面,
public class Student implements Serializable {
int age;
String address;
public Student(int age, String address) {
this.age = age;
this.address = address;
}
}
在main中執行序列化寫入本地的方法
static final String PATH = "e:/data.txt";
public static void main(String[] args) throws Exception {
write();
}
private static void write() throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
PATH));
Student student = new Student(25, "中國");
oos.writeObject(student);
oos.close();
}
執行過後,發現電腦E盤多了個文字檔案,開啟txt文字,裡面內容為 sr -com.example.cn.desigin.utils.JavaTest$Student I ageL addresstLjava/lang/String;xp t 涓浗,說明把物件以位元組流的形式存在了文字中。我們再反序列化一下,看看能否還原成物件,執行以下程式碼
private static void read() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
PATH));
Student student = (Student) ois.readObject();
System.out.println("age=" + student.age + ";address=" + student.address);
ois.close();
}
打印出的內容為 age=25;address=中國,說明反序列化成功。這樣寫,看似沒問題,實際上有隱患。如果我們的 Student 類,以後不會做任何屬性的擴充套件,也不會在裡面新增空格之類的,總之就是不會再去修改這個類,連個空格都不加之類的,那麼可以這樣寫;如果不敢保證,比如說肯能再擴充套件一個 性別 的屬性,那麼一旦 Student 的類變化了,E盤中txt文字內容反序列化的時候,就會出錯了。那麼怎麼辦呢?這時候 serialVersionUID 就登場了,我們在 Student 中宣告它就可以了,private static final long serialVersionUID = 1L; 或者讓系統自動生成它的值,在我的電腦上是 private static final long serialVersionUID = 6392945738859063583L;
public class Student implements Serializable {
private static final long serialVersionUID = 6392945738859063583L;
int age;
String address;
int sex;
public Student(int age, String address, int sex) {
this.age = age;
this.address = address;
this.sex = sex;
}
}
如此,序列化文字中沒有這個屬性的值時,反序列化以後,值時預設值,String 型別為 null, int 型別為 0 ,依次類推。
預設的序列化會把所有屬性全都記錄到文字中,如果說Student中,如果我們不想把 address 屬性序列化怎麼辦?一種方法是儲存字串,把物件通過 Gson 等第三方工具類把物件轉換為json 型別的字串,然後把 address 屬性及對應的值刪掉,json串支援刪除節點的功能,然後儲存字串,使用的時候取出字串,然後再通過 Gson 轉換為物件。這種方法繁瑣但比較保險,它支援物件Student 的包名字的變換及類名的變化,缺點是比較繁瑣,總之如果你的bean物件經常變化包名的話,這是一個不錯的方法,如果bean是萬年位置不變的話,可以用第二種方法。第二種方法就是序列化提供的關鍵字 transient ,哪個屬性不需要被序列化就用它來修飾即可,比如
public class Student implements Serializable {
private static final long serialVersionUID = 6392945738859063583L;
int age;
transient String address;
public Student(int age, String address) {
this.age = age;
this.address = address;
}
}
序列化文字內容為 sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I agexp ,反序列化以後,列印物件值為 age=25;address=null, 如此,證明此方案可以。以上是預設的序列化,即系統給咱們預設的道路,按照這條路走就可以了。如果你不想走常規路,或者預設的路滿足不了你們公司的需求,那麼可以自定義序列化格式,形成自己的定製版。想自己定製,成為自己的定製版,那麼只需要編寫 writeObject 和 readObject 方法即可,還以 Student 為例,如下
public class Student implements Serializable {
private static final long serialVersionUID = 6392945738859063583L;
int age;
String address;
public Student(int age, String address) {
this.age = age;
this.address = address;
}
//JAVA BEAN自定義的writeObject方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeInt(age);
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
this.age = in.readInt();
this.address = in.readObject().toString();
}
}
執行後,儲存到本地的序列化的值為 sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I ageL addresst Ljava/lang/String;xpw t 涓浗x, 反序列後列印的物件的值為 age=25;address=中國。 如果只想序列化 age 屬性,那麼不要把 address 寫入即可, 把 out.writeObject(address); 和 this.address = in.readObject().toString(); 這兩行程式碼註釋掉即可,序列化的值為 sr -com.example.cn.desigin.utils.JavaTest$StudentX窷G9? I ageL addresst Ljava/lang/String;xpw x, 反序列化物件值為 age=25;address=null,這是第三種不想序列化某個屬性的方法,定製版。
使用定製版需要注意些事項,writeObject 和 readObject 方法中, write 和 read 物件屬性時,一定要對上順序,順序不能錯亂,否則就錯了。
下面稍微講一下原理,我們發現,Student 物件的父類是 Object,裡面沒有 writeObject(ObjectOutputStream out)方法,那麼Student 中的這個方法就不是重寫了,怎麼回事呢?一步步看吧, 我們呼叫 oos.writeObject(student); 方法,看一下原始碼
public final void writeObject(Object object) throws IOException {
writeObject(object, false);
}
這個方法,會呼叫 writeObjectInternal(object, unshared, true, true); 方法,把 student 引用繼續往下傳, 這個方法有兩行比較關鍵的程式碼,
Class<?> objClass = object.getClass();
ObjectStreamClass clDesc = ObjectStreamClass.lookupStreamClass(objClass);
看看靜態方法 ,裡面用到了Map快取技術,
static ObjectStreamClass lookupStreamClass(Class<?> cl) {
WeakHashMap<Class<?>, ObjectStreamClass> tlc = getCache();
ObjectStreamClass cachedValue = tlc.get(cl);
if (cachedValue == null) {
cachedValue = createClassDesc(cl);
tlc.put(cl, cachedValue);
}
return cachedValue;
}
看一下 createClassDesc(cl) 方法中的關鍵程式碼
private static ObjectStreamClass createClassDesc(Class<?> cl) {
ObjectStreamClass result = new ObjectStreamClass();
result.methodWriteReplace = findMethod(cl, "writeReplace");
result.methodReadResolve = findMethod(cl, "readResolve");
result.methodWriteObject = findPrivateMethod(cl, "writeObject", WRITE_PARAM_TYPES);
result.methodReadObject = findPrivateMethod(cl, "readObject", READ_PARAM_TYPES);
result.methodReadObjectNoData = findPrivateMethod(cl, "readObjectNoData", EmptyArray.CLASS);
if (result.hasMethodWriteObject()) {
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}
result.setFlags(flags);
return result;
}
可看到這就明白了,原來是通過反射來檢查 bean 中是否有重寫這幾個方法,通過反射來呼叫方法,所以自定義序列化時,我們自己寫這兩個方法,而不是重寫,因為父類沒有。
細心的同學會發現,下面的方法有所不同,
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.age = in.readInt();
this.address = in.readObject().toString();
}
序列化時,多了個 out.defaultWriteObject(); 方法, 反序列化時,多了個 in.defaultReadObject(); 方法,那麼這兩個方法是幹嘛用的呢?很明顯,它們倆是對應著的,在下對這一塊也不是很瞭解,按照個人的體會,這兩個方法是相對的,要麼都存在,要麼都不存在; defaultWriteObject() 和 defaultReadObject() 是系統預設的序列化, out.writeInt(age); out.writeObject(address);這個是自己自定義的,可以理解為 他們是 父類和子類 方法中的關係,相同於實現父類方法同時,又擴充套件了子類的方法,如果 defaultWriteObject() 和自定義序列化中同時操作了 age的值,例如
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age + 10);
out.writeObject(address);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
this.age = in.readInt();
this.address = in.readObject().toString();
this.age = age - 1;
}
按照 Student student = new Student(23, "中國"); oos.writeObject(student); 此時,自定義為準,比如傳入的age是23,out.defaultWriteObject();對應的就是23,但我們自定義時,把age的值增加了10,變為33,然後序列化,此時序列化本地文字中的值是 33; 然後反序列化時, in.defaultReadObject(); 讀出來的是 33 ,在下面有減去了1,即置為 32。
執行結果, 是 age:32 address:中國 。