關於Java序列化的問題你真的會嗎?
引言
在持久化資料物件的時候我們很少使用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序列化有了更多的瞭解?
作者:楊亨
來源:宜信技術學院
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2679384/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java內部類你真的會嗎?Java
- 在Java中,你真的會日期轉換嗎Java
- 細思極恐-你真的會寫java嗎?Java
- 細思極恐 - 你真的會寫 Java 嗎?Java
- 15個問題自查你真的瞭解java編譯優化嗎?Java編譯優化
- 關於Java序列化你不知道的事Java
- Java中執行緒池,你真的會用嗎?Java執行緒
- 你真的會寫單例模式嗎——Java實現單例模式Java
- 每天加班的你,真的會工作嗎?
- java中的Arrays這個工具類你真的會用嗎Java
- 對於經常接觸的分頁你確定你真的會嗎
- 你真的懂01揹包問題嗎?01揹包的這幾問你能答出來嗎?
- 你真的會vue-router嗎?Vue
- 你真的瞭解EF嗎?關於EntityFramework的高階優化Framework優化
- 這幾個關於Spring 依賴注入的問題你清楚嗎?Spring依賴注入
- 關於C# yield 你會使用嗎?C#
- 前端er,你真的會用 async 嗎?前端
- 堆排序你真的學會了嗎?排序
- 你真的會使用資料庫的索引嗎?資料庫索引
- Java執行緒安全面試題,你真的瞭解嗎?Java執行緒面試題
- 你真的會用PostGIS中的buffer緩衝嗎?
- 你真的會搭建測試環境嗎?
- Flutter篇之你真的會使用Future嗎?Flutter
- Myabtis動態SQL,你真的會了嗎?SQL
- 關於時間 PHP 處理包遇到的問題時間序列化差值問題PHP
- Java中的深淺拷貝問題,你清楚嗎?Java
- 關於學習Python的疑問,你都清楚了嗎?Python
- 最怕的就是假裝努力,你真的會學習嗎?
- 你真的會判斷 _objc_msgForward_stret 嗎OBJForward
- 你真的會用二分查詢嗎?
- 使用Typora編寫Markdown你真的會了嗎
- 面試常遇的打家劫舍問題你學會了嗎~面試
- 你真的理解this嗎
- CAS你知道嗎?底層如何實現?ABA問題又是什麼?關於這些你知道答案嗎
- 靈魂拷問,你真的瞭解DNS嗎?DNS
- Leetcode刷題中關於java的一些小問題LeetCodeJava
- Java併發(7)- 你真的瞭解 ReentrantReadWriteLock 嗎?Java
- 你真的瞭解JAVA中物件和類、this、super和static關鍵字嗎Java物件