:notebook: 本文已歸檔到:「blog」html
:keyboard: 本文中的示例代碼已歸檔到:「javacore」java
:bell: 注意:使用 Java 對象序列化,在保存對象時,會把其狀態保存爲一組字節,在將來,再將這些字節組裝成對象。必須注意地是,對象序列化保存的是對象的」狀態」,即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量。git
Java 經過對象輸入輸出流來實現序列化和反序列化:程序員
java.io.ObjectOutputStream
類的 writeObject()
方法能夠實現序列化;java.io.ObjectInputStream
類的 readObject()
方法用於實現反序列化。序列化和反序列化示例:github
public class SerializeDemo01 {
enum Sex {
MALE,
FEMALE
}
static class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = null;
private Integer age = null;
private Sex sex;
public Person() { }
public Person(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + ", sex=" + sex + '}';
}
}
/** * 序列化 */
private static void serialize(String filename) throws IOException {
File f = new File(filename); // 定義保存路徑
OutputStream out = new FileOutputStream(f); // 文件輸出流
ObjectOutputStream oos = new ObjectOutputStream(out); // 對象輸出流
oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 保存對象
oos.close();
out.close();
}
/** * 反序列化 */
private static void deserialize(String filename) throws IOException, ClassNotFoundException {
File f = new File(filename); // 定義保存路徑
InputStream in = new FileInputStream(f); // 文件輸入流
ObjectInputStream ois = new ObjectInputStream(in); // 對象輸入流
Object obj = ois.readObject(); // 讀取對象
ois.close();
in.close();
System.out.println(obj);
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
final String filename = "d:/text.dat";
serialize(filename);
deserialize(filename);
}
}
// Output:
// Person{name='Jack', age=30, sex=MALE}
複製代碼
被序列化的類必須屬於 Enum、Array 和 Serializable 類型其中的任何一種。數據庫
若是不是 Enum、Array 的類,若是須要序列化,必須實現 java.io.Serializable
接口,不然將拋出 NotSerializableException
異常。這是由於:在序列化操做過程當中會對類型進行檢查,若是不知足序列化類型要求,就會拋出異常。apache
咱們不妨作一個小嚐試:將 SerializeDemo01 示例中 Person 類改成以下實現,而後看看運行結果。編程
public class UnSerializeDemo {
static class Person { // 其餘內容略 }
// 其餘內容略
}
複製代碼
輸出:結果就是出現以下異常信息。json
Exception in thread "main" java.io.NotSerializableException:
...
複製代碼
請注意 serialVersionUID
字段,你能夠在 Java 世界的無數類中看到這個字段。bash
serialVersionUID
有什麼做用,如何使用 serialVersionUID
?
serialVersionUID
是 Java 爲每一個序列化類產生的版本標識。它能夠用來保證在反序列時,發送方發送的和接受方接收的是可兼容的對象。若是接收方接收的類的 serialVersionUID
與發送方發送的 serialVersionUID
不一致,會拋出 InvalidClassException
。
若是可序列化類沒有顯式聲明 serialVersionUID
,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID
值。儘管這樣,仍是建議在每個序列化的類中顯式指定 serialVersionUID
的值。由於不一樣的 jdk 編譯極可能會生成不一樣的 serialVersionUID
默認值,從而致使在反序列化時拋出 InvalidClassExceptions
異常。
serialVersionUID
字段必須是 static final long
類型。
咱們來舉個例子:
(1)有一個可序列化類 Person
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
// 構造方法、get、set 方法略
}
複製代碼
(2)開發過程當中,對 Person 作了修改,增長了一個字段 email,以下:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
private String email;
// 構造方法、get、set 方法略
}
複製代碼
因爲這個類和老版本不兼容,咱們須要修改版本號:
private static final long serialVersionUID = 2L;
複製代碼
再次進行反序列化,則會拋出 InvalidClassException
異常。
綜上所述,咱們大概能夠清楚:serialVersionUID
用於控制序列化版本是否兼容。若咱們認爲修改的可序列化類是向後兼容的,則不修改 serialVersionUID
。
若是僅僅只是讓某個類實現 Serializable
接口,而沒有其它任何處理的話,那麼就會使用默認序列化機制。
使用默認機制,在序列化對象時,不只會序列化當前對象自己,還會對其父類的字段以及該對象引用的其它對象也進行序列化。一樣地,這些其它對象引用的另外對象也將被序列化,以此類推。因此,若是一個對象包含的成員變量是容器類對象,而這些容器所含有的元素也是容器類對象,那麼這個序列化的過程就會較複雜,開銷也較大。
注意:這裏的父類和引用對象既然要進行序列化,那麼它們固然也要知足序列化要求:被序列化的類必須屬於 Enum、Array 和 Serializable 類型其中的任何一種。
在現實應用中,有些時候不能使用默認序列化機制。好比,但願在序列化過程當中忽略掉敏感數據,或者簡化序列化過程。下面將介紹若干影響序列化的方法。
當某個字段被聲明爲 transient 後,默認序列化機制就會忽略該字段。
咱們將 SerializeDemo01 示例中的內部類 Person 的 age 字段聲明爲 transient
,以下所示:
public class SerializeDemo02 {
static class Person implements Serializable {
transient private Integer age = null;
// 其餘內容略
}
// 其餘內容略
}
// Output:
// name: Jack, age: null, sex: MALE
複製代碼
從輸出結果能夠看出,age 字段沒有被序列化。
不管是使用 transient
關鍵字,仍是使用 writeObject()
和 readObject()
方法,其實都是基於 Serializable
接口的序列化。
JDK 中提供了另外一個序列化接口--Externalizable
。
可序列化類實現 Externalizable
接口以後,基於 Serializable
接口的默認序列化機制就會失效。
咱們來基於 SerializeDemo02 再次作一些改動,代碼以下:
public class ExternalizeDemo01 {
static class Person implements Externalizable {
transient private Integer age = null;
// 其餘內容略
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException { }
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }
}
// 其餘內容略
}
// Output:
// call Person()
// name: null, age: null, sex: null
複製代碼
從該結果,一方面能夠看出 Person 對象中任何一個字段都沒有被序列化。另外一方面,若是細心的話,還能夠發現這這次序列化過程調用了 Person 類的無參構造方法。
Externalizable
繼承於 Serializable
,它增添了兩個方法:writeExternal()
與 readExternal()
。這兩個方法在序列化和反序列化過程當中會被自動調用,以便執行一些特殊操做。當使用該接口時,序列化的細節須要由程序員去完成。如上所示的代碼,因爲 writeExternal()
與 readExternal()
方法未做任何處理,那麼該序列化行爲將不會保存/讀取任何一個字段。這也就是爲何輸出結果中全部字段的值均爲空。Externalizable
接口的類必需要提供一個無參的構造方法,且它的訪問權限爲 public
。對上述 Person 類做進一步的修改,使其可以對 name 與 age 字段進行序列化,但要忽略掉 gender 字段,以下代碼所示:
public class ExternalizeDemo02 {
static class Person implements Externalizable {
transient private Integer age = null;
// 其餘內容略
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}
// 其餘內容略
}
// Output:
// call Person()
// name: Jack, age: 30, sex: null
複製代碼
實現 Externalizable
接口能夠控制序列化和反序列化的細節。它有一個替代方法:實現 Serializable
接口,並添加 writeObject(ObjectOutputStream out)
與 readObject(ObjectInputStream in)
方法。序列化和反序列化過程當中會自動回調這兩個方法。
示例以下所示:
public class SerializeDemo03 {
static class Person implements Serializable {
transient private Integer age = null;
// 其餘內容略
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
// 其餘內容略
}
// 其餘內容略
}
// Output:
// name: Jack, age: 30, sex: MALE
複製代碼
在 writeObject()
方法中會先調用 ObjectOutputStream
中的 defaultWriteObject()
方法,該方法會執行默認的序列化機制,如上節所述,此時會忽略掉 age 字段。而後再調用 writeInt() 方法顯示地將 age 字段寫入到 ObjectOutputStream
中。readObject() 的做用則是針對對象的讀取,其原理與 writeObject() 方法相同。
注意:
writeObject()
與readObject()
都是private
方法,那麼它們是如何被調用的呢?毫無疑問,是使用反射。詳情可見ObjectOutputStream
中的writeSerialData
方法,以及ObjectInputStream
中的readSerialData
方法。
當咱們使用 Singleton 模式時,應該是指望某個類的實例應該是惟一的,但若是該類是可序列化的,那麼狀況可能會略有不一樣。此時對第 2 節使用的 Person 類進行修改,使其實現 Singleton 模式,以下所示:
public class SerializeDemo04 {
enum Sex {
MALE, FEMALE
}
static class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = null;
transient private Integer age = null;
private Sex sex;
static final Person instatnce = new Person("Tom", 31, Sex.MALE);
private Person() {
System.out.println("call Person()");
}
private Person(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public static Person getInstance() {
return instatnce;
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
public String toString() {
return "name: " + this.name + ", age: " + this.age + ", sex: " + this.sex;
}
}
/** * 序列化 */
private static void serialize(String filename) throws IOException {
File f = new File(filename); // 定義保存路徑
OutputStream out = new FileOutputStream(f); // 文件輸出流
ObjectOutputStream oos = new ObjectOutputStream(out); // 對象輸出流
oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 保存對象
oos.close();
out.close();
}
/** * 反序列化 */
private static void deserialize(String filename) throws IOException, ClassNotFoundException {
File f = new File(filename); // 定義保存路徑
InputStream in = new FileInputStream(f); // 文件輸入流
ObjectInputStream ois = new ObjectInputStream(in); // 對象輸入流
Object obj = ois.readObject(); // 讀取對象
ois.close();
in.close();
System.out.println(obj);
System.out.println(obj == Person.getInstance());
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
final String filename = "d:/text.dat";
serialize(filename);
deserialize(filename);
}
}
// Output:
// name: Jack, age: null, sex: MALE
// false
複製代碼
值得注意的是,從文件中獲取的 Person 對象與 Person 類中的單例對象並不相等。爲了能在單例類中仍然保持序列的特性,可使用 readResolve()
方法。在該方法中直接返回 Person 的單例對象。咱們在 SerializeDemo04 示例的基礎上添加一個 readObject
方法, 以下所示:
public class SerializeDemo05 {
// 其餘內容略
static class Person implements Serializable {
// 添加此方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
// 其餘內容略
}
// 其餘內容略
}
// Output:
// name: Tom, age: 31, sex: MALE
// true
複製代碼
Java 官方的序列化存在許多問題,所以,不少人更願意使用優秀的第三方序列化工具來替代 Java 自身的序列化機制。
Java 官方的序列化主要體如今如下方面:
固然咱們還有更加優秀的一些序列化和反序列化的工具,根據不一樣的使用場景能夠自行選擇!