1. 程式人生 > 其它 >java序列化,深度解析

java序列化,深度解析

技術標籤:java 基礎jdk

文章目錄

java序列化,深度解析

物件序列化是什麼?

java程式啟動會啟動一個相應的jvm程序,在程式執行期間,若新生成了物件,則會在jvm堆上分配空間(大多數情況),進行物件的表示,即物件的生命週期是短於jvm的,物件只能存在jvm程序執行時,但在某些情況下,我們希望將記憶體中的物件狀態(即物件的例項資料)儲存起來,儲存的地方可能是檔案,可能是資料庫,然後在將來的某一個時間讀取儲存的"物件"(此時是二進位制資料的物件),將其恢復成記憶體中的物件。java的序列化

機制即提供了這樣一種功能,可以將記憶體中的物件轉化為位元組,位元組可用於持久化儲存,也可以進行遠端傳輸(例如可以一臺windows機器通過序列化將物件傳輸到另一臺遠端的linux機器)。反序列化自然就是將位元組轉化為記憶體中的物件

如何使用?

若需要類支援序列化功能,只需實現java.io.Serializable 介面即可。該介面是一個標記介面,不含有任何方法。此處建立一個支援序列化功能的類Person,供後面講解。

package demo;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = -7763700527072968555L; private String name; private Integer age; private double height; public Person(String name, Integer age,double height) { System.out.println("Person的有參構造器"); this.name = name; this
.age = age; this.height=height; } public Person() { System.out.println("Person無參構造器"); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", height=" + height + '}'; } }

測試類將可序列化的類儲存到檔案中

package demo;
import java.io.*;
class PersonTest {
    public static void main(String[] args) throws Exception {
        String filePath="C:\\test\\person.out";
        Person person = new Person("zhangsan", 21,1.80);
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
        //將物件序列化儲存到檔案中
        outputStream.writeObject(person);
        outputStream.close();
        //將檔案中的位元組還原為記憶體中的物件(反序列化)
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(filePath));
        Person readObject = (Person) inputStream.readObject();
        inputStream.close();
        System.out.println(readObject);
    }

}

測試結果為

Person的有參構造器
Person{name='zhangsan', age=21, height=1.8}

從輸出結果可以知道兩點:1.Serializable實現類反序列化的時候不會呼叫類的構造器,會直接根據儲存的位元組序列還原成記憶體中的物件。 2.引用型別資料和基礎型別資料均可以序列化

預設的序列化機制

若物件裡面也引用了其他物件,則預設的序列化機制會遞迴序列化,直到所有的物件都被序列化。(被引用的物件也需要支援序列化,否則不能被序列化,會丟擲NotSerializableException異常)。增加Address類,並在前面的Person類中增加Address類

package demo;
import java.io.Serializable;
public class Address implements Serializable {
    
    private static final long serialVersionUID = 8670510229642968881L;
    private String province;
    private String city;
    private String county;
    
    public Address(String province, String city, String county) {
        this.province = province;
        this.city = city;
        this.county = county;
    }

    public String getProvince() {
        return province;
    }

    public void setProvince(String province) {
        this.province = province;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getCounty() {
        return county;
    }

    public void setCounty(String county) {
        this.county = county;
    }

    @Override
    public String toString() {
        return "Address{" +
                "province='" + province + '\'' +
                ", city='" + city + '\'' +
                ", county='" + county + '\'' +
                '}';
    }
}

修改Person類

package demo;
import java.io.Serializable;
public class Person implements Serializable {
    private static final long serialVersionUID = -7763700527072968555L;
    private String name;
    private Integer age;
    private double height;
    private Address address;
   //重點貼出修改的區域性程式碼,其他保持不變

修改測試程式PersonTest.java

package demo;
import java.io.*;
class PersonTest {
    public static void main(String[] args) throws Exception {
        String filePath="C:\\test\\person.out";
        Address address = new Address("四川", "成都", "蒲江縣");
        Person person = new Person("zhangsan", 21,1.80,address);
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
        //將物件序列化儲存到檔案中
        outputStream.writeObject(person);
        outputStream.close();
        //將檔案中的位元組還原為記憶體中的物件(反序列化)
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(filePath));
        Person readObject = (Person) inputStream.readObject();
        inputStream.close();
        System.out.println(readObject);
    }
}

執行測試程式,結果如下

Person的有參構造器
Person{name='zhangsan', age=21, height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}}

