天天看點

Java的序列化和反序列化

Java的序列化和反序列化

概述

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編譯器會預設賦予它一個獨一無二的編号,保證它是唯一的。但這樣做是否會給我們帶來影響?

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 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,在序列化後,去添加字段或者函數,就不會影響後期還原。

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 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類,添加或修改字段,進行反序列化。

我們可以發現,由編譯器預設自動給我們生成的UID編碼,并不可控,對同一個類,A編譯器編譯,賦予一個UID的值和B編譯器編譯賦予的UID值也有可能不同,是以為了提高可控性,确定性,我們在一個可序列化的類中應該明确為它指派。

Externalizable接口

以上我們可以發現,所有的序列化操作都是預設的,自動幫我們完成。但有時我們并不想這樣,有些屬性我們并不想序列化,想要自定義的方式去序列化它。為此,Java提供了一個Externalizable接口,友善使用者自定義序列化過程,它和Serializable有什麼差別?

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();
}           

重寫完兩個函數,發現對象持久化完成。但細心的小夥伴可能會發現,我們序列化的成員變量都是執行個體變量。就會有一個疑問,換成靜态變量試試?

靜态變量被序列化?

其實序列化(預設序列化)被不儲存靜态變量,因為靜态變量屬于類本身,對象序列化,顧名思義就是指的對象本身狀态,并不包含靜态變量。

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這個關鍵字就是你的不二人選,它的作用很簡單,就是控制變量的序列化,在變量聲明前加上這個關鍵字即可。

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 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 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

多出五位元組存儲空間就是新增引用和一些控制資訊空間,反序列時,恢複引用關系,person和person1都指向唯一的對象,二者相等,輸出true,這樣的存儲規則就極大節省了存儲的空間。

注意事項

可以發現,我很多地方加了預設序列化的情況下,如果是自定義序列化,那麼transient這些就統統無效,是不是感覺可控性增強不少,序列化還得注意幾個點。

如果有内部類,或者是要序列化的對象的成員變量是一個對象類,那麼也必須繼承序列化的接口,否則會出錯滴。

子類即使沒有實作序列化的接口,隻要父類實作了,那子類就可以直接序列化。

參考:Java序列化的進階認識

===============================================================

如發現錯誤,請及時留言,lz及時修改,避免誤導後來者。感謝!!!

原文位址

https://www.cnblogs.com/dslx/p/10648414.html