java序列化和反序列化基础学习

一、前言

前文分析了java的反序列化的DNSURL利用链,但是对于java反序列化的一些过程不是很了解,这篇主要记录下学习java反序列基础知识

二、原理

概念

1、什么是序列化和反序列化

(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;

(2)**序列化:**对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。

(3)**反序列化:**客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。

2、为什么需要序列化和反序列化

(1)利用序列化实现远程通信,即在网络上传送对象的字节序列。

(2)java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据:可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的"深复制",即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列

(3)序列化可以将内存中的类写入文件或数据库中

3、实现Java对象序列化与反序列化的方法c

假定一个User类,它的对象需要序列化,可以有如下三种方法:

(1)若User类仅仅实现了Serialzable接口

ObjectOutputStream采用默认的序列化方式,对User对象的费Transient的实例变量进行序列化

ObjectInputStream采用默认的反序列化方式,对User对象的非Transient的实例变量进行反序列化

(2)若User类仅实现Serialzable接口,并且内部还自定义了readObject(ObjectInputStream in) 和writeObject(ObjectOutputStream out)

ObjectOutputStream 调用User对象的writeObjcet(ObjectOutputStream out)方法进行序列化

ObjectInputStream 调用User对象的readObject(ObjectInputStream in)方法进行反序列化

(3)若User类实现了Externalnalizable接口,且User类必须实现readExternal(ObjectInput in) 和 writeExternal(ObjectOutput out)方法

ObjectOutputStream 调用 User对象的writeExternal(ObjectOutput out)的方法进行序列化。

ObjectInputStream 调用User对象的readExternal(Object in)的方法进行反序列化。

序列化的接口及特点

Serializable接口

此接口是java提供的序列化接口,这其实是一个空接口

java 复制代码
public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。

当然里面也记录很多的东西,比如:

java 复制代码
 * Classes that require special handling during the serialization and
 * deserialization process must implement special methods with these exact
 * signatures:
 *//在序列化和反序列化过程中需要特殊处理的类必须使用这些确切的方法实现特殊方法
 * <PRE>
 * private void writeObject(java.io.ObjectOutputStream out)
 *     throws IOException
 * private void readObject(java.io.ObjectInputStream in)
 *     throws IOException, ClassNotFoundException;
 * private void readObjectNoData()
 *     throws ObjectStreamException;
 * </PRE>
1、基本使用

通过ObjectOutputStream 将需要序列化数据写入到流中,因为Java IO是一种装饰者模式,因此可以通过 ObjectOutStream包装 FileOutStream 将数据写入到文件中或者包装ByteArrayOutStream 将数据写入到内存中。 同理,可以通过ObjectInputStream将数据从磁盘FileInputStream 或者内存ByteArrayInputStream 读取出来然后转化为指定的对象即可。

2、ObjectOutputStream

ObjectOutputStream 将java对象的原始数据类型和图形写入OutputStream,可以使用ObjectInputStream读取(重构)对象。可以通过使用流的文件来完成对象的持久存储。如果流是网络套接字流,则可以在另一个主机或另一个进程中重新构建对象。

writeObject方法用于将对象写入流。任何对象,包括字符串和数组,都是用writeObject编写的。可以将多个对象或基元写入流中。必须从相应的ObjectInputStream中读取对象,这些对象具有与写入时相同的类型和顺序

也可以使用DataOutput中的适当方法将原始数据类型写入流中。也可以使用writeUTF方法编写字符串

对象的默认序列化机制会对写入的类,类签名以及所有非瞬态(transient)和非静态字段的值。对其他对象的引用(瞬态和静态字段除外)也会导致这些对象写入。使用引用共享机制对单个对象的多个引用进行编码,以便可以将对象的图形恢复与写入原始图像时相同的形状。
注意项:

对象的默认序列化机制会写入对象的类,类签名以及所有非瞬态和非静态字段的值。 对其他对象的引用(瞬态或静态字段除外)也会导致这些对象被写入。 

什么是类签名?

在开发 JNI( Java Native Interface , Java 本地接口 ) 时需要调用 Java 层的方法或创建引用 , 此时就会用到 Java 签名机制 . 比如基本数据类型的签名如下所示:

具体参考1. java的数据类型的签名 - 简书

什么是非瞬态(transient)字段?

瞬态变量( Transient ) 是一个 Java 关键词 , 它用于标记类的成员变量在持久化到字节流时不要被序列化 ; 在通过网络套接字流传输字节流时 , transient 关键词标记的成员变量不会被序列化 .
因此 , 如果仅想序列化某个类中部分变量 , 除了可以通过继承 Externalizable 接口来指定需要序列化的成员变量 ; 还可以将其他变量添加 transient 关键词 , 使得变量不被序列化 .
3、ObejctInputStream

ObjectOutputStream相反,可以将数据流重构成对象

ObjectInputStream 类在重构对象时会从本地 JVM 虚拟机中加载对应的类 , 以确保重构时使用的类与被序列化的类是同一个 . 也就是说 : 反序列化进程的 JVM 虚拟机中必须加载被序列化的类 .

   public final Object readObject()
        throws IOException, ClassNotFoundException {
        return readObject(Object.class);
    }

返回值是Object,还需要通过强制类型转换成预期的类型

4、Serializable接口的特点
(1) 序列化类的属性没有实现 Serializable 那么在序列化就会报错
(2)在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。

Animal 是父类,它没有实现 Serilizable 接口

java 复制代码
Animal.class
public class Animal{
    private String color;
    
    public Animal(){//没有无参构造将会报错
        System.out.println("调用Animal 无参构造");
    }


    public Animal(String color) {

        this.color = color;
        System.out.println("调用Animal 有color参数的构造");
    }

    @Override
    public String toString(){
        return "Animal{" + "color='" + color + '\'' + '}';
    }
}

BlackCat 是Animal的子类

java 复制代码
BalckCat.java

import java.io.Serializable;

public class BlackCat extends Animal implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;

    public BlackCat(){
        super();
        System.out.println("调用黑猫的无参构造");
    }

    public BlackCat(String color, String name){
        super(color);
        this.name = name;
        System.out.println("调用黑猫的 color参数的构造");
    }

    @Override
    public String toString(){
        return "BlackCat{" + "name='" + name + '\'\t' + super.toString() + '\'' + '}';
    }
}