控制序列化

  1. 物件中的欄位可能因為某些原因,只需要序列化部分欄位,例如,密碼等欄位由於安全原因不需要序列化,此時可以用transient關鍵字修飾不需要序列化的欄位。修改前面的Person類
package demo;
import java.io.Serializable;
public class Person implements Serializable {
    private static final long serialVersionUID = -7763700527072968555L;
    private  String name;
    private transient Integer age;
    private transient double height;
    private transient Address address;   
    //重點貼出修改的區域性程式碼,其他保持不變   

執行測試程式,結果如下:

Person的有參構造器
Person{name='zhangsan', age=null, height=0.0, address=null}

可以觀察到age,height,address欄位沒有被序列化儲存到檔案中,輸出的值是均為其對應型別的預設值,並且transient關鍵字可以用在任何型別

  1. 精確控制序列化過程 Externalizable介面,其為Serializable的子介面,提供writeExternal(ObjectOutput out)控制序列化,readExternal(ObjectInput in)控制反序列化。修改Person類使其同時實現兩個介面Serializable和Externalizable
package demo;

import java.io.*;

public class Person implements Externalizable, Serializable {


    private static final long serialVersionUID = -7763700527072968555L;
    private  String name;
    private  Integer age;
    private  double height;
    private  Address address;

    public Person(String name, Integer age,double height,Address address) {
        System.out.println("Person的有參構造器");
        this.name = name;
        this.age = age;
        this.height=height;
        this.address=address;
    }

    public Person() {
    }
  //省略setter和getter方法  

    public void writeExternal(ObjectOutput out) throws IOException {
        //暫時沒有實現
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
       //暫時沒有實現
    }
}

執行測試程式,結果如下:

Person的有參構造器
Person{name='null', age=null, height=0.0, address=null}

結果可以看到Person物件在實現Externalizable介面沒有反序列化成功,可以猜測若同時實現Externalizable和Serializable介面,以Externalizable介面為準

重新修改Person物件,並新增必要的用於測試的方法,貼出修改部分

package demo;
import java.io.*;
public class Person implements Externalizable {
    private String name;
    private Integer age;
    private double height;
    private Address address;

    public Person(String name, Integer age, double height, Address address) {      
        this.name = name;
        this.age = age;
        this.height = height;
        this.address = address;
    }
    
    public Person() {
        System.out.println("Person的無參構造器");
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        //手動控制序列化過程
        out.writeObject(name);
        out.writeObject(age);
        out.writeDouble(height);
        out.writeObject(address);

    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //序列化欄位順序需要和反序列化欄位順序一致
        name = ((String) in.readObject());
        age = ((Integer) in.readObject());
        height = in.readDouble();
        address = ((Address) in.readObject());
    }
}

執行測試程式,結果如下:

Person的無參構造器
Person{name='zhangsan', age=21, height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}}

從結果得出:1. 物件序列化和反序列化成功。2. Externalizable介面實現類物件在反序列化過程中會呼叫無參構造器

修改Person物件序列化和反序列化方法,使其順序不對應

  public void writeExternal(ObjectOutput out) throws IOException {
        //手動控制序列化過程
        out.writeObject(name);
        out.writeObject(age);
        out.writeDouble(height);
        out.writeObject(address);
    }

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        //手動控制反序列化過程
        name = ((String) in.readObject());
        age = ((Integer) in.readObject());
        address = ((Address) in.readObject());  //此處的address欄位和height欄位沒有對應
        height = in.readDouble();

    }

執行測試程式,結果如下:

Person的無參構造器
Exception in thread "main" java.io.OptionalDataException
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1365)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
	at demo.Person.readExternal(Person.java:81)
	at java.io.ObjectInputStream.readExternalData(ObjectInputStream.java:1849)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1806)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
	at demo.PersonTest.main(PersonTest.java:16)

從結果得出:1. Externalizable介面實現類物件反序列化過程首先是呼叫Person類的無參構造器,然後再根據儲存的二進位制資料還原成記憶體中的物件。2.序列化過程欄位的順序和反序列化過程欄位順序需要保持一致,否則會丟擲異常

Externalizable介面實現類在序列化過程中,只序列化部分欄位,可以實現transient關鍵字的效果,當然也需要保持序列化和反序列化過程欄位順序一致

