本文目錄
- 一、序列化是什麼
- 二、為什麼需要序列化
- 三、序列化怎麼用
- 四、序列化深度探祕
- 4.1 為什麼必須實現Serializable介面
- 4.2 被序列化物件的欄位是引用時該怎麼辦
- 4.3 同一個物件會被序列化多次嗎
- 4.4 只想序列化物件的部分欄位該怎麼辦
- 4.5 被序列化物件具有繼承關係該怎麼辦
- 五、serialVersionUID的作用及自動生成
- 六、序列化的缺點
- 七、參考文獻
前言
Oracle 公司計劃廢除 Java 中的古董:序列化技術,因為它帶來了許多嚴重的安全問題(如序列化儲存安全、反序列化安全、傳輸安全等),據統計,至少有3分之1的漏洞是序列化帶來的,這也是 1997 年誕生序列化技術的一個巨大錯誤。但是,序列化技術現在在 Java 應用中無處不在,特別是現在的持久化框架和分散式技術中,都需要利用序列化來傳輸物件,如:Hibernate、Mybatis、Java RMI、Dubbo等,即物件要儲存或者傳輸都不可避免要用到序列化技術,所以刪除序列化技術將是一個長期的計劃。
你在實際工作中可能會很難有機會真正用到Java自帶的序列化技術了,工業界一般也會選擇一些更安全的物件編解碼方案例如Google的Protobuf等。所以,對於Java序列化,我們不必再投入過多的精力學習,你花20分鐘讀完本文所掌握的知識,對於應付日常原始碼閱讀中遇到的遺留的Java序列化技術應該是足夠了。
一、序列化是什麼
序列化機制允許將實現序列化的Java物件轉換成位元組序列,這些位元組序列可以儲存在磁碟上,或通過網路傳輸,以備以後重新恢復成原來的物件。序列化機制使得物件可以脫離程式的執行而獨立存在。
- 序列化:將一個Java物件寫入IO流中
- 反序列化:從IO流中恢復該Java物件
本文中用序列化來簡稱整個序列化和反序列化機制。
二、為什麼需要序列化
所有可能在網路上傳輸的物件的類都應該是可序列化的,否則程式將會出現異常,比如RMI(Remote Method Invoke,即遠端方法呼叫,是JavaEE的基礎)過程中的引數和返回值;所有需要儲存到磁碟裡的物件的類都必須可序列化,比如Web應用中需要儲存到HttpSession或ServletContext屬性的Java物件。
因為序列化是RMI過程的引數和返回值都必須實現的機制,而RMI又是Java EE技術的基礎——所有的分散式應用常常需要跨平臺、跨網路,所以要求所有傳遞的引數、返回值必須實現序列化。因此序列化機制是Java EE平臺的基礎。通常建議:程式建立的每個JavaBean類都實現Serializable。
三、序列化怎麼用
如果一個類的物件需要序列化,那麼在Java語法層面,這個類需要:
- 實現Serializable介面
- 使用ObjectOutputStream將物件輸出到流,實現物件的序列化;使用ObjectInputStream從流中讀取物件,實現物件的反序列化
下面我們通過程式碼示例來看看序列化最基本的用法。我們建立了Person類,其擁有兩個基本型別的屬性,並實現了Serializable介面。testSerialize方法用來測試序列化,testDeserialize方法用來測試反序列化。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 148.2); 10 Person two = new Person(35, 177.8); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable { 40 int age; 41 double height; 42 43 public Person(int age, double height) { 44 this.age = age; 45 this.height = height; 46 } 47 48 @Override 49 public String toString() { 50 return "Person{" + 51 "age=" + age + 52 ", height=" + height + 53 '}'; 54 } 55 }
四、序列化深度探祕
4.1 為什麼必須實現Serializable介面
如果某個類需要支援序列化功能,那麼它必須實現Serializable介面,否則會報 java.io.NotSerializableException。Serializable介面是一個標誌性介面(Marker Interface),也就是說,該介面並不包含任何具體的方法,是一個空介面,僅僅用來判斷該類是否能夠序列化。JDK8中Serializable介面的原始碼如下:
1 package java.io; 2 3 public interface Serializable { 4 }
在 ObjectOutputStream.java 的 writeObject0 方法中,我們確實可以看到對物件是否實現了 Serializable介面進行了驗證(第15行),否則會丟擲 NotSerializableException 異常(第22行)。
1 private void writeObject0(Object obj, boolean unshared) 2 throws IOException 3 { 4 boolean oldMode = bout.setBlockDataMode(false); 5 depth++; 6 try { 7 ... 8 // remaining cases 9 if (obj instanceof String) { 10 writeString((String) obj, unshared); 11 } else if (cl.isArray()) { 12 writeArray(obj, desc, unshared); 13 } else if (obj instanceof Enum) { 14 writeEnum((Enum<?>) obj, desc, unshared); 15 } else if (obj instanceof Serializable) { 16 writeOrdinaryObject(obj, desc, unshared); 17 } else { 18 if (extendedDebugInfo) { 19 throw new NotSerializableException( 20 cl.getName() + "\n" + debugInfoStack.toString()); 21 } else { 22 throw new NotSerializableException(cl.getName()); 23 } 24 } 25 } finally { 26 depth--; 27 bout.setBlockDataMode(oldMode); 28 } 29 }
4.2 被序列化物件的欄位是引用時該怎麼辦
在第三部分“序列化怎麼用”部分的示例中,Person類的欄位全都是基本型別,我們知道基本型別其地址中直接存放的就是它的值,那如果是引用型別呢?引用型別其地址中存放的是指向堆記憶體中的一個地址,難道序列化時就是將這個地址進行了儲存嗎?顯然,這是說不通的,因為物件的記憶體地址是可變的,在同一系統的不同執行時刻或者是不同系統中,物件的地址肯定是不同的,因此,序列化記憶體地址沒有意義。
如果被序列化物件的欄位是引用,那麼要求該引用的型別也是可序列化實現了Serializable介面的,否則無法序列化。當對某個物件進行序列化時,系統會自動把該物件的所有Field依次進行序列化,如果某個Field引用到另一個物件,則被引用的物件也會被序列化;如果被引用的物件的Field也引用了其他物件,則被引用的物件也會被序列化,這種情況被稱為遞迴序列化。
4.3 同一個物件會被序列化多次嗎
如果物件A和物件B同時引用了物件C,那麼,當序列化物件A和物件B時,物件C會被序列化兩次嗎?答案顯然是不會。
要解釋這個問題,就不得不說一下Java序列化的基本演算法了:
- 所有序列化到二進位制流的物件都有一個序列化編號
- 當程式試圖序列化一個物件時,程式將先檢查該物件是否已經被序列化過,只有該物件從未(在本次虛擬機器中)被序列化過,系統才會將該物件轉換成位元組序列並賦予一個唯一的編號
- 如果某個物件已經序列化過,程式將只是直接輸出其序列化編號,而不是再次重新序列化該物件
4.4 只想序列化物件的部分欄位該怎麼辦
在一些特殊的場景下,如果一個類裡包含的某些Field值是敏感資訊,例如銀行賬戶資訊等,這時不希望系統將該Field值進行序列化;或者某個Field的型別是不可序列化的,因此不希望對該Field進行遞迴序列化,以避免引發java.io.NotSerializableException異常。
此時,我們就需要自定義序列化了。自定義序列化的常用方式有兩種:
- 使用transient關鍵字
- 重寫writeObject與readObject方法
我們先看第一種方式,使用transient關鍵字。transient關鍵字只能用於修飾Field,不可修飾Java程式中的其他成分。使用transient修飾的屬性,java序列化時,會忽略掉此欄位,所以反序列化出的物件,被transient修飾的屬性是預設值。對於引用型別,值是null;基本型別,值是0;boolean型別,值是false。
下列程式碼中,我們把People的height欄位設定為transient,在反序列化時,可觀察到輸出為預設值0.0。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 156.6); 10 Person two = new Person(16, 177.7); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable{ 40 protected int age; 41 protected transient double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 @Override 52 public String toString() { 53 return "Person{" + 54 "age=" + age + 55 ", height=" + height + 56 '}'; 57 } 58 }
程式輸出:
Person{age=12, height=0.0} Person{age=16, height=0.0} Process finished with exit code 0
使用transient關鍵字修飾Field雖然簡單、方便,但被transient修飾的Field將被完全隔離在序列化機制之外,這樣導致在反序列化恢復Java物件時無法取得該Field值。Java還提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程式控制如何序列化各Field,甚至完全不序列化某些Field(與使用transient關鍵字的效果相同)。在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法,這些特殊的方法用以實現自定義序列化。
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
- writeObject()方法負責寫入特定類的例項狀態,以便相應的readObject()方法可以恢復它。通過重寫該方法,程式設計師可以完全獲得對序列化機制的控制,可以自主決定哪些Field需要序列化,需要怎樣序列化。在預設情況下,該方法會呼叫out.defaultWriteObject來儲存Java物件的各Field,從而可以實現序列化Java物件狀態的目的。
- readObject()方法負責從流中讀取並恢復物件Field,通過重寫該方法,程式設計師可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些Field,以及如何進行反序列化。在預設情況下,該方法會呼叫in.defaultReadObject來恢復Java物件的非靜態和非瞬態Field。在通常情況下,readObject()方法與writeObject()方法對應,如果writeObject()方法中對Java物件的Field進行了一些處理,則應該在readObject()方法中對其Field進行相應的反處理,以便正確恢復該物件。
- 當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化的物件。例如,接收方使用的反序列化類的版本不同於傳送方,或者接收方版本擴充套件的類不是傳送方版本擴充套件的類,或者序列化流被篡改時,系統都會呼叫readObjectNoData()方法來初始化反序列化的物件。
下面的示例程式碼中,我們在writeObject方法中對Person的欄位進行了簡單的加密處理,在readObject方法中對其進行了相應的解密。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Person one = new Person(12, 156.6); 10 Person two = new Person(16, 177.7); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Person.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Person.txt"))) { 26 Person one = (Person) input.readObject(); 27 Person two = (Person) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person implements Serializable{ 40 protected int age; 41 protected double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 private void writeObject(java.io.ObjectOutputStream out) 52 throws IOException { 53 System.out.println("Encryption!"); 54 out.writeInt(age + 1); 55 out.writeDouble(height - 1); 56 } 57 private void readObject(java.io.ObjectInputStream in) 58 throws IOException, ClassNotFoundException { 59 System.out.println("Decryption!"); 60 this.age = in.readInt() - 1; 61 this.height = in.readDouble() + 1; 62 } 63 64 @Override 65 public String toString() { 66 return "Person{" + 67 "age=" + age + 68 ", height=" + height + 69 '}'; 70 } 71 }
4.5 被序列化物件具有繼承關係該怎麼辦
被序列化物件具有繼承關係時無非就兩種情況,第一,該類具有子類,第二,該類具有父類。
當該類實現了Serializable介面且具有子類時,根據官方文件中的說明,其子類天然具有可被序列化的屬性,不需要顯式實現Serializable介面;。
All subtypes of a serializable class are themselves serializable.
當該類實現了Serializable介面且具有父類時,,該類的父類需要實現Serializable介面嗎?在JDK8中Serializable介面的官方文件中有這樣一段話:
1 /** 2 * ...... 3 * 4 * To allow subtypes of non-serializable classes to be serialized, the 5 * subtype may assume responsibility for saving and restoring the 6 * state of the supertype's public, protected, and (if accessible) 7 * package fields. The subtype may assume this responsibility only if 8 * the class it extends has an accessible no-arg constructor to 9 * initialize the class's state. It is an error to declare a class 10 * Serializable if this is not the case. The error will be detected at 11 * runtime. 12 * 13 * During deserialization, the fields of non-serializable classes will 14 * be initialized using the public or protected no-arg constructor of 15 * the class. A no-arg constructor must be accessible to the subclass 16 * that is serializable. The fields of serializable subclasses will 17 * be restored from the stream. 18 */
閱讀文件我們得知,為了使得不可序列化類的子類能夠序列化,其子類必須擔負起儲存和恢復其超類的public、protected 和 package(if accessible)例項域的責任,且要求其父類必須有一個可訪問的無參建構函式以使得在反序列化時能夠初始化例項域。
我們寫程式碼驗證一下,如果父類中沒有可訪問的無參建構函式會發生什麼,注意Person類中沒有無參建構函式。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Student one = new Student(12, 156.6, "1234"); 10 Student two = new Student(16, 177.7, "5678"); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Student.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Student.txt"))) { 26 Student one = (Student) input.readObject(); 27 Student two = (Student) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person{ 40 protected int age; 41 protected double height; 42 43 public Person(int age, double height) { 44 this.age = age; 45 this.height = height; 46 } 47 48 @Override 49 public String toString() { 50 return "Person{" + 51 "age=" + age + 52 ", height=" + height + 53 '}'; 54 } 55 } 56 57 class Student extends Person implements Serializable{ 58 private String id; 59 60 public Student(int age, double height, String id) { 61 super(age, height); 62 this.id = id; 63 } 64 65 @Override 66 public String toString() { 67 return "Student{" + 68 "age=" + age + 69 ", height=" + height + 70 ", id='" + id + '\'' + 71 '}'; 72 } 73 }
程式輸出產生異常:
java.io.InvalidClassException: Student; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) at SerializableTest.testDeserialize(SerializableTest.java:26) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:497) ... Process finished with exit code 0
當我們為Person類新增預設建構函式時:
1 class Person{ 2 protected int age; 3 protected double height; 4 5 public Person() { 6 } 7 8 public Person(int age, double height) { 9 this.age = age; 10 this.height = height; 11 } 12 13 @Override 14 public String toString() { 15 return "Person{" + 16 "age=" + age + 17 ", height=" + height + 18 '}'; 19 } 20 }
程式輸出如下,我們可觀察到,父類中的欄位都是預設值,只有子類中的欄位得到了正確的序列化。出現這種情況的原因是子類並沒有擔負起序列化父類中欄位的責任。
Student{age=0, height=0.0, id='1234'} Student{age=0, height=0.0, id='5678'} Process finished with exit code 0
為了解決上述問題,我們需要藉助上一節中學到的知識,使用自定義的序列化方法writeObject和readObject來主動將父類中的欄位進行序列化。
1 import org.junit.Test; 2 3 import java.io.*; 4 5 public class SerializableTest { 6 7 @Test 8 public void testSerialize() { 9 Student one = new Student(12, 156.6, "1234"); 10 Student two = new Student(16, 177.7, "5678"); 11 12 try (ObjectOutputStream output = 13 new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) { 14 output.writeObject(one); 15 output.writeObject(two); 16 } catch (IOException e) { 17 e.printStackTrace(); 18 } 19 } 20 21 @Test 22 public void testDeserialize() { 23 24 try (ObjectInputStream input = 25 new ObjectInputStream(new FileInputStream("Studnet.txt"))) { 26 Student one = (Student) input.readObject(); 27 Student two = (Student) input.readObject(); 28 29 System.out.println(one); 30 System.out.println(two); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } catch (ClassNotFoundException e) { 34 e.printStackTrace(); 35 } 36 } 37 } 38 39 class Person{ 40 protected int age; 41 protected double height; 42 43 public Person() { 44 } 45 46 public Person(int age, double height) { 47 this.age = age; 48 this.height = height; 49 } 50 51 @Override 52 public String toString() { 53 return "Person{" + 54 "age=" + age + 55 ", height=" + height + 56 '}'; 57 } 58 } 59 60 class Student extends Person implements Serializable{ 61 private String id; 62 63 public Student(int age, double height, String id) { 64 super(age, height); 65 this.id = id; 66 } 67 68 private void writeObject(java.io.ObjectOutputStream out) 69 throws IOException { 70 out.defaultWriteObject(); 71 out.writeInt(age); 72 out.writeDouble(height); 73 } 74 75 private void readObject(java.io.ObjectInputStream in) 76 throws IOException, ClassNotFoundException { 77 in.defaultReadObject(); 78 this.age = in.readInt(); 79 this.height = in.readDouble(); 80 } 81 82 @Override 83 public String toString() { 84 return "Student{" + 85 "age=" + age + 86 ", height=" + height + 87 ", id='" + id + '\'' + 88 '}'; 89 } 90 }
程式輸出如下,可以看到完全正確。
Student{age=12, height=156.6, id='1234'} Student{age=16, height=177.7, id='5678'} Process finished with exit code 0
五、serialVersionUID的作用及自動生成
我們知道,反序列化必須擁有class檔案,但隨著專案的升級,class檔案也會升級,序列化怎麼保證升級前後的相容性呢?
java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,物件也可以正確被反序列化回來。如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。下面是JDK 8中ArrayList的原始碼中的serialVersionUID。
1 public class ArrayList<E> extends AbstractList<E> 2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable 3 { 4 private static final long serialVersionUID = 8683452581122892189L; 5 6 /** 7 * Default initial capacity. 8 */ 9 private static final int DEFAULT_CAPACITY = 10; 10 ... 11 }
序列化版本號可自由指定,如果不指定,JVM會根據類資訊自己計算一個版本號,這樣隨著class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class檔案沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。
什麼情況下需要修改serialVersionUID呢?分三種情況。
- 如果只是修改了方法,反序列化不容影響,則無需修改版本號
- 如果只是修改了靜態Field或瞬態Field,則反序列化不受任何影響
- 如果修改類時修改了非靜態Field、非瞬態Field,則可能導致序列化版本不相容。如果物件流中的物件和新類中包含同名的Field,而Field型別不同,則反序列化失敗,類定義應該更新serialVersionUID Field值。如果只是新增了例項變數,則反序列化回來新增的是預設值;如果減少了例項變數,反序列化時會忽略掉減少的例項變數。
我們在日常程式設計實踐中,一般會選擇使用IDE來自動生成serialVersionUID,這樣可以最大化地減少重複的可能性。對於IntelliJ IDEA,自動生成serialVersionUID有三步:
- 修改IDEA配置:File->Setting->Editor->Inspections->Serialization issues->Serializable class without ’serialVersionUID’
- 類實現Serializable介面
- 在類名上執行Alt+Enter,然後選擇生成serialVersionUID即可
六、序列化的缺點
Java序列化存在四個致命缺點,導致其不適用於網路傳輸:
- 無法跨語言:在網路傳輸中,經常會有異構語言的程式的互動,但Java序列化技術是Java語言內部的私有協議,其他語言無法進行反序列化。目前所有流行的RPC框架都沒有使用Java序列化作為編解碼框架。
- 潛在風險高:不可信流的反序列化可能導致遠端程式碼執行(RCE)、拒絕服務(DoS)和一系列其他攻擊。
- 序列化後的碼流太大
- 序列化的效能較低
在真正的生產環境中,一般會選擇其它編解碼框架,領先的跨平臺結構化資料表示是 JSON 和 Protocol Buffers,也稱為 protobuf。JSON 由 Douglas Crockford 設計用於瀏覽器與伺服器通訊,Protocol Buffers 由谷歌設計用於在其伺服器之間儲存和交換結構化資料。JSON 和 protobuf 之間最顯著的區別是 JSON 是基於文字的,並且是人類可讀的,而 protobuf 是二進位制的,但效率更高。
七、參考文獻
- 《瘋狂Java講義》第2版,李剛著,電子工業出版社
- 《Java核心技術》第10版,霍斯特曼等著,機械工業出版本
- 《Netty權威指南》第2版,李林鋒著,電子工業出版社
- 《Effective Java》第2版,Joshua Bloch著,機械工業出版社