Java 内置接口 Serializable示例详解

Java
244
0
0
2023-06-10
目录
  • 引言
  • Serializable 接口
  • Serializable 是一个标记型接口
  • serializable Version UID
  • Java 序列化与JSON序列化的区别
  • Java序列化相较于 JSON 的优势
  • Java 类对象的序列化代码演示
  • 总结

引言

上一部分我们着重讲了 Java 集合框架中在开发项目时经常会被用到的数据容器,在讲解、演示使用实践的同时,把这个过程中遇到的各种相关知识点:泛型、Lambada、Stream 操作,一并给大家做了梳理。

从这篇开始我们进入下一部分,用三到五部分给大家梳理一下,在用 Java 编程时,那些我们绕不开的 interface;从最基本的 Serializable 到 Comparable 和 Iterator 这些,再到 Java 为了支持函数式编程而提供的 Function、Predicate 等 interface。

这些 Java 内置提供的 interface 或多或少我们在写 Java 代码的时候都见过,有的甚至是潜移默化地在日常编码中已经实现过其中的一些 interface,只不过我们没有察觉到罢了。相信通过阅读着几篇文章,一定会让你在写 Java 代码时更清楚自己是在做什么,不会再被这些个似曾相识的 interface 困扰到。

本文大纲如下:

Serializable 接口

作为 Java 中那些绕不开的内置接口 这个小系列的开篇文章,首先要给大家介绍的 interface 是 Serializable。

Serializable这个接口的全限定名(包名 + 接口名)是 java.io.Serializable,这里给大家说个小技巧,当你看到一个类或者接口的包名前缀里包含java.io那就证明这个类 / 接口它跟数据的传输有关。

Serializable 是 Java 中非常重要的一个接口,如果一个类的对象是可序列化的,即对象在程序里可以进行序列化和反序列化,对象的类就一定要实现Serializable接口。那么为什么要进行序列化和反序列化呢?

序列化的意思是将对象的状态转换为字节流;反序列化则相反。换句话说,序列化是将 Java 对象转换为静态字节流(序列),然后我们可以将其保存到文件、数据库或者是通过通过网络传输,反序列化则是在我们读取到字节流后再转换成 Java 对象的过程;这也正好解释了为什么Serializable 接口会归属到java.io包下面。

Serializable 是一个标记型接口

虽说需要进行序列化的对象,它们的类都需要实现 Serializable 接口,但其实你会发现,我们在让一个类实现 Serializable 接口时,并没有额外实现过什么抽线方法。

import java.io.Serializable;
public class Person implements Serializable {
    private String name;
    private int age;
}

比如向上面个类文件里的内容,Person 类声明实现 Serializable 接口后,并没有去实现什么抽象方法,IDE 也不会用红线警告提示我们:“你有一个抽象方法需要实现” ,原因是 Serializable 接口里并没有声明抽象方法。

public interface Serializable {
}

这种不包含任何方法的 interface 被称为标记型接口,类实现 Serializable接口不必实现任何特定方法,它只起标记作用,让 Java 知道该类可以用于对象序列化。

serializable Version UID

虽说一个类实现了 Serializable 接口的时候不需要实现特定的方法,但是经常会看到一些实现了Serializable的类中,都有一个名为serialVersionUID类型为long的私有静态 属性。

import java.io.Serializable;
public static class Person implements Serializable {
    private static final long serialVersionUID = -L;
    public String name;
    public int    age;
}

该属性修饰符里使用了final即赋值后不可更改。Java 的对象序列化 API 在从读取到的字节序列中反序列化出对象时,使用 serialVersionUID 这个静态类属性来判断:是否序列化对象时使用了当前相同版本的类进行的序列化。Java 使用它来验证保存和加载的对象是否具有相同的属性,确保在序列化上是兼容的。

大多数的 IDE 都可以自动生成这个 serialVersionUID静态属性的值,规则是基于类名、属性和相关的访问修饰符。任何更改都会导致不同的数字,并可能导致 InvalidClassException。 如果一个实现 Serializable 的类没有声明 serialVersionUID,JVM 会在运行时自动生成一个。但是,强烈建议每个可序列化类都声明 serialVersionUID,因为默认生成的serialVersionUID依赖于编译器,因此可能会导致意外的InvalidClassExceptions。