對比Externalizable和Serializable介面可知:1. Serializable介面反序列化過程不會呼叫無參構造器,完全以二進位制資料恢復物件。2.Externalizable會呼叫無參構造器,然後以將二進位制的資料放入到生成的物件中

  1. 通過在Serializable介面中新增特定方法實現序列化和反序列化控制,修改Person類如下:

    package demo;
    import java.io.*;
    public class Person implements Serializable {
        
       private static final long serialVersionUID = -7763700527072968555L;
        private String name;
        private Integer age;
        private double height;
        private transient Address address;//address屬性此時不能被序列化
    
        public Person(String name, Integer age, double height, Address address) {
            this.name = name;
            this.age = age;
            this.height = height;
            this.address = address;
        }
        public Person() {
            System.out.println("Person的無參構造器");
        }
        
       //新增的方法必須為這個簽名
        private void writeObject(ObjectOutputStream stream)
                throws IOException {
            stream.defaultWriteObject();//執行預設的遞迴序列化機制,不會序列化address屬性
            stream.writeObject(address);//顯示將transient關鍵字標記的欄位序列化
        }
     //新增的方法必須為這個簽名
        private void readObject(ObjectInputStream stream)
                throws IOException, ClassNotFoundException {
            stream.defaultReadObject();
            address = (Address) stream.readObject();//顯示將transient關鍵字標記的欄位反序列化並賦值到相應的屬性
        }
    }
    
    

    執行測試程式,結果如下:

    Person{name='zhangsan', age=21, height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}}
    

    結果可以看到:1. 新增的方法在序列化和反序列化過程中會自動呼叫。2. 顯示序列化的優先順序高於transient關鍵字的優先順序

    注意觀察新增的兩個方法,均是私有的private,按照開發習慣,通常這種標準的方法應該新增在介面中,但因為介面中的方法必須是public的,因此這兩個方法不能新增到介面,在新增方法的時候要嚴格按照簽名新增。此外,方法定義為private,意味著方法只能在類的內部呼叫,可是Person類中並沒有呼叫,實際上Person類的writeObject呼叫是由java.io.ObjectStreamClass物件使用反射呼叫的

     void invokeWriteObject(Object obj, ObjectOutputStream out)
            throws IOException, UnsupportedOperationException
        {
            requireInitialized();
            if (writeObjectMethod != null) {
                try {
                    writeObjectMethod.invoke(obj, new Object[]{ out }); //此處通過反射呼叫物件的writeObject方法
                } catch (InvocationTargetException ex) {
                    Throwable th = ex.getTargetException();
                    if (th instanceof IOException) {
                        throw (IOException) th;
                    } else {
                        throwMiscException(th);
                    }
                } catch (IllegalAccessException ex) {               
                    throw new InternalError(ex);
                }
            } else {
                throw new UnsupportedOperationException();
            }
        }
    

    writeObjectMethod則是在ObjectStreamClass物件的構造器中初始化:

    if (externalizable) {
        cons = getExternalizableConstructor(cl);
    } else {
        cons = getSerializableConstructor(cl);
        writeObjectMethod = getPrivateMethod(cl, "writeObject",
                                             new Class<?>[] { ObjectOutputStream.class },
                                             Void.TYPE); //此處通過反射獲取writeObject方法物件,cl即為添加了writeObject方法的類的Class物件
        readObjectMethod = getPrivateMethod(cl, "readObject",
                                            new Class<?>[] { ObjectInputStream.class },
                                            Void.TYPE); //此處通過反射獲取readObject方法物件,cl即為添加了readObject方法的類的Class物件
        readObjectNoDataMethod = getPrivateMethod(
            cl, "readObjectNoData", null, Void.TYPE);
        hasWriteObjectData = (writeObjectMethod != null);
    }
    

    ObjectStreamClass類是對可序列化類的描述,裡面封裝了一些描述屬性,ObjectStreamClass物件的建立通過lookup方法

        private Class<?> cl;   //序列化類的Class物件
        private String name;  //序列化類的名字
        private volatile Long suid;  //序列化類的serialVersionUID
        private boolean isProxy;   //序列化類是否是動態代理類
        private boolean isEnum;  //序列化類是否是列舉
        private boolean serializable;  //序列化類是否實現了Serializable介面
        private boolean externalizable; //序列化類是否實現了Externalizable介面
        private Constructor<?> cons; //序列化適當的構造器,Serializable介面和Externalizable介面介面獲取構造器的方式不同
    

序列化版本serialVersionUID

