Serializable——一个面试中容易被深入问到的问题

你好,我是吴计可师,一个工作十多年的后端开发,曾就职京东、阿里等多家互联网头部企业。

点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读


01
什么是序列化?


序列化(Serialization)是将对象的状态转化为可存储或可传输的格式的过程。在 Java 中,序列化通常指的是将对象转换为字节流,使得对象可以通过网络进行传输,或者将对象的状态存储到磁盘上。在 Java 中,序列化的过程是由 java.io.Serializable 接口和相关的类如 ObjectOutputStream 和 ObjectInputStream 来实现的。


02
为什么需要序列化?


序列化有很多实际应用场景,以下是几个常见的使用场景:

对象持久化:将对象的状态保存到磁盘中,以便后续恢复。这在保存用户数据、配置文件、缓存等场景中非常常见。

网络传输:将对象通过网络发送到远程主机。序列化将对象转化为字节流,可以通过网络协议进行传输(如 HTTP、RPC 等)。

分布式系统中的通信:在分布式系统中,不同服务之间可能需要传递对象数据,序列化将对象转化为字节流,使得数据可以在不同的系统之间传递。


03
序列化的过程


序列化(Serialization):将 Java 对象转换成字节流。Java 提供了 ObjectOutputStream 类来实现这一过程。这个字节流可以被存储在磁盘上,或通过网络传输。

  • 调用 ObjectOutputStream.writeObject() 方法将对象写入字节流。

  • 序列化对象时,如果对象包含引用类型的字段,这些字段也会递归地被序列化。

  • 序列化时还会保存类的元数据(类名、字段类型等)。

反序列化(Deserialization):将字节流重新构造成原来的 Java 对象。Java 提供了 ObjectInputStream 类来实现这一过程。

  • 调用 ObjectInputStream.readObject() 方法从字节流中恢复对象。

  • 在反序列化时,Java 会根据字节流中的数据(如类名、字段值)重建对象及其状态。


04
如何实现序列化?


Java 对象要想进行序列化,必须实现 java.io.Serializable 接口。Serializable 是一个标记接口(marker interface),它没有任何方法,唯一的作用就是告知 JVM 该对象是可序列化的。

