跳至主要內容

序列化机制详解

Quest大约 7 分钟基础知识序列化

什么是Java序列化

程序运行时,可以在内存中创建Java对象实例,这些Java对象的生命周期会随JVM停止运行而消失,如果在JVM停止之后需要持久化内存中的Java对象,并随JVM重新运行时在内存中恢复这些Java对象,或者通过网络传输Java对象,这些场景都需要运用序列化。

序列化: 将Java对象转换成字节序列过程
反序列化: 将字节序列还原成Java对象过程

序列化常见应用场景

序列化有主要有两类用途:通过序列机制将Java对象持久化磁盘、让Java对象通过网络传输

序列化用途
序列化用途

常见应用场景

  • 将对象序列化为字节流后,可以将字节流保存至磁盘文件或存储到数据库中,实现对象的持久化存储,当需要时,再将保存的对象进行反序列化,这样可以在程序重新启动或数据恢复时重新加载对象。
  • 消息队列系统中,消息的发送之前需要将对象序列化为字节流,接收端再进行反序列化。
  • 对象通过网络传输(远程RPC调用时)之前需要先将对象序列化,接收到字节序列后再进行反序列化。

Java序列化API

java.io.Serializable接口

Serializable接口是Java序列化的核心接口,接口中没有方法或字段。实现该接口的类可以将其对象转换为字节流进行序列化,并且可以从字节流中反序列化为对象。

public interface Serializable {
}

java.io.Externalizable接口

Externalizable接口是Serializable接口的子接口,接口中定义了两个抽象方法:writeExternal()readExternal()两个方法,如果使用此接口实现序列化和反序列化,实现Externalizable接口的同时需要重写writeExternal()readExternal()

public interface Externalizable extends java.io.Serializable {

    void writeExternal(ObjectOutput out) throws IOException;

    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

java.io.ObjectOutputStream类

ObjectOutputStream类表示对象输出流,该类的writeObject(Object obj)方法可以指定对象进行序列化,再将字节序列写入输出流中。

java.io.ObjectInputStream类

ObjectInputStream类表示对象输出流,该类的readObject()方法从输入流中读取字节序列,反序列化为一个对象。

序列化底层原理

Serializable是一个空接口,如果保证实现了该接口就可以进行对象的序列化和反序列化呢?

public interface Serializable {
}

通过文末例子制造java.io.NotSerializableException异常堆栈,从堆栈信息进入序列化源码:
writeObject > writeObject0 > writeOrdinaryObject > writeSerialData > invokeWriteObject

Exception in thread "main" java.io.NotSerializableException: 
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1548)
	at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1509)
	at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at com.rbd.chat.util.Person.main(Person.java:52)

writeObject0方法中代码:

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

ObjectOutputStream 在序列化时,会判断被序列化的对象属于StringarrayenumSerializable其中哪一种类型,如果都不是抛出NotSerializableException异常,如果对象的类实现Serializable接口则继续调用writeOrdinaryObject方法实现序列化的下一步操作,所以Serializable接口的实现作为一个序列化标志。

序列化使用

定义一个Person类并实现Serializable接口:

public class Person implements Serializable {

    private String name;

    private String sex;

    private String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getAge() {
        return age;
    }

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

将对象序列化到文件中

通过ObjectOutputStream类中writeObject(Object obj)方法实现将对象输出到文件中:

ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/data/out.ser"));

Person person = new Person();
person.setName("张三");
person.setSex("男");
person.setAge(25);

outputStream.writeObject(person);

outputStream.flush();
outputStream.close();

输出的文件:

从文件中反序列化对象

通过ObjectInputStream类中readObject()方法反序列化对象:

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("/data/out.ser"));

Person person = (Person)inputStream.readObject();
inputStream.close();

System.out.println(person.toString());

运行结果:

序列化实践

serialVersionUID

