• 面试题:Java序列化与反序列化


    目录

    序列化和反序列化的概念

    当我们在Java中创建对象的时候,对象会一直存在,直到程序终止时。但有时候可能存在一种"持久化"场景:我们需要让对象能够在程序不运行的情况下,仍能存在并保存其信息。当程序再次运行时 还可以通过该对象的保存下来的信息 来重建该对象。序列化和反序列化 就应运而生了,序列化机制可以使对象可以脱离程序的运行而独立存在。

    • 序列化: 将对象转换成二进制字节流的过程
    • 反序列化:从二进制字节流中恢复对象的过程

    应用场景?

    1. 对象在进行网络传输的时候,需要先被序列化,接收到序列化的对象之后需要再进行反序列化;比如远程方法调用 RPC
    2. 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
    3. 将对象存储到内存中,需要进行序列化,将对象从内存中读取出来需要进行反序列化。
    4. 将对象存储到数据库(如 Redis)时,需要用到序列化,将对象从缓存数据库中读取出来需要反序列化。

     

     

    序列化实现的方式

    如果使用Jdk自带的序列化方式实现对象序列化的话,那么这个类应该实现Serializable接口或者Externalizable接口

    继承Serializable接口,普通序列化

    首先我们定义一个对象类User

    1. public class User implements Serializable {
    2. //序列化ID
    3. private static final long serialVersionUID = 1L;
    4. private int age;
    5. private String name;
    6. public User(int age, String name) {
    7. this.age = age;
    8. this.name = name;
    9. }
    10. public static long getSerialVersionUID() {
    11. return serialVersionUID;
    12. }
    13. public int getAge() {
    14. return age;
    15. }
    16. public void setAge(int age) {
    17. this.age = age;
    18. }
    19. public String getName() {
    20. return name;
    21. }
    22. public void setName(String name) {
    23. this.name = name;
    24. }
    25. }

     然后我们编写一下测试类:

     

    1. public class serTest {
    2. public static void main(String[] args) throws Exception, IOException {
    3. SerializeUser();
    4. DeSerializeUser();
    5. }
    6. /**
    7. * 序列化方法
    8. * @throws IOException
    9. */
    10. private static void SerializeUser() throws IOException {
    11. User user = new User(11, "小张");
    12. //序列化对象到指定的文件中
    13. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\jun\\Desktop\\example"));
    14. oos.writeObject(user);
    15. oos.close();
    16. System.out.println("序列化对象成功");
    17. }
    18. /**
    19. * 反序列化方法
    20. * @throws IOException
    21. * @throws ClassNotFoundException
    22. */
    23. private static void DeSerializeUser() throws IOException, ClassNotFoundException {
    24. //读取指定的文件
    25. File file = new File("C:\\Users\\jun\\Desktop\\example");
    26. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    27. User newUser = (User)ois.readObject();
    28. System.out.println("反序列化对象成功:"+ newUser.getName()+ ","+newUser.getAge());
    29. }
    30. }

    结果:

    序列化对象成功
    反序列化对象成功:小张,11

    一个对象想要被序列化,那么它的类就要继承Serializable接口或者它的子接口

    继承Serializable接口类的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。如果不想序列化的字段可以使用transient关键字修饰

    1. private int age;
    2. private String name;
    3. private transient password;//属性:密码,不想被序列化

     

    我们需要注意的是:使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,这必然会导致了在反序列化时无法获取该属性的值。
    其实我们完全可以在通过在需要序列化的对象的Java类里加入writeObject()方法readObject()方法来控制如何序列化各属性,某些属性是否被序列化

    如果User有一个属性是引用类型的呢?比如User其中有一个属性是类Person:

    private Person person;
    

     

    那如果要想User可以序列化,那Person类也必须得继承Serializable接口,不然程序会报错

    另外大家应该注意到serialVersionUID了吧,在日常开发的过程中,经常遇到,暂且放放,我们后文再详细讲解

    继承Externalizable接口,强制自定义序列化

    对于Externalizable接口,我们需要知道以下几点:

    1. Externalizable继承自Serializable接口
    2. 需要我们重写writeExternal()与readExternal()方法,这是强制性的
    3. 实现Externalizable接口的类必须要提供一个public的无参的构造器,因为反序列化的时候需要反射创建对象
    4. Externalizable接口实现序列化,性能稍微比继承自Serializable接口好一点

    首先我们定义一个对象类ExUser

    1. public class ExUser implements Externalizable {
    2. private int age;
    3. private String name;
    4. //注意,必须加上pulic 无参构造器
    5. public ExUser() {
    6. }
    7. public int getAge() {
    8. return age;
    9. }
    10. public void setAge(int age) {
    11. this.age = age;
    12. }
    13. public String getName() {
    14. return name;
    15. }
    16. public void setName(String name) {
    17. this.name = name;
    18. }
    19. @Override
    20. public void writeExternal(ObjectOutput out) throws IOException {
    21. out.writeObject(name);
    22. out.writeInt(age);
    23. }
    24. @Override
    25. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    26. this.name = (String)in.readObject();
    27. this.age = in.readInt();
    28. }
    29. }

     我们接着编写测试类:

    1. public class serTest2 {
    2. public static void main(String[] args) throws Exception, IOException {
    3. SerializeUser();
    4. DeSerializeUser();
    5. }
    6. /**
    7. * 序列化方法
    8. * @throws IOException
    9. */
    10. private static void SerializeUser() throws IOException {
    11. ExUser user = new ExUser();
    12. user.setAge(10);
    13. user.setName("小王");
    14. //序列化对象到指定的文件中
    15. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\jun\\Desktop\\example"));
    16. oos.writeObject(user);
    17. oos.close();
    18. System.out.println("序列化对象成功");
    19. }
    20. /**
    21. * 反序列化方法
    22. * @throws IOException
    23. * @throws ClassNotFoundException
    24. */
    25. private static void DeSerializeUser() throws IOException, ClassNotFoundException {
    26. File file = new File("C:\\Users\\jun\\Desktop\\example");
    27. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    28. ExUser newUser = (ExUser)ois.readObject();
    29. System.out.println("反序列化对象成功:"+ newUser.getName()+ ","+newUser.getAge());
    30. }
    31. }

     

    结果:

    序列化对象成功
    反序列化对象成功:小王,10

    因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,transient关键字在这里是无效的。

    Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

    serialVersionUID的作用

    如果反序列化使用的serialVersionUID与序列化时使用的serialVersionUID不一致,会报InvalidCalssException异常。这样就保证了项目迭代升级前后的兼容性
    serialVersionUID是序列化前后的唯一标识符,只要版本号serialVersionUID相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
    默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

    serialVersionUID有两种显式的生成方式:

    1. 默认的1L,比如:private static final long serialVersionUID = 1L;
    2. 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:

    private static final long serialVersionUID = xxxxL;

    静态变量不会被序列化

    凡是被static修饰的字段是不会被序列化的,我们来看一个例子:

    1. //实体类
    2. public class Student implements Serializable {
    3. private String name;
    4. public static Integer age;//静态变量
    5. public String getName() {
    6. return name;
    7. }
    8. public void setName(String name) {
    9. this.name = name;
    10. }
    11. public static Integer getAge() {
    12. return age;
    13. }
    14. public static void setAge(Integer age) {
    15. Student.age = age;
    16. }
    17. }
    18. //测试类
    19. public class shallowCopyTest {
    20. public static void main(String[] args) throws Exception {
    21. Student student1 = new Student();
    22. student1.age = 11;
    23. //序列化,将数据写入指定的文件中
    24. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\student1"));
    25. oos.writeObject(student1);
    26. oos.close();
    27. Student student2 = new Student();
    28. student2.age = 21;
    29. //序列化,将数据写入指定的文件中
    30. ObjectOutputStream oos2 = new ObjectOutputStream(new FileOutputStream("D:\\student2"));
    31. oos2.writeObject(student1);
    32. oos2.close();
    33. //读取指定的文件
    34. File file = new File("D:\\student1");
    35. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    36. Student student1_new = (Student)ois.readObject();
    37. System.out.println("反序列化对象,student1.age="+ student1_new.getAge());
    38. //读取指定的文件
    39. File file2 = new File("D:\\student1");
    40. ObjectInputStream ois2 = new ObjectInputStream(new FileInputStream(file2));
    41. Student student2_new = (Student)ois2.readObject();
    42. System.out.println("反序列化对象,student2.age="+ student2_new.getAge());
    43. }
    44. }

     

    结果:

    反序列化对象,student1.age=21
    反序列化对象,student2.age=21

    为啥结果都是21
    我们知道对象的序列化是操作的堆内存中的数据,而静态的变量又称作类变量,其数据存放在方法区里,类一加载,就初始化了。
    又因为静态变量age没有被序列化,根本就没写入文件流中,所以我们打印的值其实一直都是当前Student类的静态变量age的值,而静态变量又是所有的对象共享的一个变量,所以就都是21

    使用序列化实现深拷贝

    我们再来看一个例子:

    1. //实体类 继承Cloneable
    2. public class Person implements Serializable{
    3. public String name;//姓名
    4. public int height;//身高
    5. public StringBuilder something;
    6. ...//省略 getter setter
    7. public Object deepClone() throws Exception{
    8. // 序列化
    9. ByteArrayOutputStream bos = new ByteArrayOutputStream();
    10. ObjectOutputStream oos = new ObjectOutputStream(bos);
    11. oos.writeObject(this);
    12. // 反序列化
    13. ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    14. ObjectInputStream ois = new ObjectInputStream(bis);
    15. return ois.readObject();
    16. }
    17. }
    18. //测试类,这边类名笔者就不换了,在之前的基础上改改
    19. public class shallowCopyTest {
    20. public static void main(String[] args) throws Exception {
    21. Person p1 = new Person("小张", 180, new StringBuilder("今天天气很好"));
    22. Person p2 = (Person)p1.deepClone();
    23. System.out.println("对象是否相等:"+ (p1 == p2));
    24. System.out.println("p1 属性值=" + p1.getName()+ ","+ p1.getHeight() + ","+ p1.getSomething());
    25. System.out.println("p2 属性值=" + p2.getName()+ ","+ p2.getHeight() + ","+ p2.getSomething());
    26. // change
    27. p1.setName("小王");
    28. p1.setHeight(200);
    29. p1.getSomething().append(",适合出去玩");
    30. System.out.println("...after p1 change....");
    31. System.out.println("p1 属性值=" + p1.getName()+ ","+ p1.getHeight() + ","+ p1.getSomething());
    32. System.out.println("p2 属性值=" + p2.getName()+ ","+ p2.getHeight() + ","+ p2.getSomething());
    33. }
    34. }

     

    结果:

    对象是否相等:false
    p1 属性值=小张,180,今天天气很好
    p2 属性值=小张,180,今天天气很好
    ...after p1 change....
    p1 属性值=小王,200,今天天气很好,适合出去玩
    p2 属性值=小张,180,今天天气很好

    详情见:面试题:深拷贝、浅拷贝、引用拷贝的区别

    常见序列化协议对比

    除了JDK 自带的序列化方式,还有一些其他常见的序列化协议:

    1. 基于二进制: hessian、kyro、protostuff
    2. 文本类序列化方式: JSON 和 XML

    采用哪种序列化方式,我们一般需要考虑序列化之后的数据大小,序列化的耗时,是否支持跨平台、语言,或者公司团队的技术积累。这边就不展开讲了,大家感兴趣自行去了解

    小结

    1. JDK自带序列化方法一般有2种:继承Serializable接口继承Externalizable接口
    2. static修饰的类变量、transient修饰的实例变量都不会被序列化。
    3. 序列化对象的引用类型成员变量,也必须是可序列化的
    4. serialVersionUID 版本号是序列化和反序列化前后唯一标识,建议显式定义
    5. 序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,反序列化出来的对象会有一定风险。可以重写readObject()方法,加以限制
    6. 除了JDK自带序列化方法,还有hessian、kyro、protostuff、 JSON 和 XML等
  • 相关阅读:
    Sparse R-CNN 论文精读
    golang 编程规范查漏补缺
    C语言库总结
    pycharm 远程运行报错 Failed to prepare environment
    postman教程-14-生成随机数
    Spark与SQL之间NB的转换_withClumn,split及SubString
    国家网络安全宣传周知识竞赛活动小程序界面分享
    后端程序员利用 AI 给网站制作专业 favicon
    15种下载文件的方法&文件下载方法汇总&超大文件下载
    基础会计习题集(第五版-樊
  • 原文地址:https://blog.csdn.net/m0_73088370/article/details/126541585