序列化

一、作用

  • 序列化是指把一个 Java 对象变成二进制内容(字节序列),本质上就是一个 byte[] 数组。

    将数据从程序 (内存) 中写到磁盘、光盘等存储设备中,输出流。

    为什么要把 Java 对象序列化呢?因为序列化后可以把 byte[] 保存到文件中,或者把 byte[] 通过网络传输到远程,这样,就相当于把 Java 对象存储到文件或者通过网络传输出去了。

  • 反序列化,即把二进制内容(也就是 byte[] 数组)恢复为原先的 Java 对象。

    读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中,输入流

    有了反序列化,保存到文件中的 byte[] 数组又可以 “变回” Java 对象,或者从网络上读取 byte[] 并把它 “变回” Java 对象。

二、实现:使用ObjectInputStream 和 ObjectOutputStream

1、序列化

把一个 Java 对象变为 byte[] 数组,需要使用 ObjectOutputStream。它负责把一个 Java 对象写入一个字节流:

ObjectOutputStream (对象输出流)既可以写入基本类型,如 intboolean,也可以写入 String(以 UTF-8 编码),还可以写入实现了 Serializable 接口的 Object

1
2
3
4
5
6
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
// ... 其他省略 ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void serialize() throws IOException {

Student student = new Student();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 1000 );

ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();

System.out.println("序列化成功!已经生成student.txt文件");
System.out.println("==============================================");
}

2、反序列化

ObjectInputStream 负责从一个字节流读取 Java 对象:

1
2
3
4
5
6
7
8
9
10
public static void deserialize() throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
// 调用 readObject() 可以直接返回一个 Object 对象。要把它变成一个特定类型,必须强制转型。
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();

System.out.println("反序列化结果为:");
System.out.println( student );
}

readObject() 可能抛出的异常有:

  • ClassNotFoundException:没有找到对应的 Class;
  • InvalidClassException:Class 不匹配。

对于 ClassNotFoundException,这种情况常见于一台电脑上的 Java 程序把一个 Java 对象,例如,Person 对象序列化以后,通过网络传给另一台电脑上的另一个 Java 程序,但是这台电脑的 Java 程序并没有定义 Person 类,所以无法反序列化。

对于 InvalidClassException,为IOException的子类。这种情况常见于序列化的 Person 对象定义了一个 int 类型的 age 字段,但是反序列化时,Person 类定义的 age 字段被改成了 long 类型。所以导致 class 不兼容。为了避免这种 class 定义变动导致的不兼容,可以自定义一个serialVersionUID

三、Serializable 接口和serialVersionUID静态常量

1、Serializable 接口

查看ObjectOutputStreamwriteObject(Object obj)方法,在源码writeObject0()下:

ObjectOutputStream_writeObject

如果一个对象既不是字符串、数组、枚举,也没有实现Serializable接口,序列化时会抛出异常。

一个 Java 对象要能序列化,必须实现一个特殊的 java.io.Serializable 接口,它的定义如下:

1
2
public interface Serializable {
}

Serializable 接口没有定义任何方法,它是一个空接口。即标记接口(Marker Interface)。

2、serialVersionUID静态常量

Java 的序列化允许 class 定义一个特殊的 serialVersionUID 静态变量,用于标识 Java 类的序列化 “版本”,通常可以由 IDE 自动生成。serialversionUID不一致会抛出序列化运行时异常。

1
private static final long serialVersionUID = -4392658638228508589L;
  • serialVersionUID 是序列化前后的唯一标识符,其实是验证版本一致性的。

    在反序列化时,JVM 会把字节流中的序列号 ID 和被序列化类中的序列号 ID 做比对,如果不一致,会报InvalidClassException异常。

  • 一旦类实现了 Serializable,建议明确的定义一个 serialVersionUID。不然在修改类后,反序列化会发生异常。

    因为如果没有显式定义serialVersionUID,系统会默认定义一个。在类发生改变后,系统定义的serialVersionUID会发生变化,则反序列化时发现版本不一致,就会发生异常。

  • alibaba手册中要求谨慎修改serialversionUID

1θ.【强制】序列化类新增属性时,请不要修改 serialversionUID 字段,避免反序列失败:如果完全不兼容升级,避免反序列化混乱,那么请修改 serialversionUID 值。
说明:注意 serialversionUID 不一致会抛出序列化运行时异常

  • 两种显式生成方式:
    • private static final long serialVersionUID = 1L;
    • 或者借助 IDE 自动生成private static final long serialVersionUID = xxxxL;

四、static修饰的字段 或 transient修饰的字段不会被序列化

1、被 static 修饰的字段,不会被序列化

序列化保存的是对象的状态而非类的状态,序列化并不保存静态变量。

2、被 transient 修饰符修饰的字段不会被序列化

在变量声明前加上transient关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

五、父类的序列化

1、若父类实现 Serializable接口,则其子类自动实现Serializable接口,可以序列化。

序列化该子类对象,子类变量和继承的父类变量都会序列化。

父类实现了 Serializable,子类没有,父类有 Integer a;Integer b ,子类有 Integer c;Integer d。

序列化子类,a,b,c,d都会被序列化。

2、若子类实现 Serializable接口,而父类没有实现 Serializable接口。

序列化子类实例的时候,父类的属性是直接被跳过不保存,父类变量值都是无参构造函数后的值。

子类实现了 Serializable,而父类没有,父类有 Integer a ;Integer b ,子类有 Integer c ; Integer d 。

序列化子类,子类变量c,d会被序列化。反序列化后获取到父类变量值 a=null,b=null。

  • 如果子类实现 Serializable接口,父类没有实现,父类需要有默认的无参的构造函数

父类没有实现 Serializable 接口时,虚拟机是不会序列化父对象的,而一个 Java 对象的构造必须先有父对象,才有子对象,反序列化也不例外。所以反序列化时,为了构造父对象,只能调用父类的无参构造函数作为默认的父对象。因此当我们取父对象的变量值时,它的值是调用父类无参构造函数后的值。

在父类无参构造函数中,若未对变量进行初始化,父类变量值都是默认声明的值,如 int 型的默认是 0,string 型的默认是 null。

如果父类没有无参构造函数,或抛出异常:java.io.InvalidClassException : no valid constructor