概述
Java對象的序列化和反序列化,這個詞對我來說追溯到大學階段,學Java對象流時知道有這東西。老師告訴我們可以把Java對象化作位元組流,儲存檔案或網絡通信。然後就是巴啦巴拉,一臉懵逼。舉個例子,有一台北京的Java虛拟機現在運作的某個對象要調用一台在長春運作的Java虛拟機内的某個對象,這是兩個不同的Java虛拟機程序,我們沒辦法直接傳遞對象的引用,現在我們隻能把長春的這個對象序列化,變成一塊一塊碎片,傳給北京的虛拟機,北京虛拟機反序列化後就造出了一個對象,然後就可以正常使用。說得通俗點,這個序列化就是跨程序資料傳輸。

序列化(Serializable接口)
要序列化的類通過實作java.io.Serializable接口啟動序列化的功能,如果它有子類,所有的子類本身也都可序列化。
Person類
public class Person implements Serializable
{
private String name;
private int age;
private String sex;
public Person(String name,int age,String sex)
{
this.name = name;
this.age = age;
this.sex = sex;
}
public int getAge()
{
return age;
}
public String getName()
{
return name;
}
public String getSex()
{
return sex;
}
@Override
public String toString()
{
return "姓名:" + this.name + ",年齡:" + this.age + ",性别:" + this.sex;
}
}
serializable接口沒有函數或者字段,我們可以看到我們implements接口,沒實作任何的函數,它僅僅用于辨別可序列化,如果我們沒有實作這個辨別接口而進行序列化,會抛出一個NotSerializableException異常
。
Main類
public class Main
{
public static void main(String[] args)
{
serializePerson();
}
private static void serializePerson()
{
try {
Person customer = new Person("張三",15,"男");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/Person"));
objectOutputStream.writeObject(customer);
System.out.println("Person序列化完成。。。");
objectOutputStream.close();
}catch (Exception e){
e.printStackTrace();
System.out.println("Person序列化出錯。。。");
}
}
}
輸出
Person序列化完成。。
在 E盤下就會有一個Person檔案,用notepad++打開,依稀可以見到一些熟悉的字眼
我們用二進制檢視器打開這個檔案
左邊第一個部分是序列化的檔案頭AC ED 00 05,其他還有關于序列化的類描述,裡面的各個屬性值,還有父類的資訊,lz實在看不懂了,有大佬分析過序列化檔案,有興趣可自行百度檢視。
反序列化
Main類添加DeserializePerson函數
private static Object DeserializePerson()
{
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(new File("E:/Person")));
Object object = objectInputStream.readObject();
System.out.println("反序列化完成。。。");
return object;
} catch (Exception e)
{
e.printStackTrace();
}finally
{
if (objectInputStream != null )
{
try
{
objectInputStream.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
return null;
}
反序列化完成。。。
姓名:張三,年齡:15,性别:男
serialVersionUID(辨別)
知道serializable是辨別的語義,這個辨別是在哪?如果我們沒特意指定,在編譯過程中Java編譯器會預設賦予它一個獨一無二的編号,保證它是唯一的。但這樣做是否會給我們帶來影響?
public class Person implements Serializable
{
private String name;
private int age;
private String sex;
//添加了一個屬性
private String number;
public Person(String name,int age,String sex)
{
this.name = name;
this.age = age;
this.sex = sex;
}
public int getAge()
{
return age;
}
public String getName()
{
return name;
}
public String getSex()
{
return sex;
}
@Override
public String toString()
{
return "姓名:" + this.name + ",年齡:" + this.age + ",性别:" + this.sex;
}
}
public class Main
{
public static void main(String[] args)
{
//serializePerson();
Person person = (Person) DeserializePerson();
System.out.println(person);
}
private static Object DeserializePerson()
{
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(new File("E:/Person")));
Object object = objectInputStream.readObject();
System.out.println("反序列化完成。。。");
return object;
} catch (Exception e)
{
e.printStackTrace();
}finally
{
if (objectInputStream != null )
{
try
{
objectInputStream.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
return null;
}
private static void serializePerson()
{
try {
Person customer = new Person("張三",15,"男");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/Person"));
objectOutputStream.writeObject(customer);
System.out.println("Person序列化完成。。。");
objectOutputStream.close();
}catch (Exception e){
e.printStackTrace();
System.out.println("Person序列化出錯。。。");
}
}
}
發現抛出了一個異常
本地的檔案流中的class(序列化)和修改完的Person.class,不相容了(UID),處于安全機制考慮,程式抛出錯誤,拒絕載入。如何保證UID版本一緻,那隻能自己指定UID,在序列化後,去添加字段或者函數,就不會影響後期還原。
public class Person implements Serializable
{
private static final long serialVersionUID = 55555L;
private String name;
private int age;
private String sex;
public Person(String name,int age,String sex)
{
this.name = name;
this.age = age;
this.sex = sex;
}
public int getAge()
{
return age;
}
public String getName()
{
return name;
}
public String getSex()
{
return sex;
}
@Override
public String toString()
{
return "姓名:" + this.name + ",年齡:" + this.age + ",性别:" + this.sex;
}
}
public class Main
{
public static void main(String[] args)
{
serializePerson();
Person person = (Person) DeserializePerson();
System.out.println(person);
}
private static Object DeserializePerson()
{
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(new File("E:/Person")));
Object object = objectInputStream.readObject();
System.out.println("反序列化完成。。。");
return object;
} catch (Exception e)
{
e.printStackTrace();
}finally
{
if (objectInputStream != null )
{
try
{
objectInputStream.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
return null;
}
private static void serializePerson()
{
try {
Person customer = new Person("張三",15,"男");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:/Person"));
objectOutputStream.writeObject(customer);
System.out.println("Person序列化完成。。。");
objectOutputStream.close();
}catch (Exception e){
e.printStackTrace();
System.out.println("Person序列化出錯。。。");
}
}
}
序列化後,我們注釋掉main函數裡的serializePerson();修改Person類,添加或修改字段,進行反序列化。
反序列化完成。。。
姓名:張三,年齡:15,性别:男
我們可以發現,由編譯器預設自動給我們生成的UID編碼,并不可控,對同一個類,A編譯器編譯,賦予一個UID的值和B編譯器編譯賦予的UID值也有可能不同,是以為了提高可控性,确定性,我們在一個可序列化的類中應該明确為它指派。
Externalizable接口
以上我們可以發現,所有的序列化操作都是預設的,自動幫我們完成。但有時我們并不想這樣,有些屬性我們并不想序列化,想要自定義的方式去序列化它。為此,Java提供了一個Externalizable接口,友善使用者自定義序列化過程,它和Serializable有什麼差別?
Person類
public class Person implements Externalizable
{
private static final long serialVersionUID = 55555L;
private String name;
private int age;
private String sex;
public Person()
{
}
public Person(String name,int age,String sex)
{
this.name = name;
this.age = age;
this.sex = sex;
}
public int getAge()
{
return age;
}
public String getName()
{
return name;
}
public String getSex()
{
return sex;
}
@Override
public String toString()
{
return "姓名:" + this.name + ",年齡:" + this.age + ",性别:" + this.sex;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
{
}
}
Main類不變,輸出
Person序列化完成。。。
反序列化完成。。。
姓名:null,年齡:0,性别:null
與serialization接口相比,我們很快就能看出,Externalizable接口對Person類進行序列化和反序列化之後得到的對象的狀态并沒有儲存下來,所有屬性的值都變成預設值。它們之間有什麼關系和差別?
- Externalizable繼承了Serializable,它定義了兩個抽象函數,writeExternal和readExternal,我們進行序列化和反序列需要重寫,可以指定序列化哪些屬性。
- Externalizable序列化的類必須有一個無參構造函數,否則會報錯。因為Externalizable序列化的時候,讀取對象時,會調用無參構造函數建立一個新的對象,之後将儲存對象的字段的值填充到新對象中。
修改Person類,重新序列化
@Override
public void writeExternal(ObjectOutput out) throws IOException
{
out.writeObject(name);
out.writeObject(age);
out.writeObject(sex);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
{
this.name = (String)in.readObject();
this.age = (int)in.readObject();
this.sex = (String)in.readObject();
}
Person序列化完成。。。
反序列化完成。。。
姓名:張三,年齡:15,性别:男
重寫完兩個函數,發現對象持久化完成。但細心的小夥伴可能會發現,我們序列化的成員變量都是執行個體變量。就會有一個疑問,換成靜态變量試試?
靜态變量被序列化?
其實序列化(預設序列化)被不儲存靜态變量,因為靜态變量屬于類本身,對象序列化,顧名思義就是指的對象本身狀态,并不包含靜态變量。
public class Person implements Serializable
{
private static final long serialVersionUID = 55555L;
private String name;
private int age;
private String sex;
private static String money;
public Person(String name,int age,String sex,String money)
{
this.name = name;
this.age = age;
this.sex = sex;
this.money = money;
}
public int getAge()
{
return age;
}
public String getName()
{
return name;
}
public String getSex()
{
return sex;
}
@Override
public String toString()
{
return "姓名:" + this.name + ",年齡:" + this.age + ",性别:" + this.sex + ",資産:" + money;
}
}
public class Main {
public static void main(String[] args) throws Exception {
serializablePerson();
Person person = (Person)DeserializablePerson();
System.out.println(person);
}
//示範使用,并不規範
private static void serializablePerson() throws Exception {
Person person = new Person("張三",15,"男","5000000");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/Person"));
objectOutputStream.writeObject(person);
objectOutputStream.close();
System.out.println("序列化完成。。。");
}
private static Object DeserializablePerson() throws Exception
{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
Object object = objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列完成。。。");
return object;
}
}
序列化完成。。。
反序列完成。。。
姓名:張三,年齡:15,性别:男,資産:5000000
結果跟我們的結論出乎意料,靜态變量被序列化了,真的是這樣嗎?導緻這個原因是因為我們測試都是在一個程序裡面的。JVM把money這個變量加載進來了,是以導緻我們看到的是加載過的money。我們可以這樣做,多寫一個Main類,讓JVM退出後,重新加載。
MainTest類
public class MainTest {
public static void main(String[] args) throws Exception {
Person person = (Person)DeserializablePerson();
System.out.println(person);
}
private static Object DeserializablePerson() throws Exception
{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
Object object = objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列化完成。。。");
return object;
}
}
現在先運作Main類,得到的是我們上面的接口,現在運作MainTest類
反序列化完成。。。
姓名:張三,年齡:15,性别:男,資産:null
可以發現靜态成員變量并沒有被儲存下來,變成一個預設值。
transient關鍵字(預設序列化)
有時候我們并不想自定義序列化,然而有些成員變量我們也不想序列化。那麼transient這個關鍵字就是你的不二人選,它的作用很簡單,就是控制變量的序列化,在變量聲明前加上這個關鍵字即可。
public class Person implements Serializable
{
private static final long serialVersionUID = 55555L;
private String name;
private int age;
private String sex;
private static String money;
//銀行賬戶
private transient String bankNumber;
//銀行密碼
private transient int passWord;
public Person(String name,int age,String sex,String money,String bankNumber,int passWord)
{
this.name = name;
this.age = age;
this.sex = sex;
this.money = money;
this.bankNumber = bankNumber;
this.passWord = passWord;
}
public int getAge()
{
return age;
}
public String getName()
{
return name;
}
public String getSex()
{
return sex;
}
@Override
public String toString()
{
return "姓名:" + this.name + ",年齡:" + this.age + ",性别:" + this.sex + ",資産:" + money + ",我的銀行賬戶是:" + this.bankNumber + ",我的銀行密碼:" + this.passWord;
}
}
public class Main {
public static void main(String[] args) throws Exception {
serializablePerson();
Person person = (Person)DeserializablePerson();
System.out.println(person);
}
//示範使用,并不規範
private static void serializablePerson() throws Exception {
Person person = new Person("張三",15,"男","5000000","564654979797464646",123456);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/Person"));
objectOutputStream.writeObject(person);
objectOutputStream.close();
System.out.println("序列化完成。。。");
}
private static Object DeserializablePerson() throws Exception
{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
Object object = objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列完成。。。");
return object;
}
}
序列化完成。。。
反序列完成。。。
姓名:張三,年齡:15,性别:男,資産:5000000,我的銀行賬戶是:null,我的銀行密碼:0
這些關鍵資訊就都不會被序列化到檔案中,當然我有500w的話
序列化的存儲規則
Java序列化為了節省存儲空間,有特定的存儲規則,寫入檔案為同一對象的時候,并不會再将對象的内容存儲,而隻是再次存儲一份引用。
public class Main {
public static void main(String[] args) throws Exception {
serializablePerson();
Person person = (Person) DeserializablePerson();
System.out.println(person);
}
//示範使用,并不規範
private static void serializablePerson() throws Exception {
Person person = new Person("張三",15,"男","5000000","564654979797464646",123456);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:/Person"));
objectOutputStream.writeObject(person);
objectOutputStream.flush();
System.out.println(new File("D:/Person").length());
objectOutputStream.writeObject(person);
objectOutputStream.close();
System.out.println(new File("D:/Person").length());
System.out.println("序列化完成。。。");
}
private static Object DeserializablePerson() throws Exception
{
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("D:/Person")));
Person person =(Person) objectInputStream.readObject();
Person person1 =(Person) objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列完成。。。");
System.out.print("是否同一個對象=====>");
System.out.println(person == person1);
return person;
}
}
108
113
序列化完成。。。
反序列完成。。。
是否同一個對象=====>true
姓名:張三,年齡:15,性别:男,資産:5000000,我的銀行賬戶是:null,我的銀行密碼:0
多出五位元組存儲空間就是新增引用和一些控制資訊空間,反序列時,恢複引用關系,person和person1都指向唯一的對象,二者相等,輸出true,這樣的存儲規則就極大節省了存儲的空間。
注意事項
可以發現,我很多地方加了預設序列化的情況下,如果是自定義序列化,那麼transient這些就統統無效,是不是感覺可控性增強不少,序列化還得注意幾個點。
- 如果有内部類,或者是要序列化的對象的成員變量是一個對象類,那麼也必須繼承序列化的接口,否則會出錯滴。
- 子類即使沒有實作序列化的接口,隻要父類實作了,那子類就可以直接序列化。
參考:Java序列化的進階認識
===============================================================
如發現錯誤,請及時留言,lz及時修改,避免誤導後來者。感謝!!!