前面生成的Person類中存在一個靜態欄位serialVersionUID,用作序列化和反序列化的版本控制,只有版本相同時,才能將位元組反序列化為記憶體中的物件,重新還原出物件的狀態,若序列化位元組中的serialVersionUID和類的serialVersionUID不一致,則會丟擲異常。

serialVersionUID可以顯示指定,如 private static final long serialVersionUID = 8623666669972053776L;也可以不用指定,那麼就會根據Java™ Object Serialization Specification規定的類的資訊生成相應的serialVersionUID,一般都會顯示指定serialVersionUID ,因為不同的編譯器實現計算的serialVersionUID可能不同,進而導致丟擲InvalidClassException異常。

註釋測試程式的反序列化部分,首先進行序列化

package demo;
import java.io.*;
class PersonTest {
    public static void main(String[] args) throws Exception {
        String filePath="C:\\test\\person.out";
        Address address = new Address("四川", "成都", "蒲江縣");
        Person person = new Person("zhangsan", 21,1.80,address);
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
        //將物件序列化儲存到檔案中
        outputStream.writeObject(person);
        outputStream.close();
        //將檔案中的位元組還原為記憶體中的物件(反序列化)
//        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(filePath));
//        Person readObject = (Person) inputStream.readObject();
//        inputStream.close();
//        System.out.println(readObject);
    }
}

修改Person類的serialVersionUID,使其版本加一

註釋測試程式的序列化部分,進行反序列化

package demo;
import java.io.*;
class PersonTest {
    public static void main(String[] args) throws Exception {
        String filePath="C:\\test\\person.out";
//        Address address = new Address("四川", "成都", "蒲江縣");
//        Person person = new Person("zhangsan", 21,1.80,address);
//        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
//        //將物件序列化儲存到檔案中
//        outputStream.writeObject(person);
//        outputStream.close();
//        將檔案中的位元組還原為記憶體中的物件(反序列化)
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(filePath));
        Person readObject = (Person) inputStream.readObject();
        inputStream.close();
        System.out.println(readObject);
    }
}

執行測試程式,結果如下:

Exception in thread "main" java.io.InvalidClassException: demo.Person; local class incompatible: stream classdesc serialVersionUID = 8623666669972053776, local class serialVersionUID = 8623666669972053777
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
	at demo.PersonTest.main(PersonTest.java:16)

結果可以看出:丟擲了InvalidClassException異常,並且提示指出兩個類的版本不相容(incompatible),同時給出了類的版本號

序列化的常常用在遠端呼叫中,將本地的物件序列化為位元組,並通過網路傳輸給遠端的一個機器,遠端的機器通過反序列化還原成記憶體中的物件,實現資料的傳遞。現在考慮本地機器和遠端機器在serialVersionUID不變的情況下,增減欄位對序列化和反序列化的影響,serialVersionUID不一致無法序列化,因此不考慮。本地和遠端增減欄位總共4中情況,假設+表示增加欄位,-表示減少欄位,沒有符號則表示欄位不變,欄位增減修改Person類

  1. 本地+sex,遠端 (表示本地增加sex欄位,遠端版本欄位保持不變):測試時先增加sex進行序列化,表示本地版本。然後刪除sex進行反序列化,表示遠端版本,測試結果如下:

    Person{name='zhangsan', age=21, height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}}
    

    結果可以看出:序列化檔案中多餘的欄位對反序列化沒有影響,遠端版本中沒有相應的欄位,自然沒有欄位值

  2. 本地,遠端+sex:測試時,先進行序列化,表示本地版本,然後新增sex進行反序列化,表示遠端版本

    Person{name='zhangsan', age=21, height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}, sex='null'}
    

    結果可以看出:遠端版本中多餘的欄位,由於序列化檔案中沒有相應的欄位值,因此遠端版本中的欄位取其型別的預設值

  3. 本地-age,遠端:測試時,先刪除age並進行序列化,表示本地版本,然後增加age欄位並進行反序列化,表示遠端版本

    Person{name='zhangsan', age=null, height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}}
    

    結果可以看出:由於本地序列化檔案中缺少age欄位,因此反序列化的類中age欄位取預設值

  4. 本地,遠端-age:測試時,先進行序列化,表示本地版本,然後刪除age,程序反序列化,表示遠端版本

    Person{name='zhangsan', height=1.8, address=Address{province='四川', city='成都', county='蒲江縣'}}
    

    結果可以看出:由於遠端版本缺少age欄位,因此即使序列化檔案中有age值,反序列化後的類中任然沒有age欄位值

    綜上:1. 只要版本一致,增減欄位對序列化和反序列化過程沒有影響。2.反序列化過程不會強制要求屬性個數一致,若反序列化時,欄位少了,自然就沒有相應是欄位值,若反序列化時,欄位多了,多餘的欄位會取預設值。