import java.io.Serializable;public class Person implements Serializable {    private String name;    private int age;    // 构造方法、getters 和 setters}

上面的 Person 类实现了 Serializable 接口,表示它的对象可以被序列化。


05
序列化示例代码


序列化过程:

import java.io.*;public class SerializationExample {    public static void main(String[] args) {        Person person = new Person("Alice", 30);        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"))) {            out.writeObject(person);  // 将对象写入文件            System.out.println("对象已序列化");        } catch (IOException e) {            e.printStackTrace();        }    }}


反序列化过程:

import java.io.*;public class DeserializationExample {    public static void main(String[] args) {        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"))) {            Person person = (Person) in.readObject();  // 从文件读取对象            System.out.println("对象已反序列化:" + person.getName() + ", " + person.getAge());        } catch (IOException | ClassNotFoundException e) {            e.printStackTrace();        }    }}


序列化时的注意事项:

serialVersionUID:

  • serialVersionUID 是用来验证序列化版本的标识符。每当类的结构发生变化时,serialVersionUID 应该做相应的修改。

  • 如果没有显式定义,Java 会自动生成一个 serialVersionUID,但它是根据类的结构自动生成的,不适合跨版本兼容。因此,建议显式定义一个 serialVersionUID。

private static final long serialVersionUID = 1L;


transient 关键字:

  • 如果你不希望某个字段被序列化,可以使用 transient 关键字修饰该字段。这样,字段值在序列化时会被忽略,反序列化时该字段会被赋予默认值(如 null、0 等)。

private transient String password;  // 该字段不会被序列化

继承关系中的序列化:

  • 当一个类实现了 Serializable 接口时,它的子类会自动继承这一特性,不需要子类显式实现 Serializable。但是,如果父类没有实现 Serializable,子类也必须实现该接口,才能进行序列化。


06
序列化与反序列化的常见问题


类结构变更:如果类的结构发生了变化(比如添加了新的字段、修改了字段的类型等),可能会导致反序列化失败。为了避免这种问题,可以显式定义 serialVersionUID,并确保类的兼容性。

性能问题:序列化和反序列化是比较昂贵的操作,特别是当对象的字段较多或者对象深度嵌套时。应当合理使用序列化,例如避免在高频请求中反复进行序列化操作。


07
直击面试


7.1 如果一个类没有实现 Serializable 接口,会发生什么情况?

如果一个类没有实现 Serializable 接口,那么该类的对象不能被序列化。当尝试对该对象进行序列化操作时,Java 会抛出 java.io.NotSerializableException 异常。


7.2 什么是 serialVersionUID,它有什么作用?

serialVersionUID 是一个版本控制号,用于验证序列化和反序列化过程中类的版本一致性。当类在不同版本之间发生变化时,反序列化时会检查 serialVersionUID,如果不同则抛出 InvalidClassException 异常。它是保证反序列化过程不会出错的重要字段。

作用:

  • 确保序列化版本一致性。

  • 防止因为类的结构变化导致反序列化失败。

示例:

private static final long serialVersionUID = 1L;

7.3 Serializable 和 Externalizable 的区别是什么?

  • Serializable:它是一个标记接口,Java 默认提供实现来处理序列化和反序列化。你可以通过实现这个接口,使得对象可以被序列化。序列化的过程由 Java 自动进行,通常会将类的所有字段进行序列化(除非被标记为 transient)。

  • Externalizable:它是 Serializable 的子接口,提供了更细粒度的控制。它要求类必须实现 writeExternal() 和 readExternal() 方法,开发者可以自己定义如何序列化和反序列化对象。

区别:

  • Serializable 序列化过程是自动的,Externalizable 需要手动定义序列化和反序列化的行为。

  • Externalizable 在序列化时允许开发者选择性地存储字段,给了更大的灵活性。


7.4 如何防止一个类的字段被序列化?

可以使用 transient 关键字来标记不希望被序列化的字段。被 transient 修饰的字段在序列化过程中将被忽略,不会被写入字节流。

private transient String password; // 这个字段不会被序列化

7.5 如果一个类的父类实现了 Serializable 接口,子类需要实现 Serializable 吗?

回答:如果一个类的父类实现了 Serializable 接口,子类会自动具备序列化能力,除非子类显式声明不实现 Serializable。也就是说,子类继承了父类的序列化特性,但可以通过重写 writeObject 和 readObject 方法来定制序列化行为。


7.6 如果你修改了一个已经序列化的类(例如增加了一个新字段),反序列化时会发生什么?

修改类后,如果没有相应地更新 serialVersionUID,反序列化时可能会抛出 InvalidClassException,因为类的版本不匹配。为了防止这种情况,通常建议在类中声明 serialVersionUID,即使你修改了类的结构,也可以保持兼容性。

解决方法:

  • 修改类时更新 serialVersionUID。

  • 在修改类时保持 serialVersionUID 一致,或使用兼容的方式修改类字段(如保持字段名称不变)。


7.7 如何反序列化一个对象时恢复 transient 字段的值?transient 修饰的字段不会被序列化,因此在反序列化时这些字段的值会被设置为默认值(例如 null 或 0)。如果需要在反序列化时恢复这些字段的值,可以在反序列化后通过自定义方法手动设置。

解决方法: 在类中定义 readObject 方法,或者在反序列化后通过其他方式恢复 transient 字段的值。


7.8 如何在 Java 中实现深度复制(Deep Clone)?

回答:如果一个对象中的字段是引用类型,那么仅仅通过 clone() 或 Serializable 的浅拷贝方式是不够的,因为引用类型的字段会共享原对象中的实例。深度复制(Deep Clone)要求对对象的所有字段(包括引用类型字段)进行递归复制。

通过 Serializable 实现深度复制的一种方式是:将对象序列化为字节流,再通过反序列化恢复对象,达到深度复制的目的。


今天的内容就分享到这儿,喜欢的朋友可以关注,点赞。有什么不足的地方欢迎留言指出,您的关注是我前进的动力!

END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海

喜欢此内容的人还喜欢

谈谈id那些事(五)——美团的 Leaf 的ID生成


一个阿里二面面试官必问的问题


谈谈id那些事(三)——阿里巴巴的 TDDL的ID生成


分享面试:mysql数据库索引失效的情况


面试常被忽略的问题——内存区域划分