一、序列化

1.1 什么是序列化,为什么要序列化?

我们在运行Java程序的时候,各个对象是有状态的。比如,我们创建了如下的一个Fruit对象,它的名字和重量是它当前的状态信息:

public static void main(String[] args) {
Fruit apple = new Fruit();
apple.setName("apple");
apple.setWeight(23);
}

在有的场景下,我们需要将该对象当前的状态信息持久化地保留下来,或者借助网络传输到别的地方,而这些场景是无法以对象的形态保留信息的,毕竟只有Java运行的时候才有对象这个概念。

因此,我们只能以文本或者字符的形式来表示当前Java中该对象的状态信息。那么,将Java中一个动态的对象表示成文本或者字符的过程,我们就称之为序列化。

1.2 如何在Java中使用序列化

常见的Java序列化方法有Java原生序列化、Hessian序列化、kryo序列化、Json序列化等,这里以介绍Java原生序列化为例。

默认声明的类是不能支持序列化的,只有当这个类实现了Serializable接口,才可以被序列化。而这个Serializable接口中不包含任何方法和属性,它仅仅是起到一个序列化标识的作用。下面是一个例子:

@Data
public class Fruit implements Serializable {
private String name;
private Integer weight;
// setters and getters...
}
public static void main(String[] args) {
Fruit apple = new Fruit();
apple.setName("apple");
apple.setWeight(23);
saveToFile(apple);
System.out.println("序列化结束");
}
private static void saveToFile(Fruit fruit){
try {
FileOutputStream fs = new FileOutputStream("fruit.txt");
ObjectOutputStream os = new ObjectOutputStream(fs);
os.writeObject(fruit);
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
如果以上Fruit类没有实现Serializable接口,程序运行就会报错:
java.io.NotSerializableException: cn.zx.demo.Fruit
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at cn.zx.demo.Main.saveToFile(Main.java:27)
at cn.zx.demo.Main.main(Main.java:19)

只有实现了该接口,才能正常运行并生成一个txt文件,这个就是我们apple对象序列化后的结果。

1.3 反序列化的使用

反序列化,顾名思义,就是要把以文本形式持久化了的对象信息再还原成Java运行时动态的对象的状态。

我们使用上面序列化中的例子,将持久化了的文本txt读入:

public static void main(String[] args) {
Fruit apple = readFromFile("fruit.txt");
// print apple
System.out.println(apple.getName());
// print 23
System.out.println(apple.getWeight());
}
private static Fruit readFromFile(String fileName){
Fruit fruit = null;
try{
FileInputStream fs = new FileInputStream(fileName);
ObjectInputStream os = new ObjectInputStream(fs);
fruit = (Fruit) os.readObject();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return fruit;
}

反序列化成功,我们成功地从序列化了的文本信息中恢复了动态的Java对象及其序列化时的状态信息。

1.4 版本标识serialVersionUID

当一个对象被序列化后,接着需要被反序列化时,如何判断能否顺利地反序列化形成对象呢?因为原来的类可能被修改了,可能和序列化时的类是不一样的。

因此,需要使用serialVersionUID用来标识反序列化时类的版本和序列化时类的版本是否一致。当值是一致的,则表示可以序列化,反之则不能序列化,会报如下错误:

java.io.InvalidClassException: cn.zx.demo.code.Fruit; local class incompatible: stream classdesc serialVersionUID = -4079670274710263332, local class serialVersionUID = -1938591684852446153
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1843)

在默认情况下,serialVersionUID的值不需要我们手动指定,系统会自动指定,只要我们确定序列化和反序列化前后的类没有发生变化,那么就不会出现如上的问题。

倘若我们将序列化之后的类增加了一个属性,或者加了一个别的逻辑,改变了类的程序结构,那么系统就会自动更改serialVersionUID的值,从而使得反序列化失败,报出如上的错误。比如我们加一个属性:

@Data
public class Fruit implements Serializable {
private String name;
private Integer weight;
private Double price;
// setters and getters...
}

但这是不合理的,因为我们只是加了一个属性,并不妨碍反序列化,新加的属性让其值为空不就行了,所以,让系统自动指定serialVersionUID的值在这种场景下就不是那么合理。我们需要自己指定它的值。

@Data
public class Fruit implements Serializable {
private static final long serialVersionUID = -1L;
private String name;
private Integer weight;
// setters and getters...
}

然后,随便你怎么改动类的结构,只要保证serialVersionUID不变,程序就不会比较这个值是否一致,从而尝试反序列化。

比如新加的一个属性反序列化后,内存中的它只不过是没有值而已,其它可以反序列化的属性不受影响。如果你改动的是原先序列化时就存在的属性,那么也没有关系,反序列化找不到对应的属性就会跳过,把能反序列化的都赋值,不能的全部为空。

1.5 序列化的使用注意事项

父类如果实现了序列化的接口,那么其子类自动拥有了序列化的能力;

一个已经实现了序列化的类引用了其它的类,那么其它的类也必须实现序列化的接口,否则序列化过程会报错java.io.NotSerializableException;

静态变量和transient修饰的变量不会被序列化;

1.6 序列化的常见使用场景

远程方法调用(RPC);

对象存储到文件或者数据库中;

实现对象的深拷贝;

二、transient

2.1 transient的作用是什么

在将对象序列化的时候,并不是所有的字段都需要保存起来。比如一些敏感信息,我们只要求在内存中使用就好,序列化的时候不要连同它们一起持久化。这时候,Java中的transient关键字就派上用场了,被它修饰了的字段就不会被序列化:

private transient Integer weight;

我们仍然使用上面的序列化和反序列化的例子,只不过给Fruit类中的weight属性加上transient关键字,希望它不要被序列化。

然后运行一次序列化和反序列的程序,得到的打印结果中weight为null。由此证明了transient生效了。

2.2 transient的使用注意事项

被transient修饰的变量无法序列化,随后反序列时便无法恢复该变量的值;

transient只能用来修饰变量,无法修饰类和方法;

类中的静态变量也是无法序列化的,因为类变量属于类,不属于对象。

关于如上第三点,给出实验进行证明:

public class Fruit implements Serializable {
private static final long serialVersionUID = 6950076107144781481L;
private String name;
private transient Integer weight;
private static String color;
public Fruit(){}
// getters and setters...
}
public static void main(String[] args) {
Fruit apple = new Fruit();
apple.setName("apple");
apple.setWeight(23);
Fruit.setColor("red");
saveToFile(apple);
System.out.println("序列化结束");
}
private static void saveToFile(Fruit fruit){
try {
FileOutputStream fs = new FileOutputStream("fruit.txt");
ObjectOutputStream os = new ObjectOutputStream(fs);
os.writeObject(fruit);
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

先执行如上程序,将设置好静态变量color的apple对象进行序列化,然后再执行如下程序得出反序列化后的结果:

public static void main(String[] args) {
Fruit apple = readFromFile("fruit.txt");
// print apple
System.out.println(apple.getName());
// print null
System.out.println(apple.getWeight());
// print null
System.out.println(Fruit.getColor());
}
private static Fruit readFromFile(String fileName){
Fruit fruit = null;
try{
FileInputStream fs = new FileInputStream(fileName);
ObjectInputStream os = new ObjectInputStream(fs);
fruit = (Fruit) os.readObject();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return fruit;
}

三、待解决的疑问

你最常用的序列化和反序列化的业务场景是什么?

答复:在1.6中已经列出场景。

日常开发中,最常使用的场景就是数据库数据的读写、关联方系统Json数据的传输,但是并没有留意到DTO和PO有实现Serializable接口,更没有serialVersionUID的声明,这是为什么呢?它们不算序列化的操作吗?

答复:Json是Java中的另一种序列化方法,不是原生的序列化方法,不需要实现Serializable接口。