【Java笔记分享】序列化与反序列化 & 对象流

笔记源代码地址:B站 库米豪斯巴达锅铲祖师 空间

昨天断网了,作业没法做,文章没法更,写了一晚上贪吃蛇就写了个框架qwq

序列化与反序列化的认识

在学习对象流之前,我们需要了解一下序列化的概念

你有一个类对象,而你现在需要关机,但你不能失去这个类对象的信息,在这种情况下,你需要以一定的格式在文件中保存这个对象的信息

这种将对象的信息转换成可存储,运输,读取的,且具有一定格式的形式的过程,叫做序列化(serialization)。这种存储形式一般使用文件

而反序列化(deserialization),就是相反地,将对象信息的载体中的数据读取,并组装成对象以使用的过程

在Java中,对于对象的序列化与反序列化,常常使用对象流来操作

对象流的使用

我们要序列化,也就是将对象写入文件,而输出操作一定需要流。这里,我们使用ObjectOutputStream来序列化

基本用法还是普通流的用法,只是有两点要注意

  • ObjectOutputStream需要作为包装流使用
  • 专门用于序列化方法是writeObject()

所以,有了前面的基础,对于一个ArrayList对象,我们可以直接写出这样的序列化操作

try (var oos = new ObjectOutputStream(new FileOutputStream("obj"))) {
    oos.writeObject(new ArrayList());
    
    oos.flush();
} catch (IOException e) {
    e.printStackTrace();
}

不用创建文件,生成FileOutputStream的时候会自动创建一个。这里文件输出流的路径不用带后缀,要是带了奇奇怪怪的后缀反而容易出错(我就是把对象序列化在txt里面,然后反序列化的时候,文件的数据一大半被删掉了。。。我也不知道为什么)

至于反序列化,我们来推断一下用法。根据序列化的方法和流方法的共同点,我们可以推断出应该使用readObject()方法来反序列化,且这个方法会返回一个Object并抛出ClassNotFoundException。对于这个被返回的对象,有可能是Object的子类对象,而返回时标注的类型是Object,所以如果要作为Object的子类来操作,我们需要类型强制转换。以之前添加的ArrayList对象为例,我们应该这样写

try (var ois = new ObjectInputStream(new FileInputStream("obj"))) {
    Object list = ois.readObject();
    if (list instanceof ArrayList trueList) {
        System.out.println(trueDog);
    }
} catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
}

注:这里的instanceof关键字的用法是JDK14以后的新写法,叫做 “instanceof模式匹配机制”,在判断的同时完成了转换的操作。可等效为:

if (list instanceof ArrayList) {
    ArrayList trueList = (ArrayList) list;
    System.out.println(trueList);
}

于是,控制台上就出现了你写入的对象的信息(如果出现EOFException,那没准你是在用txt保存呢,把后缀去掉没准就好了呢)

对于多个对象的序列化,推荐是将这些对象都存进集合,然后序列化这个集合,下次拿出来的时候对象也都在里面

自定义可序列化类

当你写项目的时候,一定会自己定义类,然后你要将自己定义的类序列化,屁颠屁颠地去试了,发现,不支持

比如我这里自定义一个Dog类,创建了一个对象并试图序列化,看到控制台没有Dog类的信息,而是这么几行:

java.io.NotSerializableException: Dog
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1202)
	at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:362)
	at Test.main(Test.java:9)

这个异常很好理解,就是说Dog类不能序列化,在Java中,如果要让一个类可序列化,需要实现一个接口:Serializable(可序列化的)

这个接口。。。怎么回事,这里面啥都没有,甚至连注解都没有

那是因为这个接口不是给程序员看的,是给JVM看的。JVM看到了,就知道这个类是可以序列化的

但是!还有一个问题:如果类中有比较机密,涉及公共安全,丢失或泄露会造成巨大影响,然后这个类还需要序列化。。。

等死?不,这个问题其实带久不

Java当然是考虑到这一点的,于是我们解锁了一个新的关键字:transient

transient,本意是“转瞬即逝的;瞬态”,在Java中的专业释义可以是:不参与序列化的,位于修饰符列表(就是和public,static这种关键字一个用法的),修饰的字段所在类可序列化的时候才会起作用

比如银行用户类,内有用户名,内有密码,密码不能给人看,于是我们用transient关键字来修饰,再封装一下,双重保险,黑帽子黑客直呼内行并放弃治疗(不太可能,不要低估他们的能力)

private transient String password;

那么这种被transient修饰过变量的类对象,反序列化的时候,就不会有这个属性的数据。对于基本数据类型,统一赋值为0;对于引用数据类型,统一赋值为null

序列化版本号

这是一个long类型的常量,说了你不信,这是用来检测两个对象是不是同一个类的

ovo

我这么说了你肯定不信。我把这个一放,肯定有人觉得眼熟的。然后我们来解释为什么它是这个作用的

private static final long serialVersionUID = 1L;

这个,在百度百科上有直白但没那么好理解的解释:类的最初版本的指纹。稍微解释一下,就是:同一个类名,但是内容不同的情况下,这两种模样的类各有一个互不相同的序列化版本号。这里又要涉及到JVM对同类的判定

对于JVM来说,你类名相同,不足以构成同类,只有当类名和序列化版本号都一样的时候,会被判定为同类。因为你即使类名相同,程序员还是可以更改类的内容的

这样,你写了一个类,不写序列化版本号,给它序列化了,然后你把这个类的源代码改了,过一段时间再去读取,给你报异常。因为源代码更改后,相对原来的类必定会有读不到的数据或者白读的数据,这两个类长得不一样了,即使类名相同也不能成功反序列化


让我们来测试一下这个特性

使用eclipse或vscode的同学可以尝试一下,随便写一个类,多离谱都没关系,然后实现接口,然后你会发现它报警告,根据警告提示信息,把序列化版本号生成一下(现在不要点带“默认”或者“default”的那一条,我们测试呢)

使用IDEA的同学,不好意思IDEA不给你报警告,但是设置里可以调,自行百度调好后回来学习

你有没有看到它给你生成那老长一段数字?

给他删了,然后对这个类进行离谱的更改;小到删掉一个static,大到加了好几个方法都可以,然后你再生成一下序列化版本号

你有没有发现这次的长数字和之前的不一样了?就是你更改了类,然后Java觉得你这个类不一样了,给你生成了一个新的

有幸没有改动很大的同学,可以尝试给它改回原来的样子,然后重新生成,和第一次的是一样的

no,不要急着去看这样造成的异常。我看到很多案例,全在问:为什么我这个类改了,序列化版本号重新生成了,它不给我报异常

JVM只会看你要操作的是不是同一个类,是就给你反序列化,也就是:如果没有生成序列化版本号,你只要保证序列化和反序列化的过程中,你那类的源代码一下没动,或者动了但又回去了,那就是同一个类,如果你生成了序列化版本号而且没改,那你可劲动,给它删光了它还是同一个类。

原因是就算你自己没创建序列化版本号,JVM它会自动生成。你得到的长数字就是它自动生成的。你改了源代码后,它会重新生成,前后就不是一个类了。

由此:报异常的条件是:你序列化后,序列化版本号动过了不调回来,再去反序列化,它给你报异常

finally——

这块内容好像各种问题都会出来,总有人死也学不会。up是根据自身遇到过的困难来解释的,如果你遇到了其它奇怪的问题,来问我吧私聊我,不信我的可以发评论区

java
84 views
Comments
登录后评论
Sign In
·

还有java反序列化的安全问题。这个很重要,可以了解下。