中午一个网友来信说自己和面试官干起来了,看完他的描述真是苦笑不得,这年头是怎么了,最近互联网CS消息满天飞,怎么连面试官都SB起来了呢?
大概是这样的:这位网友面试时被问及了Serializable接口的底层实现原理,因为这是一个标识性的空接口,大部分同学在学习时都秉持着会用就行(说实话,Build哥在这之前也没怎么细研究过,都是拿来就用),几乎不太去关注底层的东西,这位网友亦是如此,在这种情况下,自然回答的心虚,这下可被面试官抓住了把柄,一顿带有人身攻击的狂输出,让面试现场变成了撕B现场,具体可看聊天截图😂😂😂
基于这位网友的面试经历,Build哥又赶紧去重新学了一下Serializable关键字,以及它背后的实现,别到时候咱也被暴怼,下面咱们一起来重温一下。
一、序列化与反序列化
首先,我们先来了解一下两个概念 序列化
与 反序列化
。
- 序列化: 将Java对象转换为一个字节序列(包含对象的数据、对象的类型和对象中存储的属性等信息)的过程,以便于在网络上传输或者存储在文件中。
- 反序列化: 是序列化的逆过程,将字节序列转为Java对象的过程。
1.1 序列化与反序列化的应用场景
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件(如系统中excle的上传与下载)之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。
序列化与发序列化的流转过程可参考下图:
有个问题,如果在我的对象中,有些变量并不想被序列化应该怎么办呢?
答:不想被序列化的变量我们可以使用transient
或static
关键字修饰;transient 关键字的作用是阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复;而static关键字修饰的变量并不属于对象本身,所以也同样不会被序列化!具体原因,我们在后面会解释,继续往下看。
二、Java中的序列流
为了探讨Java对象序列化与反序列化的过程,以及Serializable关键字在整个过程中的作用,我们先来提一个 序列流
的概念,刚好我们最近也在写关于Java IO的相关博客。
Java 的序列流(ObjectInputStream 和 ObjectOutputStream)是一种可以将 Java 对象序列化和反序列化的流。这个属于基本的字节输入流与输出流的演变,之前的博文中已经介绍了它们的用法,在这里就不再展开了。
- ObjectOutputStream:将序列化后的字节序列写入到文件、网络等输出流中。
- ObjectInputStream:可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象(包含对象的数据、对象的类型和对象中存储的属性等信息)。
三、序列化实战
OK,有了上面两个理论知识作为铺垫,我们接下来就可以进行序列化的实战了,首先,我们要先创建一个包含简单属性的类,这里我们创建了一个Person类,里面有name和age两个属性字段。然后,我们通过ObjectOutputStream流将对象写出到文件(序列化),然后再通过ObjectInputStream读取文件中的数据,输出为一个person对象(反序列化)。
话不多说,直接上代码:
public class Test {
public static void main(String[] args) throws IOException {
//初始化对象信息
Person person = new Person();
person.setName("JavaBuild");
person.setAge(30);
System.out.println(person.getName()+" "+person.getAge());
//序列化过程
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\person.txt"));) {
objectOutputStream.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
//反序列化过程
try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\person.txt"));) {
Person p = (Person) objectInputStream.readObject();
System.out.println(p.getName() + " " + p.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然后我们执行一下,结果,哦吼!报错了,提示了NotSerializableException,原因是我们在创建Person类时,并没有实现Serializable接口。
很多初学的同学会很奇怪,跟进这个Serializable接口中发现里面空空如也,为啥我们不实现它就无法进行序列化呢?
跟着上面报错中的堆栈信息,我们进入ObjectOutputStream的writeObject0方法中一探究竟!其中有部分源码如下:
// 判断对象是否为字符串类型,如果是,则调用 writeString 方法进行序列化
if (obj instanceof String) {
writeString((String) obj, unshared);
}
// 判断对象是否为数组类型,如果是,则调用 writeArray 方法进行序列化
else if (cl.isArray()) {
writeArray(obj, desc, unshared);
}
// 判断对象是否为枚举类型,如果是,则调用 writeEnum 方法进行序列化
else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
// 判断对象是否为可序列化类型,如果是,则调用 writeOrdinaryObject 方法进行序列化
else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
}
// 如果对象不能被序列化,则抛出 NotSerializableException 异常
else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
从这段源码中我们可以发现,在序列化的时候,writeObject0方法内部会对对象进行类型判断,包括字符串、数组、枚举或Serializable,这些条件都不满足的话,就会抛出NotSerializableException异常,因此,即便Serializable接口什么都没有,但需要是初始化的类实现了它的话,就满足了obj instanceof Serializable,可以进行序列话操作!
我们将上面的测试代码中Person类实现Serializable接口后,再看结果:
序列化与反序列化都成功了,并获得了预期的打印结果。
那么它们的具体实现流程是怎么样的呢?
- 序列化: 以 ObjectOutputStream 为例吧,跟如它的源码时发现,它在序列化的时候会依次调用 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。
- 反序列化: 以 ObjectInputStream 为例,它在反序列化的时候会依次调用 readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。
四、总结
由此可见,Serializable 接口之所以定义为空,是因为它只起到了一个标识的作用,告诉程序实现了它的对象是可以被序列化的,但真正序列化和反序列化的操作并不需要它来完成,就像这里的序列流才是主要实现序列化的驱动器!