SuperMain测试类

java 复制代码
SuperMain.java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SuperMain {
    private static final String FILE_PATH = "/.super.bin";

    public static void main(String[] args) throws Exception{
        serializeAnimal();
        deserializeAnimal();
    }

    private static void serializeAnimal()  throws Exception{
        BlackCat black = new BlackCat("black", "我是黑猫");
        System.out.println("序列化前: " + black.toString());
        System.out.println("=================开始序列化==============");

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
        oos.writeObject(black);
        oos.flush();
        oos.close();
    }

    private static void deserializeAnimal() throws Exception{
        System.out.println("==============开始反序列化===================");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
        BlackCat black = (BlackCat) ois.readObject();
        ois.close();
        System.out.println(black);
    }
}

输出结果

从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。

(3)一个实现Serializable接口的子类也是可以被序列化的
(4)静态成员变量是不能被序列化的

序列化是针对对象属性的,而静态成员变量是属于类的。这里"不能序列化"的意思是序列化信息中不包含这个静态成员域

(5)transient标识的对象成员变量不参与序列化
java 复制代码
MyList.java

import java.io.Serializable;
import java.util.Arrays;

public class MyList implements Serializable {
    private String name;

    /*
    transient 表示该成员不需要被序列化
     */
    private transient Object[] arr;

    public MyList(){

    }

    public MyList(String name){
        this.name = name;
        this.arr = new Object[100];
        //给前面的30个元素进行初始化
        for (int i = 0; i < 30; i++){
            this.arr[i] = i;
        }
    }

    @Override
    public String toString(){
        return "MyList{" + "name='" + name + '\'' + ", arr=" + Arrays.toString(arr) + '}';
    }


    //================自定义序列化反序列化 arr元素===================
    private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
        //执行jvm的默认序列化操作
        s.defaultWriteObject();

        //手动序列化arr 前面30个元素f
        for (int i = 0; i < 30; i++){
            s.writeObject(arr[i]);
        }
    }

    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException,ClassNotFoundException{
        s.defaultReadObject();
        arr = new Object[30];
        for (int i = 0; i < 30 ; i++){
            arr[i] = s.readObject();
        }
    }
}

测试类