我上面那个例子里,Person 类的serialVersionUID是用 Intelij IDEA 自动生成的,所以值看起来一大串,不是我自己些的。IDEA 默认不会给可序列化类自动生成 serialVersionUID 需要安装一个插件。

这里给大家放一个截图,插件的安装和使用,网上有很多例子,大家需要的话动手搜一下,这里就不再占用太多篇幅讲怎么安装和使用这个插件了。

Java 序列化与JSON序列化的区别

Java 的序列化与现在互联网上 Web 应用交互数据常用的 JSON 序列化并不是一回事儿,这是咱们需要注意的,像 Java、C#、PHP 这些编程语言,都有自己的序列化机制把自家的对象序列化成字节然后进行传输或者保存,但是这些语言的序列化机制之间并不能互认,即用 Java 把对象序列化成字节、通过网络 RESTful API 传给一个 PHP 开发的服务,PHP 是没办法反序列化还原出这个对象的。这样才有了 JSON、XML、Protocol Buffer 这样的更通用的序列化标准。

例如在实际项目开发的时候,Java 对象往往被序列化为 JSON、XML 后再在网络上传输,如果对数据大小敏感的场景,会把 Java 对象序列化成空间占用更小的一些二进制格式,比如 Protocol Buffer ( 分布式 RPC 框架 gRPC 的数据交换格式)。这样做的好处是序列化后的数据可以被非 Java 应用程序读取和反序列化,例如,在 Web 浏览器中运行的 JavaScript 可以在本地将对象序列化成 JSON 传输给 Java 写的 API 接口,也可以从 Java API接口返回响应中的 JSON 数据,反序列化成 JavaScript 本地的对象 。

像上面列举的这些对象序列化机制,是不需要我们的 Java 类实现 Serializable 接口的。这些 JSON、XML 等格式的序列化类,通常使用 Java 反射来检查类,配合一些特定的注解完成序列化。

Java序列化相较于 JSON 的优势

上面介绍了 JSON 这样的通用序列化格式的优势,有的可能会问了,那还用 Java 序列化干啥。这里再给大家分析一下,Java 对象序列化虽然在通用性上不如 JSON 那些序列化格式,但是在 Java 生态内部却是十分好用的,其最聪明的一点是,它不仅能保存对象的副本,而且还会跟着对象里面的reference,把它所引用的对象也保存起来,然后再继续跟踪那些对象的reference,以此类推。

这个机制所涵盖的范围不仅包括对象的成员数据,而且还包含数组里面的reference。如果你要自己实现对象序列化的话,那么编写跟踪这些链接的程序将会是一件非常痛苦的任务。但是,Java的对象序列化就能精确无误地做到这一点,毫无疑问,它的遍历算法是做过优化的。

另外你们在一些资料里看过 Java Bean 的定义

1、所有属性为private

2、提供默认构造方法

3、提供getter和setter

4、实现java.io.Serializable接口

那么问题来了,为什么要进行序列化?每个实体bean都必须实现serializabel接口吗?以及我做项目的时候,没有实现序列化,同样没什么影响,到底什么时候应该进行序列化操作呢?

这里转载一个网上大佬对这个问题的解释

首先第一个问题,实现序列化的两个原因:

1、将对象的状态保存在存储媒体中以便可以在以后重新创建出完全相同的副本;

2、按值将对象从一个应用程序域发送至另一个应用程序域。实现serializabel接口的作用是就是可以把对象存到字节流,然后可以恢复,所以你想如果你的对象没实现序列化怎么才能进行持久化和网络传输呢,要持久化和网络传输就得转为字节流,所以在分布式应用中及设计数据持久化的场景中,你就得实现序列化。

第二个问题,是不是每个实体bean都要实现序列化,答案其实还要回归到第一个问题,那就是你的bean是否需要持久化存储媒体中以及是否需要传输给另一个应用,没有的话就不需要,例如我们利用fastjson将实体类转化成json字符串时,并不涉及到转化为字节流,所以其实跟序列化没有关系。

第三个问题,有的时候并没有实现序列化,依然可以持久化到数据库。这个其实我们可以看看实体类中常用的数据类型,例如Date、String等等,它们已经实现了序列化,而一些基本类型,数据库里面有与之对应的数据结构,从我们的类声明来看,我们没有实现serializabel接口,其实是在声明的各个不同变量的时候,由具体的数据类型帮助我们实现了序列化操作。