序列化使用注意事項

  1. 序列化儲存的是物件的狀態,因此靜態變數不會被序列化,反序列自然也沒有值。修改Person類,增加靜態屬性country

    package demo;
    import java.io.*;
    public class Person implements Serializable {
        private static final long serialVersionUID = 8623666669972053776L;
    
        public static String COUNTRY;
        private String name;
        private Integer age;
        private double height;
        private Address address;
    	//省略未修改程式碼
    }
    
    

    修改測試程式並序列化

    package demo;
    import java.io.*;
    class PersonTest {
        public static void main(String[] args) throws Exception {
            String filePath = "C:\\test\\person.out";
            Address address = new Address("四川", "成都", "蒲江縣");
            Person person = new Person("zhangsan", 21, 1.80, address);
            Person.COUNTRY = "中國";   //賦值靜態屬性
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
            outputStream.writeObject(person);
            outputStream.close();
        }
    }
    

    反序列化,結果如下:

    Person{name='zhangsan', age=21, height=1.8, country=null, address=Address{province='四川', city='成都', county='蒲江縣'}}
    

    結果可以看出:country靜態屬性沒有被序列化,取值為預設值

  2. 基本資料型別可以直接被序列化,引用型別資料需要實現Serializable或Externalizable介面,才能序列化

  3. 預設序列化機制會遞迴序列化,若物件還引用了其他物件,則其他物件也需要支援序列化,否則會丟擲NotSerializableException,若不想序列化引用的物件,可以使用transient關鍵字

  4. 若一個類不支援序列化,但其父類支援序列化,則這個類也支援序列化。擴充套件Person類,增加Student類

    package demo;
    public class Student extends Person {
        private String schoolName;
        private String grade;
    	//省略setter,getter和toString方法
    }
    

    增加測試程式StudentTest.java

    package demo;
    import java.io.*;
    class StudentTest {
        public static void main(String[] args) throws Exception {
            String filePath = "C:\\test\\student.out";
            Student student = new Student("七中", "高一");
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
            outputStream.writeObject(student);
            outputStream.close();
    
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(filePath));
            Person readObject = (Person) inputStream.readObject();
            inputStream.close();
            System.out.println(readObject);
        }
    }
    

    測試結果如下:

    Person的無參構造器  
    Student{schoolName='七中', grade='高一'}
    

    結果可以看出:1. 若父類實現了序列化介面,則這類可以序列化(其實就是父類具有了某項功能,子類可通過繼承,擁有其功能)

  5. 若子類可以序列化,但父類不能序列化,子類是可以序列化的。(java中所有類的最頂層父類都是Object,Object類不能序列化,但是String實現了Serializable介面,可以序列化)子類擁有的功能不受到父類的影響。

  6. 單例類在預設反序列化的時候,會被破壞,導致多個例項。建立單例類Company

    package demo;
    public class Compony implements Serializable{
        private static final long serialVersionUID = -7328519208950924476L;
        //餓漢式單例
        private static final Compony COMPONY = new Compony();
        private Compony() {
            //防止在類的外面new物件
            System.out.println("Compony的私有構造器");
        }
        //通過該靜態方法對外暴露該類的唯一例項
        public static Compony getInstance() {
            return COMPONY;
        }
    }
    
    

    增加測試程式,測試反序列化後得到的Compony類是不是單例

    package demo;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    class ComponyTest {
        public static void main(String[] args) throws Exception {
            String filePath = "C:\\test\\compony.out";
            Compony compony = Compony.getInstance();
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(filePath));
            outputStream.writeObject(compony);
            outputStream.close();
    
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(filePath));
            Compony readObject = (Compony) inputStream.readObject();
            inputStream.close();
            System.out.println("Compony是否是單例類:" + (readObject == Compony.getInstance()));
        }
    }
    

    執行測試,結果如下:

    Compony是否是單例類:false
    

    從結果可以看出:1. 單例類在反序列化後被破壞,生成了一個新的物件。由於反序列化過程中會使用ObjectInputStream的readObject方法,因此從此方法追蹤

    try {
        Object obj = readObject0(false);//生成物件
       //去除無關程式碼
        return obj;
    } finally {
        passHandle = outerHandle;
        if (closed && depth == 0) {
            clear();
        }
            }
    

    繼續追蹤:

       ObjectStreamClass desc = readClassDesc(false);
            desc.checkDeserialize(); //此時的desc物件即是對序列化類的描述物件
    
            Object obj;
            try {
                obj = desc.isInstantiable() ? desc.newInstance() : null;  //此處通過反射建立物件
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
    

    繼續向下:

    Object newInstance()
            throws InstantiationException, InvocationTargetException,
                   UnsupportedOperationException
        {
         
            if (cons != null) {
                try {
                    return cons.newInstance();   //此處便是核心的通過Constructor的newInstance反射建立物件
                } catch (IllegalAccessException ex) {
                    // should not occur, as access checks have been suppressed
                    throw new InternalError(ex);
                }
            } else {
                throw new UnsupportedOperationException();
            }
        }      
    

    看到這裡是否有疑問,Serializable實現類反序列化的時候不是不會呼叫構造器嗎?怎麼會通過構造器反射建立物件,實際程式碼輸出也可以看出是沒有呼叫無參構造器的,那此時這個cons是哪個的構造器呢?在此處斷點除錯在這裡插入圖片描述

    可以看出此處的cons是Object的構造器,並不是Compony類的無參構造器

    cons的初始化程式碼如下

    //根據不同的介面實現類初始化cons
    if (externalizable) {
        cons = getExternalizableConstructor(cl); 
    } else {
        cons = getSerializableConstructor(cl);
       //去除無關程式碼    
    }
    

    綜上:在反序列化的過程中,會呼叫cons.newInstance()生成一個新的物件,通過在序列化類中新增readResolve方法可以保證類的單例,修改Compony

    package demo;
    import java.io.Serializable;
    
    public class Compony implements Serializable {
    	//省略未修改部分程式碼
        
        private Object readResolve() {
            return COMPONY;//返回值作為反序列化的物件
        }
    }
    
    

    執行測試程式

    Compony的私有構造器
    Compony是否是單例類:true
    

    結果可以看到:1. 單例類沒有被破壞。觀察Compony的readResolve方法也是私有的,類內部並沒有呼叫,猜測也是在某處反射呼叫,入口依然是java.io.ObjectInputStream#readObject方法

    if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())//若類實現了serializable or externalizable介面,且定義了readResolve方法,則返回true
            {
                Object rep = desc.invokeReadResolve(obj);  //此處反射呼叫readResolve方法
                if (unshared && rep.getClass().isArray()) {
                    rep = cloneArray(rep);
                }
                if (rep != obj) {
                    handles.setObject(passHandle, obj = rep);
                }
            }
    

    繼續向下追蹤

     if (readResolveMethod != null) {
                try {
                    return readResolveMethod.invoke(obj, (Object[]) null);//此處正真反射呼叫readResolve方法
                } catch (InvocationTargetException ex) {
                   //去除無關程式碼
                } catch (IllegalAccessException ex) {              
                    throw new InternalError(ex);
                }
            } else {
                throw new UnsupportedOperationException();
            }
    

    readResolveMethod方法初始化程式碼:java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>)

    if (externalizable) {
            cons = getExternalizableConstructor(cl);
        } else {
            cons = getSerializableConstructor(cl);
            writeObjectMethod = getPrivateMethod(cl, "writeObject",
                                                 new Class<?>[] { ObjectOutputStream.class },
                                                 Void.TYPE);
            readObjectMethod = getPrivateMethod(cl, "readObject",
                                                new Class<?>[] { ObjectInputStream.class },
                                                Void.TYPE);
            readObjectNoDataMethod = getPrivateMethod(
                cl, "readObjectNoData", null, Void.TYPE);
            hasWriteObjectData = (writeObjectMethod != null);
        }
        writeReplaceMethod = getInheritableMethod(
            cl, "writeReplace", null, Object.class);
        readResolveMethod = getInheritableMethod(
            cl, "readResolve", null, Object.class);//此處通過反射獲取定義的readResolve方法
        return null;
                    }
    

    總結:單例類反序列化的時候需要注意單例類會被破壞,需要新增readResolve方法