java 复制代码
TransientMain.java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class TransientMain {
    private static final String FILE_PATH = "./transient.bin";
    public static void main(String[] args) throws Exception{
        serializeMyList();


        deserializeMyList();
    }

    private static void serializeMyList() throws Exception{
        System.out.println("=============开始序列化============");
        MyList  WriteMyList = new MyList("ArrayList");
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
        oos.writeObject(WriteMyList);
        oos.flush();
        oos.close();

    }

    /*
    1.如果 private  Object[] arr; 没有使用 transient ,那么整个数组都会被保存,而不是保存实际存储的数据
    输出结果:MyList{name='ArrayList', arr=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null]}
    2.private transient Object[] arr;设置了 transient,表示 arr 元素不进行序列化
    输出结果:MyList{name='ArrayList', arr=null}
    3.参考 ArrayList 处理内部的 transient Object[] elementData; 数组是通过 writeObject 和 readObject 实现的
    我们的 MyList 内部也可以借鉴这种方式实现transient元素的手动序列化和反序列化。
     */
    private static void deserializeMyList() throws Exception{
        System.out.println("============开始反序列化=============");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
        MyList readMyList = (MyList) ois.readObject();
        ois.close();
        System.out.println(readMyList);
    }
}

测试输出结果

通过自定义 writeObjectreadObject 方法,可以在序列化和反序列化过程中对 transient 属性进行特殊处理,以达到保留值的目的。在这个例子中,即使 arr 被标记为 transient,但在 writeObject 方法中,手动对前 30 个元素进行了序列化操作,而在 readObject 方法中,你手动对这些元素进行了反序列化操作。因此,在反序列化后,arr 中的前 30 个元素将保留其原始值,而其他元素则为 null

(6)Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片
(7)序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:
  • 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
  • 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

Externalizable接口

这是个继承自Serializable接口的接口

java 复制代码
public interface Externalizable extends java.io.Serializable {}

public interface Externalizable extends Serializable {
  	//将要序列化的对象属性通过 var1.wrietXxx() 写入到序列化流中
    void writeExternal(ObjectOutput var1) throws IOException;
		//将要反序列化的对象属性通过 var1.readXxx() 读出来
    void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException;
}

我们需要手动编写 writeExternal()方法和readExternal()方法 , 这两个方法将取代定制好的 writeObject()方法和 readObject()方法 . 那什么时候会使用 Externalizable 接口呢 ? 当我们仅需要序列化类中的某个属性 , 此时就可以通过 Externalizable 接口中的 writeExternal() 方法来指定想要序列化的属性 . 同理 , 如果想让某个属性被反序列化 , 通过 readExternal() 方法来指定该属性就可以了. 此外 , Externalizable 序列化/反序列化还有一些其他特性 , 比如 readExternal() 方法在反序列化时会调用默认构造函数 , 实现 Externalizable 接口的类必须要提供一个 Public 修饰的无参构造函数等等

java 复制代码
Person.java

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    /*
    * 实现了Externalizable这个接口需要提供无参构造,在反序列化时会检测
    * */
    public Person(){
        System.out.println("Person:empty");
    }

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        System.out.println("Person writeExternal...");
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException{
        System.out.println("person readExternal...");

        name =  (String) in.readObject();
        age = in.readInt();
    }

    @Override
    public String toString(){
        return "Person{" + "name='" + name + '\'' + ", age= " + age + '}';
    }

}
java 复制代码
ExternalizableMain.java

import java.io.*;

public class ExternalizableMain {
    private static final String FILE_PATH = "../person.bin";

    public static void main(String[] args) throws IOException, ClassNotFoundException{
        Person person = new Person("zhangsan", 15);
        System.out.println(person.toString());
        System.out.println("=========序列化========");
        serializable(person, FILE_PATH);

        System.out.println("=========反序列化============");
        person = (Person) deserializable(FILE_PATH);
        System.out.println(person.toString());
    }


    private static void serializable(Object o, String path) throws IOException{
        FileOutputStream boas = new FileOutputStream(path);
        ObjectOutputStream oos = new ObjectOutputStream(boas);
        oos.writeObject(o);
        oos.close();
        boas.close();

    }

    private static Object deserializable(String path) throws IOException, ClassNotFoundException{
        ObjectInputStream bis = new ObjectInputStream(new FileInputStream(path));
        Object obj = bis.readObject();
        return obj;
    }
}