反序列化时,会检查字节流中的serialVersionUID与本地类中的serialVersionUID进行比较,如果相同则认为是一致的执行反序列化,如果不同则认为序列化版本不一样会抛出InvalidCastException异常。
实现序列化接口的类中需要显式声明一个serialVersionUID变量,如果没有声明,序列化机制会自动生成一个serialVersionUID用于版本比较(没有声明的情况编译多次,serialVersionUID也不会改变,如果类做了变更,serialVersionUID也会相应的改变)

序列化对单例模式的影响

单例模式的目的是确保在JVM中只有一个实例对象的存在。但是如果将一个单例对象被序列化再反序列化时,会创建一个新的实例,破坏了单例的唯一性。这是因为序列化操作将对象保存到字节流中,而反序列化操作会从字节流中重新创建一个对象实例。为了保证单例的唯一性,可以在单例类中加入readResolve()方法,保证反序列化时返回相同对象。

1.以下创建一个实现序列化接口的Person单例类:

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private static Person instance;

    private Person() {
        // 私有构造函数
    }

    public static Person getInstance() {
        if (instance == null) {
            instance = new Person();
        }
        return instance;
    }
}

2.实例化Person类并将序列化保存到本地,反序列化后与实例对象进行比较:

import java.io.*;

public class Main {
    public static void main(String[] args) {
        try {
            // 序列化单例对象
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
            Person person = Person.getInstance();
            out.writeObject(person);
            out.close();

            // 反序列化单例对象
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
            Person deserializedPerson = (Person) in.readObject();
            in.close();

            // 判断是否为同一个实例
            System.out.println("是否为同一个实例对象: " +
                    (person == deserializedPerson));
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
//返回结果:是否为同一个实例对象: false

3.在Person类中加入readResolve()方法再进对象比较:

private Object readResolve() throws ObjectStreamException {
    return instance;
}
//返回结果:是否为同一个实例对象: true

transient 与 static 关键字修饰字段不会被序列化

transient: 变量修饰符,使用它修饰变量时,可以阻止变量的序列化,反序列化,transient修饰的变量不会被恢复。transient只能修饰变量,不能修饰类与方法。

static: 序列化是针对对象的,而不是类。静态变量与类是关联的,在类加载初始化时,在类的整个生命周期保持相同的值,所以,不需要将静态变量序列化到字节流中。

serialVersionUIDstatic 关键字修饰,为何会被序列化?

serialVersionUID是一个特殊的静态变量,不会被序列化和保存到字节流中,serialVersionUID变量只会用于JVM识别。

子类实现序列化,父类没有实现序列化,父类字段值丢失

子类接口实现了Serializable接口,父类接口没有实现,代表父类不会被序列化,如果需要父类属性的值不被丢失,父类也需要实现Serializable接口。

//定义没有实现Serializable接口父类
public class Person implements Serializable {
    
    private String personId;
}

//定义实现Serializable接口的子类
public class Male extends Person implements Serializable {

    private String name;

    private Integer age;
}

//将子类实例化设置属性值并序列持久化到文件,并反序列化对象输出值
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("person.ser"));

Male male = new Male();
male.setPersonId("109283X");
male.setName("张三");
male.setAge(25);

outputStream.writeObject(male);
outputStream.flush();
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("person.ser"));

Male male1 = (Male) inputStream.readObject();
inputStream.close();

System.out.println("序列化前personID:" + male.getPersonId());
System.out.println("反序列化后personID:" + male1.getPersonId());

//执行结果:
//序列化前personID:109283X
//反序列化后personID:null

序列化类中字段类型为对象,则该对象的类必须序列化

如果类里面含有引用类型的成员变量需要进行序列化时,引用类型的类(引用类型的类继承的父类或实现的接口实现了Serializable接口也满足)也必须实现Serializable接口,否则序列化时会抛出NotSerializableException异常。

public class Person  implements Serializable{

    private String personId;

    public Male male;
}

//引用类型的类不实现 Serializable接口
public class Male{

}

//执行序列化会抛出 java.io.NotSerializableException异常
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("person.ser"));

Person person = new Person();
person.setPersonId("109283X");
person.setMale(new Male());