京东二面:说说Java序列化和反序列化

科技   2024-11-04 11:08   广东  

这是有小伙伴最近遇到的面试题。一起来看看。

一 什么是序列化和反序列化

这是两个大家经常会接触的概念~

序列化是指将对象的状态信息转换为可以存储或传输的形式的过程。在Java中,这通常意味着将对象转换为字节序列。

反序列化则是序列化的逆过程,它将字节序列恢复为对象。

一个简单的例子,对象转为 JSON 字符串就是序列化,因为变为 JSON 字符串之后就可以传输了;JSON 字符串转为对象则是反序列化。

二 序列化与反序列化的使用场景

我们日常开发中,序列化和反序列化其实还是挺常见的,只不过有时候我们忘记了自己所做的事情其实就是序列化和反序列化。

举几个常见的场景:

  1. 网络传输:在分布式系统中,对象需要在网络上传输时,需要将对象序列化后发送,接收方再进行反序列化。
  2. 数据存储:将对象状态保存到文件或数据库中,以便后续恢复。
  3. 远程方法调用(RMI):在 Java 的 RMI 中,对象需要在客户端和服务器之间传递。
  4. 对象克隆:通过序列化和反序列化实现对象的深拷贝。

类似的场景其实很多。

不过就日常开发而言,可能大家在从 Redis 中存取对象、Dubbo 远程调用,这些场景可能会明确感知到序列化这件事,其他场景可能感受就不是特别明显。

三 Java 中实现序列化与反序列化

3.1 实现 Serializable 接口

假设有一个Student类,需要将其对象序列化到文件中。

import java.io.Serializable;  
import java.io.FileOutputStream;  
import java.io.ObjectOutputStream;  
  
public class Student implements Serializable {  
    private static final long serialVersionUID = 1L// 用于版本控制  
    private String name;  
    private int age;  
  
    // 构造函数、getter和setter省略  
  
