Skip to main content

moregeek program

java 中那些绕不开的内置接口 -- serializable_wx630f055ce23fc的博客-多极客编程

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

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

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

本文大纲如下:

Java 中那些绕不开的内置接口 -- Serializable_java

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 = -7792628363939354385L;

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 中那些绕不开的内置接口 -- Serializable_java_02

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

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 = 1L;
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(20);
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 p2 = (Person) objectInputStream.readObject();
objectInputStream.close();

assertTrue(p2.getAge() == person.getAge());
assertTrue(p2.getName().equals(person.getName()));
}

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

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

总结

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

下一篇我们分享 Iterable 和 Iterator 这两个名字看起差不多的 Java 内置接口。

©著作权归作者所有:来自51CTO博客作者小二上酒8的原创作品,请联系作者获取转载授权,否则将追究法律责任

redis 缓存数据库一致性手撕面答_wx630f055ce23fc的博客-多极客编程

引言自Redis入门篇过后,已经好久没更Redis了,接下来应该从实战篇,原理篇,面试篇几个层次来展开,本篇主要是实战篇中的数据库缓存一致性问题,以问题形式展开,应对面试场景作答【melo称其为"手撕面答"】,尽量简短,某些部分可能不会进行详细介绍。🎨本篇脑图速览🎯为什么是删除缓存而不是更新缓存?🎐懒加载一种懒加载的思想,因为每次更改数据之后,不一定立马就有人来用。 若更新的次数远大于读取的次数,

从阿里规约看spring事务_博学谷狂野架构师的博客-多极客编程

目标:事务失效引发的灾难 H4如下图(张三--->李四转账) tips 下订单-------订单支付-----减库存(失败) 超卖现象 代码回忆: //实现类 public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Resource priv

java多线程(4):threadlocal_湘王的博客-多极客编程

您好,我是湘王,这是我的51CTO博客,欢迎您来,欢迎您再来~​为了提高CPU的利用率,工程师们创造了多线程。但是线程们说:要有光!(为了减少线程创建(T1启动)和销毁(T3切换)的时间),于是工程师们又接着创造了线程池ThreadPool。就这样就可以了吗?——不,工程师们并不满足于此,他们不把自己创造出来的线程给扒个底朝天决不罢手。有了线程关键字解决线程安全问题,有了线程池解决效率问题,那还有

?spring bean的8种加载方式,90%的程序员都说不全?_wx630f055ce23fc的博客-多极客编程

前言以前学习Spring框架的时候,总结了几种Bean的加载方式,不过老师说还有其它的加载方式,以下八种并不是全部,但也足以用来做很多事情了。注意以下是Spring中Bean的加载方式,不是SpringBoot,但其中的很多东西是相通的,尤其是Bean的注入方式、适用场景等,在本文中也有介绍的。分享给大家一起学习,如有错误的地方,麻烦各位在评论区帮我指出🤣🤣。1.xml+被配置的bean需要有无参

agileboot - 如何集成内置数据库h2和内置redis_wx630f055ce23fc的博客-多极客编程

背景介绍为什么我们需要内置的数据库和Redis呢?优点:内置的数据库H2,可以让我们在无依赖数据库的情况下,做集成测试。比如我们想测试添加一个学生到数据库,就需要启动一台数据库来验证结果。然而使用H2内置数据库的话,就可以在无依赖数据库的情况下,进行验证。数据库H2是内存级的,如果有大量集成测试(IT:Integration Test)的话,可以缩短测试时间。Embedded Redis同上,使我

排名前 16 的 java 工具类,哪个你没用过的?_码农小宋的博客-多极客编程

在Java中,实用程序类是定义一组执行通用功能的方法的类。这篇文章展示了最常用的Java实用工具类及其最常用的方法。类列表及其方法列表均按受欢迎程度排序。数据基于从GitHub随机选择的50,000个开源Java项目。希望您可以通过浏览列表来了解已经提供和流行的功能的一些想法,以使您知道不需要自己实现。这些方法的名称通常指示它们的作用。如果方法名称不够直观,您还可以查看其他开发人员如何在其开源项目

redis 缓存数据库一致性手撕面答_wx630f055ce23fc的博客-多极客编程

引言自Redis入门篇过后,已经好久没更Redis了,接下来应该从实战篇,原理篇,面试篇几个层次来展开,本篇主要是实战篇中的数据库缓存一致性问题,以问题形式展开,应对面试场景作答【melo称其为"手撕面答"】,尽量简短,某些部分可能不会进行详细介绍。🎨本篇脑图速览🎯为什么是删除缓存而不是更新缓存?🎐懒加载一种懒加载的思想,因为每次更改数据之后,不一定立马就有人来用。 若更新的次数远大于读取的次数,

go 云原生实战:如何增加 web 应用配置模块_宇宙之一粟的漂泊之旅的博客-多极客编程

1 介绍当我们为自己编写程序时,通常会将一些重要的配置项直接写在源代码里,比如:服务器监听的端口、数据库使用的名称和端口号、HTTP请求超时的持续时间...但是,如果我们尝试将这个项目开源分享给他人使用,用户使用的数据库的用户名和名称可能与你不相同,甚至你还要为他们的服务器使用另一个端口。如果你还设置了数据库的密码的话,为了安全,更不可能在代码中信息泄露出来。因此,本节,将介绍如何增加我们的 ​​

从阿里规约看spring事务_博学谷狂野架构师的博客-多极客编程

目标:事务失效引发的灾难 H4如下图(张三--->李四转账) tips 下订单-------订单支付-----减库存(失败) 超卖现象 代码回忆: //实现类 public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Resource priv

?spring bean的8种加载方式,90%的程序员都说不全?_wx630f055ce23fc的博客-多极客编程

前言以前学习Spring框架的时候,总结了几种Bean的加载方式,不过老师说还有其它的加载方式,以下八种并不是全部,但也足以用来做很多事情了。注意以下是Spring中Bean的加载方式,不是SpringBoot,但其中的很多东西是相通的,尤其是Bean的注入方式、适用场景等,在本文中也有介绍的。分享给大家一起学习,如有错误的地方,麻烦各位在评论区帮我指出🤣🤣。1.xml+被配置的bean需要有无参

agileboot - 如何集成内置数据库h2和内置redis_wx630f055ce23fc的博客-多极客编程

背景介绍为什么我们需要内置的数据库和Redis呢?优点:内置的数据库H2,可以让我们在无依赖数据库的情况下,做集成测试。比如我们想测试添加一个学生到数据库,就需要启动一台数据库来验证结果。然而使用H2内置数据库的话,就可以在无依赖数据库的情况下,进行验证。数据库H2是内存级的,如果有大量集成测试(IT:Integration Test)的话,可以缩短测试时间。Embedded Redis同上,使我

排名前 16 的 java 工具类,哪个你没用过的?_码农小宋的博客-多极客编程

在Java中,实用程序类是定义一组执行通用功能的方法的类。这篇文章展示了最常用的Java实用工具类及其最常用的方法。类列表及其方法列表均按受欢迎程度排序。数据基于从GitHub随机选择的50,000个开源Java项目。希望您可以通过浏览列表来了解已经提供和流行的功能的一些想法,以使您知道不需要自己实现。这些方法的名称通常指示它们的作用。如果方法名称不够直观,您还可以查看其他开发人员如何在其开源项目