Java基础-序列化与反序列化

序列化和反序列化在面试中也经常考查,下面就总结一下 Java 中的序列化和反序列化。

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

序列化是将 Java 对象转换成与平台无关的二进制流,而反序列化则是将二进制流恢复成原来的 Java 对象,二进制流便于保存到磁盘上或者在网络上传输。

如何实现序列化和反序列化?

如果想要序列化某个类的对象,就需要让该类实现 Serializable 接口或者 Externalizable 接口。

如果实现 Serializable 接口,由于该接口只是个 “标记接口”,接口中不含任何方法,序列化是使用 ObjectOutputStream(处理流)中的 writeObject(obj) 方法将 Java 对象输出到输出流中,反序列化是使用 ObjectInputStream 中的 readObject(in) 方法将输入流中的 Java 对象还原出来。

下面程序演示实现 Serializable 接口,将对象序列化到文件中,再从文件中反序列化对象。

Java Bean 类

import java.io.Serializable;
public class Person implements Serializable {
    private String name;
    private int age;
    // 此处没有提供无参的构造函数
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // 省略 getter 和 setter 方法
}

序列化和反序列化

import java.io.*;

public class Test {
    public static void main(String[] args) {
        // 序列化
        try (ObjectOutputStream outputStream = new ObjectOutputStream(new File("").getAbsolutePath()+"/object.txt"))) {
            Person person = new Person("小明", 21);
            outputStream.writeObject(person);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化
        try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File("").getAbsolutePath()+"/object.txt"))) {
            Person person = (Person) objectInputStream.readObject();
            System.out.println("name:" + person.getName() + ",age:" + person.getAge());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();

        }
    }
}

输出结果为:name:小明,age:21

需要注意的是反序列化读取的仅仅是 Java 对象中的数据,而不是包含 Java 类的信息,所以在反序列化时还需要对象所属类的字节码(class)文件,否则会出现 ClassNotFoundException 异常。

如果实现 Externalizable接口,该接口继承自 Serializable 接口,在 Java Bean 类中实现接口中的 writeExternal(out) 和 readExternal(in) 方法,需要注意的是必须提供默认的无参构造函数,否则反序列化失败。

上面的 Java Bean 代码可修改为:

import java.io.*;

public class Person implements Externalizable {
    private String name;
    private int age;
    
    // 需要提供默认的无参构造函数
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = 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 {
        this.name = in.readObject().toString();
        this.age = in.readInt();
    }
}

这两种序列化反序列化方式,前一种是使用默认的 Java 实现,而后一种是自定义实现,可以在序列化中选择如何序列化,比如对某个属性加密处理。

注意序列化属性的顺序要和属性反序列化中的顺序一样,否则在反序列化时不能恢复出原来的对象。

其实让类实现 Serializable 接口也是可以实现自定义序列化,只但需要在类中提供下面这三个方法。

private void writeObject(java.io.ObjectOutStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;

writeObject() 方法的作用和上面 writeExternal() 方法类似,readObject() 方法的作用和上面 readExternal() 方法类似,而 readObjectNoData() 方法是在序列化流不完整、序列化和反序列化版本不一致导致不能正确反序列时调用的容错方法。

使用默认的序列化方式,会将对象中的每个实例属性依次进行序列化,如果某个属性是一个类类型,那么需要保证这个类也要是可序列化的类,否则将不能序列化该对象。在 Java 的序列化机制中,被序列化后的对象都有一个编号,多次序列化同一个对象,除了第一次真正序列化对象外,其他都是保存一个序列化编号。这样的机制带来的问题就是如果在序列化一个对象后,修改了对象中的属性,也不会生效。并不是对象中每个属性都需要序列化的,如被 static 修饰的属性是属于类的,而不是只属于某个对象。使用默认序列化方式,是不会将这些属性序列化的,在自定义的序列化方式中,我们也可以将这些属性忽略掉。除此之外,可以使用 transient 关键字来修饰某个属性,这样默认的序列化方式就不会序列化该属性了,自定义还是可以的。如果在反序列化时强行得到这些没有被序列化的值,得到的会是默认值(0 或 null)。

序列化和反序列化的版本问题

在 Java 的序列化机制中,允许给类提供一个 private static final 修饰的 SerialVersionUID 类常量,来作为类版本的代号。这样即使类被修改了(如修改了方法),也会把修改前的类和修改后的类当成同一版本的类,序列化和反序列化照样可以正常使用。如果我们不显式的定义这个 SerialVersionUID,Java 虚拟机会根据类的信息帮我们自动生成,修改前和修改后的计算结果往往不同,造成版本不兼容而发生反序列化失败,另外由于平台的差异性,在程序移植中也可能出现无法反序列化。强大的 IDE 工具,也都有自动生成 SeriaVersionUID 的方法,这里就不多说了。JDK 中自带的也有生成 SeriaVersionUID 值的工具 serialver.exe,使用 serialver 类名(编译后) 命令就能生成该类的 SeriaVersionUID 值啦!

总结:

  • 序列化和反序列化的方式可以分为三种,一种是实现 Serializable 接口使用默认的序列化和反序列化方式,一种是实现 Serializable 接口但是自定义序列化和反序列化方法,另外一种是实现 Externalizable 接口,实现接口中的方法。
  • 序列化和反序列化要注意版本问题,自定义序列化和反序列化时还要注意属性的顺序要保持一致,这些都可能会导致反序列化失败。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏听Allen瞎扯淡

Integer的highestOneBit方法源码解析

在读HashMap源码的时候,遇到了Integer的highestOneBit静态方法不是太理解,所以就读了一下源码,这里记录一下。

2991
来自专栏Coding迪斯尼

自制Monkey编程语言编译器:增加数组操作API和Mapsh数据类型

1233
来自专栏闵开慧

java概念1

public static void main(String[] args) {//其中[]也可以写在args后面,args也可以随便写成其他字母,例如asd...

35611
来自专栏前端儿

表达式求值

ACM队的mdd想做一个计算器,但是,他要做的不仅仅是一计算一个A+B的计算器,他想实现随便输入一个表达式都能求出它的值的计算器,现在请你帮助他来实现这个计算器...

1282
来自专栏IT大咖说

深入学习 Java 序列化

2264
来自专栏技术碎碎念

Java8 Collectors.toMap的坑

按照常规思维,往一个map里put一个已经存在的key,会把原有的key对应的value值覆盖,然而通过一次线上问题,发现Java8中的Collectors.t...

3761
来自专栏程序生活

Python 过滤字母和数字实例1实例 2实例 3

2752
来自专栏ShaoYL

OC语言Block 续

1389
来自专栏屈定‘s Blog

Java--Enum的思考

枚举类是Java5引进的特性,其目的是替换int枚举模式或者String枚举模式,使得语义更加清晰,另外也解决了行为和枚举绑定的问题.

1394
来自专栏WindCoder

MyBatis传入参数为集合 list 数组 map写法

这几天需要or和拼接in的特定查询条件来做查询,想看看mybatis是否可以通过传递list集合实现,于是找到了他的foreach标签。

3.2K2

扫码关注云+社区

领取腾讯云代金券