引言
在持久化資料對象的時候我們很少使用Java序列化,而是使用資料庫等方式來實作。但是在我看來,Java 序列化是一個很重要的内容,序列化不僅可以儲存對象到磁盤進行持久化,還可以通過網絡傳輸。在平時的面試當中,序列化也是經常被談及的一塊内容。
談到序列化時,大家可能知道将類實作Serializable接口就可以達到序列化的目的,但當看到關于序列化的面試題時我們卻常常一臉懵逼。
1)可序列化接口和可外部接口的差別是什麼?
2)序列化時,你希望某些成員不要序列化?該如何實作?
3)什麼是 serialVersionUID ?如果不定義serialVersionUID,會發生什麼?
是不是突然發現我們對這些問題其實都還存在很多疑惑?本文将總結一些Java序列化的常見問題,并且通過demo來進行測試和解答。
問題一:什麼是 Java 序列化?
序列化是把對象改成可以存到磁盤或通過網絡發送到其它運作中的 Java 虛拟機的二進制格式的過程,并可以通過反序列化恢複對象狀态。Java 序列化API給開發人員提供了一個标準機制:通過實作 java.io.Serializable 或者 java.io.Externalizable 接口,ObjectInputStream 及ObjectOutputStream 處理對象序列化。實作java.io.Externalizable 接口的話,Java 程式員可自由選擇基于類結構的标準序列化或是它們自定義的二進制格式,通常認為後者才是最佳實踐,因為序列化的二進制檔案格式成為類輸出 API的一部分,可能破壞 Java 中私有和包可見的屬性的封裝。
序列化到底有什麼用?
實作 java.io.Serializable。
定義使用者類:
class User implements Serializable {
private String username;
private String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}
我們把對象序列化,通過ObjectOutputStream存儲到txt檔案中,再通過ObjectInputStream讀取txt檔案,反序列化成User對象。
public class TestSerialize {
public static void main(String[] args) {
User user = new User();
user.setUsername("hengheng");
user.setPasswd("123456");
System.out.println("read before Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
os.writeObject(user); // 将User對象寫進檔案
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
user = (User) is.readObject(); // 從流中讀取User的資料
is.close();
System.out.println("\nread after Serializable: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
運作結果如下:
序列化前資料:
username: hengheng
password: 123456
序列化後資料:
username: hengheng
password: 123456
到這裡,我們大概知道了什麼是序列化。
問題二:序列化時,你希望某些成員不要序列化,該如何實作?
答案:聲明該成員為靜态或瞬态,在 Java 序列化過程中則不會被序列化。
- 靜态變量:加static關鍵字。
- 瞬态變量: 加transient關鍵字。
我們先嘗試把變量聲明為瞬态。
class User implements Serializable {
private String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
在密碼字段前加上了transient關鍵字再運作。運作結果:
序列化前資料:
username: hengheng
password: 123456
序列化後資料:
username: hengheng
password: null
通過運作結果發現密碼沒有被序列化,達到了我們的目的。
再嘗試在使用者名前加static關鍵字。
class User implements Serializable {
private static String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
運作結果:
序列化前資料:
username: hengheng
password: 123456
序列化後資料:
username: hengheng
password: null
我們發現運作後的結果和預期的不一樣,按理說username也應該變為null才對。是什麼原因呢?
原因是:反序列化後類中static型變量username的值為目前JVM中對應的靜态變量的值,而不是反序列化得出的。
我們來證明一下:
public class TestSerialize {
public static void main(String[] args) {
User user = new User();
user.setUsername("hengheng");
user.setPasswd("123456");
System.out.println("序列化前資料: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
try {
ObjectOutputStream os = new ObjectOutputStream(
new FileOutputStream("/Users/admin/Desktop/test/user.txt"));
os.writeObject(user); // 将User對象寫進檔案
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
User.username = "小明";
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
user = (User) is.readObject(); // 從流中讀取User的資料
is.close();
System.out.println("\n序列化後資料: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class User implements Serializable {
public static String username;
private transient String passwd;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
}
在反序列化前把靜态變量username的值改為『小明』。
User.username = "小明";
再運作一次:
序列化前資料:
username: hengheng
password: 123456
序列化後資料:
username: 小明
password: null
果然,這裡的username是JVM中靜态變量的值,并不是反序列化得到的值。
問題三:serialVersionUID有什麼用?
我們經常會在類中自定義一個serialVersionUID:
private static final long serialVersionUID = 8294180014912103005L
這個serialVersionUID有什麼用呢?如果不設定的話會有什麼後果?
serialVersionUID 是一個 private static final long 型 ID,當它被印在對象上時,它通常是對象的哈希碼。serialVersionUID可以自己定義,也可以自己去生成。
不指定 serialVersionUID的後果是:當你添加或修改類中的任何字段時,已序列化類将無法恢複,因為新類和舊序列化對象生成的 serialVersionUID 将有所不同。Java 序列化的過程是依賴于正确的序列化對象恢複狀态的,并在序列化對象序列版本不比對的情況下引發 java.io.InvalidClassException 無效類異常。
舉個例子大家就明白了:
我們保持之前儲存的序列化檔案不變,然後修改User類。
class User implements Serializable {
public static String username;
private transient String passwd;
private String age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
加了一個屬性age,然後單另寫一個反序列化的方法:
public static void main(String[] args) {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(
"/Users/admin/Desktop/test/user.txt"));
User user = (User) is.readObject(); // 從流中讀取User的資料
is.close();
System.out.println("\n修改User類之後的資料: ");
System.out.println("username: " + user.getUsername());
System.err.println("password: " + user.getPasswd());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

報錯了,我們發現之前的User類生成的serialVersionUID和修改後的serialVersionUID不一樣(因為是通過對象的哈希碼生成的),導緻了InvalidClassException異常。
自定義serialVersionUID:
class User implements Serializable {
private static final long serialVersionUID = 4348344328769804325L;
public static String username;
private transient String passwd;
private String age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}
再試一下:
序列化前資料:
username: hengheng
password: 123456
序列化後資料:
username: 小明
password: null
運作結果無報錯,是以一般都要自定義serialVersionUID。
問題四:是否可以自定義序列化過程?
答案當然是可以的。
之前我們介紹了序列化的第二種方式:
實作Externalizable接口,然後重寫writeExternal() 和readExternal()方法,這樣就可以自定義序列化。
比如我們嘗試把變量設為瞬态。
public class ExternalizableTest implements Externalizable {
private transient String content = "我是被transient修飾的變量哦";
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(content);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
content = (String) in.readObject();
}
public static void main(String[] args) throws Exception {
ExternalizableTest et = new ExternalizableTest();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
new File("test")));
out.writeObject(et);
ObjectInput in = new ObjectInputStream(new FileInputStream(new File(
"test")));
et = (ExternalizableTest) in.readObject();
System.out.println(et.content);
out.close();
in.close();
}
}
我是被transient修飾的變量哦
這裡實作的是Externalizable接口,則沒有任何東西可以自動序列化,需要在writeExternal方法中進行手工指定所要序列化的變量,這與是否被transient修飾無關。
通過上述介紹,是不是對Java序列化有了更多的了解?
作者:楊亨
來源:宜信技術學院