测试结果:

序列化ID serialVersionUID

在上述的举例中,其实可以看到好几次添加过 serialVersionUID 字段,这便是序列化ID

serialVersionUID 是 Java 序列化机制中用于版本控制的一个字段,它是一个长整型数值。这个字段的作用是在序列化和反序列化过程中用来验证类的版本一致性。

具体来说,serialVersionUID 的作用有以下几个方面:

  1. 版本控制: serialVersionUID 可以确保在序列化和反序列化过程中,序列化的类和反序列化的类是同一个版本的。如果两个类的 serialVersionUID 不一致,反序列化操作会抛出 InvalidClassException 异常,从而避免因版本不一致导致的错误。

  2. 兼容性: 当类的结构发生变化时,可以手动指定 serialVersionUID 来确保新旧版本的类仍然可以相互序列化和反序列化。如果不指定 serialVersionUID,系统会根据类的结构自动生成一个 serialVersionUID,但这样可能会导致在类结构变化时无法正确地进行版本控制,从而导致反序列化失败。

  3. 反序列化兼容性: 如果两个版本的类具有相同的 serialVersionUID,则在反序列化时会尝试匹配字段名和类型,尽可能地填充字段值。这样可以确保在类结构变化时,仍然可以成功地反序列化旧版本的对象。

serialVersionUID 发生改变有三种情况:

  • 手动去修改导致当前的 serialVersionUID 与序列化前的不一样。
  • 我们根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,在类结构发生改变时(属性增加,删除或者类型修改了)这种也是会导致 serialVersionUID 发生变化。
  • 假如类结构没有发生改变,并且没有定义 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样。

在实际使用中,为了确保类的版本一致性和兼容性,建议手动指定 serialVersionUID。可以使用 serialver 命令或 IDE 提供的工具来生成 serialVersionUID,然后将其添加到类中。在类的结构发生变化时,应该更新 serialVersionUID 的值,以保证版本控制的正确性。

序列化的步骤与结构分析

1、序列化的步骤

(1)将对象实例相关的类元数据输出。

(2)递归地输出类的超类描述直到不再有超类。

(3)类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。

(4)从上至下递归输出实例的数据

2、序列化数据分析

在了解原理之前,先了解一下序列化的数据。需要用到SerializationDumper工具

https://github.com/NickstaDB/SerializationDumper

java 复制代码
D:\java\serializadump>java -jar SerializationDumper-v1.13.jar aced000573720004466c616700000000000000010200014c000874727565466c61677400124c6a6176612f6c616e672f537472696e673b787074000c7468697300697300666c6167

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 4 - 0x00 04
        Value - Flag - 0x466c6167
      serialVersionUID - 0x00 00 00 00 00 00 00 01
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 1 - 0x00 01
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 8 - 0x00 08
            Value - trueFlag - 0x74727565466c6167
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata
      Flag
        values
          trueFlag
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 12 - 0x00 0c
                Value - this is flag - 0x7468697300697300666c6167

字段具体介绍参考:

Java Object Serialization Specification: 6 - ObjectSerialization Stream Protocol

3、WriteObject原理分析

ObjectOutputStream 构造函数
java 复制代码
java.io.ObjectOutputStream.java
 
/**
     * Creates an ObjectOutputStream that writes to the specified OutputStream.
     * This constructor writes the serialization stream header to the
     * underlying stream; callers may wish to flush the stream immediately to
     * ensure that constructors for receiving ObjectInputStreams will not block
     * when reading the header.
     *
     * <p>If a security manager is installed, this constructor will check for
     * the "enableSubclassImplementation" SerializablePermission when invoked
     * directly or indirectly by the constructor of a subclass which overrides
     * the ObjectOutputStream.putFields or ObjectOutputStream.writeUnshared
     * methods.
//创建一个写入指定OutputStream的ObjectOutputStream。该构造函数将序列化流头写入底层流; 调用者可能希望立即刷新流,以确保接收 ObjectInputStreams 的构造函数在读取标头时不会阻塞。
      *
      // <p>如果安装了安全管理器,则当由覆盖 ObjectOutputStream.putFields 或ObjectOutputStream.writeUnshared 方法的子类构造函数直接或间接调用时,此构造函数将检查"enableSubclassImplementation"SerializedPermission。
     *
     * @param   out output stream to write to
     * @throws  IOException if an I/O error occurs while writing stream header
     * @throws  SecurityException if untrusted subclass illegally overrides
     *          security-sensitive methods
     * @throws  NullPointerException if <code>out</code> is <code>null</code>
     * @since   1.4
     * @see     ObjectOutputStream#ObjectOutputStream()
     * @see     ObjectOutputStream#putFields()
     * @see     ObjectInputStream#ObjectInputStream(InputStream)
     */
    public ObjectOutputStream(OutputStream out) throws IOException {
        verifySubclass();
        bout = new BlockDataOutputStream(out);
        handles = new HandleTable(10, (float) 3.00);
        subs = new ReplaceTable(10, (float) 3.00);
        enableOverride = false;
        writeStreamHeader();
        bout.setBlockDataMode(true);
        if (extendedDebugInfo) {
            debugInfoStack = new DebugTraceInfoStack();
        } else {
            debugInfoStack = null;
        }
    }

