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

news2024/11/18 17:28:27

一、前言

前文分析了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提供的序列化接口,这其实是一个空接口

public interface Serializable {
}

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

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

 * 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 接口

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的子类

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测试类

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标识的对象成员变量不参与序列化
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();
        }
    }
}

测试类

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接口的接口

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 修饰的无参构造函数等等

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 + '}';
    }

}
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

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.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.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.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.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.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.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.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 描述信息。

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 接口那样,自己完全去自定义序列化数据。

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 自动帮我们序列化了,

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 实例,通过这个对象就可以创建出序列化的对象。

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

ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1637156.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

24 JavaScript学习:this

this在对象方法中 在 JavaScript 中&#xff0c;this 的值取决于函数被调用的方式。在对象方法中&#xff0c;this 引用的是调用该方法的对象。 让我们看一个简单的例子&#xff1a; const person {firstName: John,lastName: Doe,fullName: function() {return this.firstN…

element的textarea字体与input字体不一致解决方案

实现 <style scoped> .el-textarea /deep/ .el-textarea__inner{font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif; } </style>效…

【Flutter】极光推送配置流程(小米厂商通道) 章二

前言 继【Flutter】极光推送配置流程(极光通道/华为厂商/IOS) 章一 并且&#xff0c;我大概率不会去修改第一篇文章的内容。 随着我自己在配置公司的项目的同时&#xff0c;我希望一直更新这个推送系列文章。 在章一配置完后&#xff0c;也是出现了一些问题&#xff0c;所以本…

【IC设计】CRC(循环冗余校验)

目录 理论解读CRC应用CRC算法参数解读常见CRC参数模型 设计实战校招编程题分类串行输入、并行计算、串行输出**串行计算、串行输出&#xff08;线性移位寄存器&#xff09;LSFR线性移位寄存器&#xff08;并转串&#xff09;(并行计算)模二除 总结——串行、并行计算的本质参考…

nowcoder——删除公共字符

删除公共字符_牛客题霸_牛客网 (nowcoder.com) 对于这个题其实就是删除字符串1中在字符串2中出现过的字符。我们来分析下解题步骤&#xff1a; 思路一&#xff1a;遍历字符串1&#xff0c;如果遍历到的字符在字符串2中出现&#xff0c;则将该字符之后的所有字符向前移一位。 …

[高质量]2024五一数学建模A题保奖思路+代码(后续会更新)

你的点赞收藏是我继续更新的最大动力&#xff0c;可点击文末卡片获取更多资料 你是否在寻找数学建模比赛的突破点&#xff1f; 作为经验丰富的数学建模团队&#xff0c;我们将为你带来2024 年华东杯&#xff08;A题&#xff09;的全面解析包。这个解决方案包不仅包括完整的代…

[C语言]典型例题:小蚂蚁爬橡皮筋、买汽水问题、导致单词块、菱形打印……

1、小蚂蚁爬橡皮筋问题 假设橡皮筋长4m&#xff0c;小蚂蚁从一端爬向另一端每天爬1m&#xff0c;且每爬了1m&#xff0c;橡皮筋会立马拉伸4m&#xff0c;在理想条件下&#xff0c;小蚂蚁需要爬多少天可以到达橡皮筋的另一端&#xff1f; 不仔细想&#xff0c;我们很可能认为小蚂…

LeetCode 543.二叉树的直径

题目描述 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,4,5]…

5个Python自动化EDA库

EDA或探索性数据分析是一项耗时的工作&#xff0c;但是由于EDA是不可避免的&#xff0c;所以Python出现了很多自动化库来减少执行分析所需的时间。EDA的主要目标不是制作花哨的图形或创建彩色的图形&#xff0c;而是获得对数据集的理解&#xff0c;并获得对变量之间的分布和相关…

OpenFeign修改HttpClient为Apache HttpClient 5

OpenFeign中http client 如果不做特殊配置&#xff0c;OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求&#xff0c; 由于默认HttpURLConnection没有连接池、性能和效率比较低。所以修改为Apache HttpClient 5。 总结为两步&#xff1a; 加依赖改yml 具体操作请往…

uniapp + uView动态表单校验

项目需求&#xff1a;动态循环表单&#xff0c;并实现动态表单校验 页面&#xff1a; <u--form label-position"top" :model"tmForm" ref"tmForm" label-width"0px" :rulesrules><div v-for"(element, index) in tmForm…

基于Springboot+Vue的Java项目-家政服务平台系统开发实战(附演示视频+源码+LW)

大家好&#xff01;我是程序员一帆&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &am…

如果通过Glide 设置图片圆角

要给图片设置一个圆角,通常方法是在ImageView 标签外添加一个CardView 标签,然后设置圆角值,但是今天遇到一个问题就是 RecyclerView Item 中这样操作的话会遇到这样的一个报错: Cannot call this method while RecyclerView is computing a layout or scrolling androidx.rec…

数据结构----顺序表详解

顺序表的定义 顺序表&#xff08;SeqList&#xff09;属于线性表的同一种&#xff0c;它同样具有线性的存储结构&#xff0c;以下是百度百科关于顺序表的定义&#xff1a; 总结下来&#xff0c; 在结构上&#xff0c;顺序表实际上的底层结构就是数组&#xff0c;而顺序表本身也…

链表-----返回倒数第K个节点回文结构的判断相交链表

目录 1.返回倒数第K个节点 2.回文结构的判断 3.相交链表的判断&#xff0c;返回交点 1.返回倒数第K个节点 &#xff08;1&#xff09;返回链表的第k个节点&#xff0c;我们这里的做法是定义两个指针&#xff0c;这两个指针之间相差的是k这个长度&#xff1b;这个过程的实现就…

网络安全知识点

网络安全 1&#xff0e; 网络安全的定义&#xff0c;网络安全的属性。 定义&#xff1a;针对各种网络安全威胁研究其安全策略和机制&#xff0c;通过防护、检测和响应&#xff0c;确保网络系统及数据的安全性。 属性&#xff1a;机密性 认证&#xff08;可鉴别性&#xff09…

手把手带你一起搭建Seata,结合SpringCloud alibaba实战(二)

手把手带你一起搭建Seata&#xff0c;结合SpringCloud alibaba实战&#xff08;二&#xff09; 前言具体实现大致流程配置微服务订单服务库存服务 测试订单服务异常库存服务异常 总结 接下来的一段时间论文解说要暂时放一放&#xff0c;咱们一起来了解下微服务方面的知识&#…

Web-SpringBootWeb

创建项目 后面因为报错&#xff0c;所以我把jdk修改成22&#xff0c;仅供参考。 定义类&#xff0c;创建方法 package com.start.springbootstart.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotati…

使用nacos实现注册中心和配置中心

实现注册中心 在pom文件中导入 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> 在bootstrap.yml中写下如下配置 spring:application:name: c…

安卓中对象序列化面试问题及回答

1. 什么是对象的序列化&#xff1f; 答&#xff1a; 序列化是将对象转换为字节流的过程&#xff0c;以便将其存储在文件、数据库或通过网络传输。反序列化则是将字节流重新转换为对象的过程。 2. 为什么在 Android 开发中需要对象的序列化&#xff1f; 答&#xff1a; 在 An…