序列化Serializable的那些事兒
說到Java的序列化,有個問題就是為什麼需要序列化,更優先的一個問題是什麼是序列化。
序列化的含義
《Java程式設計思想》中這麼解釋,Java的物件序列化是一個輕量級的持久化過程,序列化時,可以將java物件以位元組序列形式寫入硬碟、資料庫或者通過網路傳輸到另一個JVM等等,反序列化是讀取序列化的檔案,將其在JVM中還原為java物件的過程。
換句話說就是,主要將物件的可變資訊以位元組序列,儲存在檔案或資料庫中。等到需要在記憶體中恢復該物件當時的狀態時,也就是當別的機器或程式執行時需要該物件的狀態時,可使用反序列化這些位元組序列,將此物件還原出來使用。這種機制就叫做序列化。
序列化的用途
明白了序列化的含義,也不難清楚序列化的用途了。
- 當你想把的記憶體中的物件狀態儲存到一個檔案中或者資料庫中時候;
- 當你想用套接字在網路上傳送物件的時候;
- 當你想通過RMI傳輸物件的時候。
明白了序列化的含義及用途,接下來需要了解序列化的使用。
序列化的使用
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class TestSerializable { public static void main(String[] args) throws IOException, ClassNotFoundException { Person p1 = new Person(); Dog d1 = new Dog(); d1.setName("Dog1"); p1.setId(1); p1.setName("Tom"); p1.setDog(d1); // serializable FileOutputStream fos = new FileOutputStream(new File("E:/test.ser")); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(p1); oos.close(); // deserializable FileInputStream fis = new FileInputStream(new File("E:/test.ser")); ObjectInputStream ois = new ObjectInputStream(fis); Object obj = ois.readObject(); ois.close(); System.out.println(obj); int id = ((Person)obj).getId(); String name = ((Person)obj).getName(); Dog d = ((Person)obj).getDog(); System.out.println(id + " " + name + " " + d); } public static class Person implements Serializable { private static final long serialVersionUID = -1891426275960796136L; private transient int id; private String name; private transient Dog dog; public Dog getDog() { return dog; } public void setDog(Dog dog) { this.dog = dog; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public static class Dog { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } }
在序列化的使用中,主要有以下幾個問題:
- transient關鍵字的作用;
- serialVersionUID的作用;
- 序列化警告的處理方式。
transient關鍵字的作用
transient,意為短暫的,臨時的。在Java中,transient宣告一個例項變數,當物件儲存時,它的值不需要維持。換句話來說就是,用transient關鍵字標記的成員變數不參與序列化過程。
如示例程式碼中Person擁有一個transient修飾的id和Dog,程式執行後,經過序列化和反序列化,結果如下:
0 Tom null
可以發現,id和Dog可以認為分別為各自型別的預設值,id=0,dog=null。明顯這兩個變數未經過序列化過程。
注意:不需要經過序列化的型別的成員變數,使用transient修飾後可以不實現Serializable介面。如Person的Dog不需要序列化。當然,非要給Dog實現Serializable介面也不影響結果。
serialVersionUID的作用
當沒有顯式地定義long型別的serialVersionUID變數時,Java序列化機制會根據編譯的class(它通過類名,方法名等諸多因素經過計算而得,理論上是一一對映的關係,也就是唯一的)自動生成一個serialVersionUID作序列化版本比較用。
這種情況下,如果class檔案(類名、方法名等)沒有發生變化(增加空格、換行、註釋等等),就算再編譯多次,serialVersionUID也不會變化。但是一旦變化,如給類增加了方法、屬性等,那麼在反序列化時,就會出現序列化版本不一致的異常(InvalidCastException)!如下:
Exception in thread "main" java.io.InvalidClassException: com.szh.serializable.TestSerializable$Person; local class incompatible: stream classdesc serialVersionUID = -5579678255079137242, local class serialVersionUID = -1463584727334114570
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:617)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1622)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1771)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
at com.szh.serializable.TestSerializable.main(TestSerializable.java:30)
因此,serialVersionUID即代表了對應類的版本號,目的是為了保證序列化和反序列化時,類資訊的一致性和安全性。
序列化警告的處理方式
在Java中,我們對警告不應無視,每一條警告都應該做合適的處理。那對於序列化時,我們實現了Serializable介面,卻未顯示定義serialVersionUID時,IDE會提示出三種處理方式:
- Add default serial version ID;
- Add generated serial version ID;
- Add @SuppressWarnings 'serial' to 'Person'。
當我們選擇第一種處理方式時,相當於顯式地手動定義了類的當前版本(自己去維護)。當我們以後需要此類時,需要同步修改serialVersionUID。
當我們選擇第二種處理方式時,相當於顯式地自動定義了類的當前版本(根據類資訊等內容自動生成)。當我們以後需要此類時,需要重新生成serialVersionUID。
當我們選擇第三種處理方式時,相當於告訴編譯器忽略此警告。那麼問題來了,第三種方式貌似可有可無,畢竟相當於上面哪種都未選擇。答案是否定的。
在Java中,任何警告我們都不應不處理(選擇使用第三種註解方式忽略警告也認為是一種處理方式,但是三種方式都不選擇的話,則認為是不處理),雖然使用@SuppressWarnings去忽略也是一樣的執行效果。因為,這是一種很好地編碼習慣,有時警告也會造成一些意想不到的問題,所以我們應當處理程式碼中所有的警告,對於確實可以忽略掉的警告,jdk提供了這個註解來標記程式碼警告行,這樣便可以使我們的程式碼邏輯更加嚴密,因為我們處理了每一條警告。
那麼我們如何在編譯後的class檔案中,找到類的版本資訊呢?
序列化與位元組碼class檔案
class檔案,存放了十六進位制位元組碼,其中包含了類編譯時的序列版本號。我們可以使用如下命令進行解析:
javap -v class檔名 > 輸出檔名
解析後可以看出來,對於第一第二種顯式地新增版本號的類來說,能夠明顯找到serialVersionUID,而對於使用註解忽略或直接忽略的方式,則不能找到serialVersionUID。
不論那種處理警告的方式,在序列化到檔案的這個過程中,此時類的版本serialVersionUID必然儲存在了位元組序列中並儲存在檔案了。