①bout:用于写入一些类元数据还有对象中基本数据类型的值

②enableOverride :false 表示不支持重写序列化过程,如果为 true ,那么需要重写writeObjectOverride 方法。这个一般不用管它。

③writeStreamHeader() 写入头信息,具体看下面分析。

ObjectOutputStream#writeStreamHeader()
java 复制代码
java.io.ObjectOutputStream.java
  
  /**
     * The writeStreamHeader method is provided so subclasses can append or
     * prepend their own header to the stream.  It writes the magic number and
     * version to the stream.
//提供了 writeStreamHeader 方法,以便子类可以将自己的标头附加或前置到流中。 它将magic和版本写入流中。
     *
     * @throws  IOException if I/O errors occur while writing to the underlying
     *          stream
     */
    protected void writeStreamHeader() throws IOException {
        bout.writeShort(STREAM_MAGIC);
        bout.writeShort(STREAM_VERSION);
    }

①STREAM_MAGIC 声明使用了序列化协议,bout 就是一个流,将对应的头数据写入该流中

②STREAM_VERSION 指定序列化协议版本

ObjectOutStream#writeObject(obj)

上面是 ObjectOutStream 构造中做的事,下面来看看具体 writeObject 方法内部做了什么事

java 复制代码
java.io.ObjectOutputStream.java

/**
     * Write the specified object to the ObjectOutputStream.  The class of the
     * object, the signature of the class, and the values of the non-transient
     * and non-static fields of the class and all of its supertypes are
     * written.  Default serialization for a class can be overridden using the
     * writeObject and the readObject methods.  Objects referenced by this
     * object are written transitively so that a complete equivalent graph of
     * objects can be reconstructed by an ObjectInputStream.
     *//将指定对象写入 ObjectOutputStream。将写入对象的类、类的签名以及类及其所有超类型的非瞬态和非静态字段的值。可以使用 writeObject 和 readObject 方法覆盖类的默认序列化。此对象引用的对象以传递方式写入,以便 ObjectInputStream 可以重建对象的完整等效图。
     * <p>Exceptions are thrown for problems with the OutputStream and for
     * classes that should not be serialized.  All exceptions are fatal to the
     * OutputStream, which is left in an indeterminate state, and it is up to
     * the caller to ignore or recover the stream state.
     *
     * @throws  InvalidClassException Something is wrong with a class used by
     *          serialization.
     * @throws  NotSerializableException Some object to be serialized does not
     *          implement the java.io.Serializable interface.
     * @throws  IOException Any exception thrown by the underlying
     *          OutputStream.
     */
    public final void writeObject(Object obj) throws IOException {
        if (enableOverride) {//默认不执行这里
            writeObjectOverride(obj);
            return;
        }
        try {
            writeObject0(obj, false);
        } catch (IOException ex) {
            if (depth == 0) {
                writeFatalException(ex);
            }
            throw ex;
        }
    }
ObjectOutStream#writeObject0()
java 复制代码
java.io.ObjectOutputStream.java

private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    ...
    try {
     
        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;
       
        //①
        desc = ObjectStreamClass.lookup(cl, true);
        ...
        //②
        if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
        // END Android-changed:  Make Class and ObjectStreamClass replaceable.
        } else if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
        		//③
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } 
    ...
}