另外需要注意的是,在NoSql数据库中,并没有与我们Java基本类型对应的数据结构,所以在往nosql数据库中存储时,我们就必须将对象进行序列化,同时在网络传输中我们要注意到两个应用中javabean的serialVersionUID要保持一致,不然就不能正常的进行反序列化。

Java 类对象的序列化代码演示

到这里 Serializable 需要了解的基础知识就都给大家梳理出来了,这块属于选读,用 Java 编程写序列化代码的场景并不是太多,不过有兴趣就再接着往下看吧,有个印象,这样以后写代码的时候,哪天用上了,还能快速想起来在哪看过,再回来翻看。

Java 对象序列化(写入)由 ObjectOutputStream 完成,反序列化(读取)由 ObjectInputStream 完成。ObjectInputStream 和 ObjectOutputStream 是分别继承了 java.io.InputStream 和 java.io.OutputStream 抽象的实体类。 ObjectOutputStream 可以将对象的原型作为字节流写入 OutputStream。然后我们可以使用 ObjectInputStream 读取这些流。 ObjectOutputStream 中最重要的方法是:

public final void writeObject(Object o) throws IOException;

这个方法接收一个可序列化对象(实现了 Serializable 接口的类的对象)并将其转换为字节序列。同样,在ObjectInputStream 中最重要的方法是:

public final Object readObject() throws IOException, ClassNotFoundException;

此方法可以读取字节流并将其转换回 Java 对象。然后我们可以再使用类型转换(Type Cast)将其转换回原始的类型对象。

下面我们使用文章示例里的Person类再给大家演示一下 Java 的序列化代码。

public class Person implements Serializable {
    private static final long serialVersionUID =L;
    static String country = "ITALY";
    private int age;
    private String name;
    transient int height;
    // 省略 getter 和 setter
}

这里要注意一下, static 修饰的静态属性是类属性,并不属于对象,所以在序列化对象时不会把类中的静态属性序列化了,另外我们也可以使用 transient关键字修饰那些我们想在序列化过程中忽略调的对象属性。

@Test 
public void serializingAndDeserializing_ThenObjectIsTheSame() () 
  throws IOException, ClassNotFoundException { 
    Person person = new Person();
    person.setAge();
    person.setName("Joe");
    // 用指定文件路径--当前目录的 test_serialization.txt 文件创建 FileOutputStream。
    // 在写入 FileOutputStream 时, FileOutputStream 会在在项目目录中创建文件
    // “test_serialization.txt”
    FileOutputStream fileOutputStream
      = new FileOutputStream("./test_serialization.txt");
    // 以 FileOutputStream 为底层输出流创建对象输出流 ObjectOutputStream
    ObjectOutputStream objectOutputStream 
      = new ObjectOutputStream(fileOutputStream);
    // 向 ObjectOutputStream 中写入 person 对象
    objectOutputStream.writeObject(person);
    // 把数据从流中刷到磁盘上
    objectOutputStream.flush();
    objectOutputStream.close();
    // 用上面的文件路径,创建文件输入流
    FileInputStream fileInputStream
      = new FileInputStream("./test_serialization.txt");
    // 以文件输入流创建对象输入流 ObjectInputStream
    ObjectInputStream objectInputStream
      = new ObjectInputStream(fileInputStream);
    // 用对象输入流读取到文件中保存的序列化对象,反序列化成 Java Object 再转换成 Person 对象
    Person p = (Person) objectInputStream.readObject();
    objectInputStream.close(); 
    assertTrue(p.getAge() == person.getAge());
    assertTrue(p.getName().equals(person.getName()));
}

上面这个单元测试里的代码演示了,怎么把 Person 类的对象进行 Java 序列化保存到文件中,再从文件中读取对象被序列化后的字节序列,然后还原成Person类的对象。

因为我们的专栏还没有设计到 Java IO 这块的内容,所以各种输入输出流就不过多进行讲解了,为了方便大家阅读时理解上面的程序,我在上面程序注释里已经详细注释了每一步完成的操作,这些输入输出流我们等到讲到 Java IO 体系的时候再详细进行讲解。

总结

今天给大家梳理了 Java Serializable 接口的一些必须要了解的知识,Serializable 接口在我们用 Java 编程的时候经常见,但是很多人并不了解它的作用,因为它的主要作用还是用于标记类是否是可序列化类,这样 Java 的 ObjectOutputStream 和 ObjectInputStream 才能对类的对象进行序列化和反序列化。