    public static void main(String[] args) {  
        Student student = new Student("张三"20);  
        try (FileOutputStream fos = new FileOutputStream("student.ser");  
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {  
            oos.writeObject(student);  
            System.out.println("对象序列化成功!");  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

在这个案例中,Student 类通过实现 Serializable 接口来标记其可序列化。然后,使用 ObjectOutputStream 将 Student 对象写入到文件中。

3.2 实现 Externalizable 接口

与 Serializable 接口类似,但 Externalizable 接口提供了更灵活的序列化控制。

import java.io.Externalizable;  
import java.io.FileOutputStream;  
import java.io.ObjectOutput;  
import java.io.ObjectOutputStream;  
  
public class Employee implements Externalizable {  
    private String name;  
    private int age;  
  
    // 构造函数、getter和setter省略  
  
    @Override  
    public void writeExternal(ObjectOutput out) throws IOException {  
        out.writeObject(name);  
        out.writeInt(age);  
    }  
  
    @Override  
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
        name = (String) in.readObject();  
        age = in.readInt();  
    }  
  
    public static void main(String[] args) {  
        // 序列化与反序列化逻辑与Serializable类似,但会调用writeExternal和readExternal方法  
    }  
}

在这个案例中,Employee 类通过实现 Externalizable 接口来提供自定义的序列化和反序列化逻辑。

相比于 Serializable,Externalizable 的灵活性主要体现在四点:

  1. Serializable 接口的序列化过程是由 JVM 自动完成的,不允许开发者对序列化过程进行自定义。Externalizable 接口要求开发者实现 writeExternal() 和 readExternal() 两个方法,从而完全控制序列化过程。这意味着开发者可以决定哪些字段需要序列化,哪些不需要,以及如何序列化这些字段。
  2. Serializable 的自动序列化过程虽然方便,但可能不是最高效的。Externalizable 允许开发者自定义序列化过程,因此可以针对特定需求进行优化。例如,可以只序列化必要的字段,或者采用更高效的数据结构来存储序列化数据,从而提高性能。
  3. Serializable 在默认情况下会序列化对象的所有非 transient 字段。如果对象的类结构发生变化如添加或删除字段,则可能会影响序列化和反序列化的兼容性。而对于 Externalizable 接口,开发者可以精确控制哪些字段被序列化,从而更容易地管理版本兼容性问题,甚至还可以在 writeExternal() 和 readExternal() 方法中添加逻辑来处理不同版本的序列化数据。
  4. 由于 Serializable 的序列化过程是自动的,因此可能会无意中序列化敏感信息(如密码、密钥等),此外,恶意用户还可能通过修改序列化数据来攻击系统。而 Externalizable 接口允许开发者明确控制哪些信息被序列化,从而可以减少敏感信息被泄露的风险,开发者甚至还可以在序列化过程中添加额外的安全措施(如加密、签名等)来提高系统的安全性。

3.3 使用 JSON 序列化库(如 Jackson、Gson)

import com.fasterxml.jackson.databind.ObjectMapper;  
  
public class User {  
    private String username;  
    private int age;  
  
    // 构造函数、getter和setter省略  
  
    public static void main(String[] args) {  
        try {  
            User user = new User("李四"30);  
            ObjectMapper mapper = new ObjectMapper();  
            String json = mapper.writeValueAsString(user);  
            System.out.println(json); // 输出JSON字符串  
  
            // 反序列化  
            User deserializedUser = mapper.readValue(json, User.class);  
            System.out.println(deserializedUser.getUsername()); // 输出:李四  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

在这个案例中,使用 Jackson 库将 User 对象序列化为 JSON 字符串,并反序列化为 User 对象。

3.4 使用 XML 序列化库(如JAXB、XStream)

import javax.xml.bind.JAXBContext;  
import javax.xml.bind.Marshaller;  
import javax.xml.bind.annotation.XmlRootElement;  
  
@XmlRootElement  
public class Address {  
    private String street;  
    private String city;  
  
    // 构造函数、getter和setter省略  
  
    public static void main(String[] args) {  
        try {  
            Address address = new Address("123 Main St""Anytown");  
            JAXBContext jaxbContext = JAXBContext.newInstance(Address.class);  
            Marshaller marshaller = jaxbContext.createMarshaller();  
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);  
            marshaller.marshal(address, System.out); // 输出到控制台  
  
            // 反序列化逻辑通常涉及解析XML字符串或文件  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

在这个案例中,Address 类通过 @XmlRootElement 注解标记为可 XML 序列化。使用 JAXB 库将其序列化为 XML 格式并输出到控制台。

3.5 使用二进制序列化库(如 protobuf、Avro)

由于 Protobuf 的使用涉及 .proto 文件的定义和编译,这里仅简要说明。首先定义一个 .proto文件,然后使用 Protobuf 编译器生成 Java 代码,最后使用生成的代码进行序列化和反序列化。

这个松哥之前在 gRPC 教程中详细介绍过,这里不多说了,公号后台回复 gRPC。

四 static 和 transient

static 和 transient 是序列化时两个比较特殊的字段。

  • staticstatic 字段是类级别的,不属于对象实例,因此在序列化时不会被包含。
  • transienttransient 关键字修饰的字段在序列化过程中会被忽略,不会被序列化。

五 小结

以上就是松哥和大家介绍的 Java 中的序列化问题。

另外有几个点大家在序列化时候需要注意:

  1. 版本控制:通过 serialVersionUID 来确保序列化的兼容性。
  2. 性能考虑:序列化和反序列化是资源密集型操作,应考虑性能影响。
  3. 安全性:确保序列化数据的安全性,避免序列化过程中的数据泄露。
  4. 数据一致性:在反序列化时,确保数据的一致性和完整性。

Redis 实战

Redis 博大精深,然而很多时候我们说到 Redis,却只知道缓存或者分布式锁,面试的时候也只能从这两个角度去准备。

但是在实际面试中,Redis 这块能够发挥的地方可太多了:

  • Redis 中 String 类型使用了什么样的数据结构?
  • 为什么每种数据类型几乎都设计了两种以上的数据结构?
  • 为什么要延迟双删?原因是什么
  • RedLock 解决了什么问题,为什么现在又被废弃了?现在用什么?
  • watchdog 什么情况下会失效?
  • Redis 挂了怎么办?
  • 如何实现百万级排行榜?
  • 。。。

还有很多,我就不一一列举了。

所以松哥今年出了一个 Redis 课程,取名就叫《Redis-不止缓存》,希望能和大家分享一下 Redis 在缓存这个领域之外的一些用法。

这套课程基于目前最新版的 Redis7 录制,从基本用法到原理分析讲了很多,大伙来看下目录:

课程有配套的源码和笔记,同时为了确保大家确确实实能够学会并且掌握 Redis,也提供了答疑群,实时解答课程相关的问题。

课程原价 799,限时 499。

关于松哥

9 年程序员生涯,Java 畅销书作者,华为云最具价值专家,华为开发者社区之星,GitHub 知名项目作者。

目前产品有 Java 项目课程、Java 简历指导、1V1 模拟面试等,如有需求欢迎来勾搭。

感兴趣小伙伴加微信备注 Redis


江南一点雨
一站式Java全栈技术学习平台!
 最新文章