① lookup 函数用于查找当前类的 ObjectStreamClass ,它是用于描述一个类的结构信息的,通过它就可以获取对象及其对象属性的相关信息,并且它内部持有该对象的父类的 ObjectStreamClass 实例。其内部大量使用了反射,可以去看看这个类的源码。下面看看它的构造函数

java 复制代码
java.io.ObjectOutputStream.java

private ObjectStreamClass(final Class<?> cl) {
    this.cl = cl;
    name = cl.getName();
    isProxy = Proxy.isProxyClass(cl);
    isEnum = Enum.class.isAssignableFrom(cl);
    serializable = Serializable.class.isAssignableFrom(cl);
    externalizable = Externalizable.class.isAssignableFrom(cl);
    Class<?> superCl = cl.getSuperclass();
  	//superDesc 表示需要序列化对象的父类的 ObjectStreamClass,如果为空,则调用 lookUp 查找
    superDesc = (superCl != null) ? lookup(superCl, false) : null;
		//localDesc 表示自己
    localDesc = this;
		...
}

② 根据 obj 的类型去执行序列化操作,如果不符合序列化要求,那么会③位置抛出 NotSerializableException 异常

ObjectOutputStream#writeOrdinaryObject
java 复制代码
java.io.ObjectOutputStream.java

private void writeOrdinaryObject(Object obj,
                                 ObjectStreamClass desc,
                                 boolean unshared)
    throws IOException
{
    ...
    try {
        desc.checkSerialize();
        //①
        bout.writeByte(TC_OBJECT);
        //②
        writeClassDesc(desc, false);
        handles.assign(unshared ? null : obj);
        //③
        if (desc.isExternalizable() && !desc.isProxy()) {
            writeExternalData((Externalizable) obj);
        } else {
        		//④
            writeSerialData(obj, desc);
        }
    } finally {
        if (extendedDebugInfo) {
            debugInfoStack.pop();
        }
    }
}

①写入类的元数据,TC_OBJECT. 声明这是一个新的对象,如果写入的是一个 String 类型的数据,那么就需要 TC_STRING 这个标识。

②writeClassDesc 方法主要作用就是自上而下(从父类写到子类,注意只会遍历那些实现了序列化接口的类)写入描述信息。该方法内部会不断的递归调用,我们只需要关系这个方法是写入描述信息就好了,读者可以查阅一下源码。

从这里可以知道,序列化过程需要额外的写入很多数据,例如描述信息,类数据等,因此序列化后占用的空间肯定会更大。

③ desc.isExternalizable() 判断需要序列化的对象是否实现了 Externalizable 接口,这个在上面已经演示过怎么使用的,在序列化过程就是在这个地方进行判断的。如果有,那么序列化的过程就会由程序员自己控制了哦,writeExternalData 方法会回调,在这里就可以愉快地编写需要序列化的数据拉。

④ writeSerialData 在没有实现 Externalizable 接口时,就执行这个方法

ObjectOutputstream#writeSerialData
java 复制代码
java.io.ObjectOutputStream.java

private void writeSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
		//① 
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
    
        ObjectStreamClass slotDesc = slots[i].desc;
        
        if (slotDesc.hasWriteObjectMethod()) {//②
            PutFieldImpl oldPut = curPut;
            curPut = null;
            SerialCallbackContext oldContext = curContext;
            if (extendedDebugInfo) {
                debugInfoStack.push(
                    "custom writeObject data (class \"" +
                    slotDesc.getName() + "\")");
            }
            try {
                curContext = new SerialCallbackContext(obj, slotDesc);
                bout.setBlockDataMode(true);
                slotDesc.invokeWriteObject(obj, this);
                bout.setBlockDataMode(false);
                bout.writeByte(TC_ENDBLOCKDATA);
            } finally {
                curContext.setUsed();
                curContext = oldContext;
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
            curPut = oldPut;
        } else {
            defaultWriteFields(obj, slotDesc);//③
        }
    }
}

① desc.getClassDataLayout 会返回 ObjectStreamClass.ClassDataSlot[] ,我们来看看 ClassDataSlot 类,可以看到它是封装了 ObjectStreamClass 而已,所以我们就简单的认为 ① 这一步就是用于返回序列化对象及其父类的 ClassDataSlot[] 数组,我们可以从 ClassDataSlot 中获取对应 ObjectStreamClass 描述信息。

java 复制代码
static class ClassDataSlot {
    /** class descriptor "occupying" this slot */
    final ObjectStreamClass desc;
    /** true if serialized form includes data for this slot's descriptor */
    final boolean hasData;
    ClassDataSlot(ObjectStreamClass desc, boolean hasData) {
        this.desc = desc;
        this.hasData = hasData;
    }
}

② 开始遍历返回的数组,slotDesc 这个我们就简单将其看成对一个对象的描述吧。hasWriteObjectMethod 表示的是什么呢?这个其实就是你要序列化这个对象是否有 writeObject 这个 private 方法,注意哦,这个方法并不是任何接口的方法,而是我们手动写的,读者可以参考 ArrayList 代码,它内部就有这个方法。那么这个方法的作用是什么呢?这个方法我们在上面也演示过具体的使用,它就是用于自定义序列化过程的,读者可以返回到上面看看如果使用这个 writeObject 实现自定义序列化过程的。注意:其实这个过程不像实现 Externalizable 接口那样,自己完全去自定义序列化数据。

java 复制代码
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
    //执行 JVM 默认的序列化操作
    s.defaultWriteObject();
    //手动序列化 arr  前面30个元素
    for (int i = 0; i < 30; i++) {
        s.writeObject(arr[i]);
    }
}

③ defaultWriteFields 这个方法就是 JVM 自动帮我们序列化了,

java 复制代码
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
    throws IOException
{
    Class<?> cl = desc.forClass();
 
    desc.checkDefaultSerialize();
    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    desc.getPrimFieldValues(obj, primVals);
    //①
    bout.write(primVals, 0, primDataSize, false);
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    desc.getObjFieldValues(obj, objVals);
    
    //②
    for (int i = 0; i < objVals.length; i++) {
        if (extendedDebugInfo) {
            debugInfoStack.push(
                "field (class \"" + desc.getName() + "\", name: \"" +
                fields[numPrimFields + i].getName() + "\", type: \"" +
                fields[numPrimFields + i].getType() + "\")");
        }
        try {
            writeObject0(objVals[i],
                         fields[numPrimFields + i].isUnshared());
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
}

这个方法主要分为以下两步

  • ① 写入基本数据类型的数据
  • ②写入引用数据类型的数据,这里最终又调用到了 writeObject0() 方法,读者可以返回到上面去看看具体的实现。

4、readObject 原理分析

从流中读取类的描述信息 ObjectStreamClass 实例,通过这个对象就可以创建出序列化的对象。

java 复制代码
ObjectStreamClass desc = readClassDesc(false);
...
  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);
}

读取该对象及其对象的父类的 ObjectStreamClass信息

java 复制代码
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();

然后遍历得到每一个 ObjectStreamClass 对象,将对应的属性值赋值给需要反序列化的对象。

java 复制代码
private void defaultReadFields(Object obj, ObjectStreamClass desc)
    throws IOException
{
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        throw new ClassCastException();
    }
    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    bin.readFully(primVals, 0, primDataSize, false);
    if (obj != null) {
        desc.setPrimFieldValues(obj, primVals);
    }
    int objHandle = passHandle;
    //从 ObjectStreamClass 中得到对象的所有 Field 信息
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    for (int i = 0; i < objVals.length; i++) {
        ObjectStreamField f = fields[numPrimFields + i];
        objVals[i] = readObject0(f.isUnshared());
        if (f.getField() != null) {
            handles.markDependency(objHandle, passHandle);
        }
    }
    if (obj != null) {
    		//将数据保存到对象中去
        desc.setObjFieldValues(obj, objVals);
    }
    passHandle = objHandle;
}

ps:上述原理分析摘自(太深入了 ,插个眼,目前也不是很理解)java序列化与反序列化全讲解_序列化和反序列号需要构造无参函数的意义-CSDN博客

参考链接

https://blog.csdn.net/mocas_wang/article/details/107621010

https://www.cnblogs.com/javazhiyin/p/11841374.html

https://www.cnblogs.com/myseries/p/11931512.html

相关推荐
憨子周1 小时前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
懒惰的bit2 小时前
基础网络安全知识
学习·web安全·1024程序员节
霖雨2 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
P.H. Infinity3 小时前
【RabbitMQ】07-业务幂等处理
java·rabbitmq·java-rabbitmq