目录
1. 序列化流与反序列化流的基本介绍
2. 序列化流的基本用法?
3. 序列化流的作用?
4. 反序列化流的基本用法?
5. 反序列化流的作用
6. 序列化流与反序列化流使用时需要注意的细节(非常重要)
6.1 被序列化的JavaBean类必须实现 Serializable 接口,
6.2 javaBean类尽量在定义时加上"版本号"
6.3 transient 关键字的使用
6.4 序列化流得到的文件不要修改
1. 序列化流与反序列化流的基本介绍
如上图所示,序列化流与反序列化流也是IO流中会用到的一种高级流,也是用来包装基本流对象的,它们的继承关系如上图中所示。
序列化流是字节流的一种,它负责输出数据。
反序列化流也是字节流的一种,它负责输入数据。
2. 序列化流的基本用法?
如下图所示为序列化流的构造器,序列化流也是一个高级流,它的参数需要传递一个基本流对象;
序列化流地写方法也很简单,名为writeObject(),参数需要传递一个对象,如下所示;
我来简单给大家演示一下它的使用
(1)既然它是用来写对象的,我们就先创建一个JavaBean类,如下我定义了一个银行账户类
// 这里面要记住,一定要记得实现 Serializable 接口,否则会序列化失败
public class Account implements Serializable {
// 账户id
private Integer accountId;
// 账户人名称
private String accountName;
// 账户密码
private String accountPassword;
@Override
public String toString() {
return "Account{" +
"accountId=" + accountId +
", accountName='" + accountName + '\'' +
", accountPassword='" + accountPassword + '\'' +
'}';
}
public Integer getAccountId() {
return accountId;
}
public void setAccountId(Integer accountId) {
this.accountId = accountId;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public String getAccountPassword() {
return accountPassword;
}
public void setAccountPassword(String accountPassword) {
this.accountPassword = accountPassword;
}
}
接着我们来到另一个测试类,创建一个 main 方法,测试序列化流的使用,我把注释也标注进去
public static void main(String[] args) {
// 定义一个账户对象并赋予初始值
Account account = new Account(101,"张三","123456");
// 创建一个 ObjectOutputStream 的流对象
ObjectOutputStream oos = null;
try {
// 对 oos 赋值
// 将 account 的而对象的结果写道 account.txt 文件中去
oos = new ObjectOutputStream(new FileOutputStream("user-service/account.txt"));
// 将 account 对象写出
oos.writeObject(account);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 保证给程序的健壮性,关闭之前先做判空操作,否则会空指针异常
try {
if (oos != null)
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们运行此main 方法,在项目目录下会得到名为 account.txt 的文件,里面就存放着我们 account 对象的信息,我们打开查阅如下所示
可以看到,我们打开所看到的是一大堆乱码,这是为什么呢?
其实这是正常现象,序列化流可以把Java文件写到本地文件中去,通常情况下,写到本地文件的系列化流文件我们都是看不懂的,因为这本来就不是给我们看的,而是用于给程序看的。下面我们就此来引出序列化流的作用。
3. 序列化流的作用?
通过刚才的一个小例子,我们简单了解了序列化流的使用,也留下了一个问题。既然写出的文件看不懂,我们要他来做什么用呢?
怎么说呢,我举个最简单的例子,假如你电脑上有一个单机游戏,比如植物大战僵尸吧,大家不知道玩过没有,里面就有金币对吧,当我们不想玩了之后,关闭游戏游戏是会把我们的金币数额和所有的通关进度通过IO流的方式写到我们的本地磁盘上去的,当我们再次打开要玩的时候,它会再次读取到本地文件中的内容,将我们的金币数额和通关进度在读到内存中,我们就可以接着上次的进度游玩了。
游戏将金币数额与游戏进度保存的这个过程,就和序列化很相似;
游戏会将金币数额和游戏进度再次读取到内存中去,就和反序列化有些相似。
那么现在就有一个问题了,如果你是这个游戏的设计者,你在序列化的时候,序列化的得到的游戏进度文件希望玩家能看懂能修改吗?肯定不能吧,如果任意玩家都可以看得懂并且能修改序列化得到的游戏进度文件,那和开外挂有什么区别?我直接把金币改成9999999999,不用再打金币了。
这样举例子各位应该就有些明白了吧!
这也只是序列化流的用途之一,序列化流与反序列化流的用途还有很多,比如互联网传输用户数据,我们通常都是采用二进制传输,你总不能将数据原样进行传输吧,特别是如果传输的数据带有敏感信息,比如账户名称密码,银行账户密码,这些是绝对不能让用户看得懂可以修改的。一旦被不法人员拦截攻击,造成的损失会非常大,因此就可以使用我们的序列化流,将要传输的数据进行序列化处理。
4. 反序列化流的基本用法?
如上图所示,即为反序列化流的构造方法与常用方法,我们来简单的测试一下吧
如下代码,我们把刚才序列化的 account 对象在反序列化回来,我们试一试
public static void main(String[] args) {
// 定义一个账户对象并赋予初始值
Account account = new Account(101,"张三","123456");
// 创建一个 ObjectOutputStream 的流对象
ObjectInputStream ois = null;
try {
// 对 ois 赋值
// 将 account.txt 文件中保存的对象数据再读出来
ois = new ObjectInputStream(new FileInputStream("user-service/account.txt"));
// 将 account 对象读出
Object o = ois.readObject();
// 输出独到的 account 对象数据
System.out.println(o);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
// 保证给程序的健壮性,关闭之前先做判空操作,否则会空指针异常
try {
if (ois != null)
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行 main 方法,就可以得到如下图所示的结果
可以看到,原本我们创建的对象 account 就又被我们读取出来了,而且还能还得懂。
5. 反序列化流的作用
通过刚才的演示与对序列化流作用的说明,这里就不需要我在多做解释了,总而言之就一句话。
反序列化流可以将序列化的数据载读取为我们可以看的懂可以操作的原本模样。
6. 序列化流与反序列化流使用时需要注意的细节(非常重要)
6.1 被序列化的JavaBean类必须实现 Serializable 接口,
我们可以来看一下 Serializable 接口的源码,如下所示
可以看到,再序列化流接口中,没有任何东西,这种借口有一个专业名词,被称为标记型接口,备有实际含义,只是作为一个标记,只有被标记的JavaBean类才能参与序列化。如果不实现该接口,就会报错,这里应该也不用多解释,各位在做 Spring 项目的时候,通常都会在javaBean类中是实现 Serializable 接口,因为我们项目运行时的数据就是在互联网之间进行传输的,所以需要实现 Serializable 接口。
6.2 javaBean类尽量在定义时加上"版本号"
有些小伙伴可能不知道,当你对一个JavaBean类添加了 Serializable 接口要参与序列化时,该JavaBean类是有版本号的,如果你对javaBean类做了修改,版本号也会修改,一旦版本号修改,再序列化与反序列化时,就会出现错误,因此我们最好在JavaBean类中添加上版本号,这个我们可以借鉴 ArrayList 的源码设计哦,我们来看一下
在ArrayList 源码中我们可以发现,它也定义了一个版本号,而且各位要注意,这个版本号变量不是乱起的,必须是 serialVersionUID,不能是别的;如果不添加 serialVersionUID 变量,我们的javaBean类一旦发生修改,版本号也会修改,就是因为版本号不一致导致无法序列化成功,因此我们需要让版本号使用为一个,就需要定义为 final,又因为它是该类属性中共享属性,需要定义为 static ,serialVersionUID 最好定义为 long 类型。
因此,在定义版本号时,最好定义为
private static final long serialVersionUID = ?
至于取什么值,无所谓,只要遵守该变量的定义原则即可。
6.3 transient 关键字的使用
还有一些时候,我们不想让JavaBean 类中的一些变量参与序列化与反序列化,该怎么办呢?
这个时候就可以用到我们的 transient 关键字。
被 transient 关键字修饰的变量,不会参与到类的序列化与反序列化,我来给大家演示一下,还拿刚才的例子举例说明,如下图所示
我把 accountpassword 账户密码这一属性不参与数列化,因为太不安全,我们就可以在JavaBean类中 该变量前面加上 transient 关键字,一旦加上,就不会参与序列化,我们再回到 main 方法作出修改
public static void main(String[] args) {
// 定义一个账户对象并赋予初始值
Account account = new Account(101,"张三","123456");
// 创建一个 ObjectOutputStream 的流对象
ObjectOutputStream oos = null;
// 创建一个 ObjectOutputStream 的流对象
ObjectInputStream ois = null;
try {
// 对 oos 赋值
oos = new ObjectOutputStream(new FileOutputStream("user-service/account.txt"));
// 对 ois 赋值
// 将 account.txt 文件中保存的对象数据再读出来
ois = new ObjectInputStream(new FileInputStream("user-service/account.txt"));
// 先将 account 对象写出
oos.writeObject(account);
// 再将 account 对象读出
Object o = ois.readObject();
// 输出独到的 account 对象数据
System.out.println(o);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
// 保证给程序的健壮性,关闭之前先做判空操作,否则会空指针异常
try {
if (oos != null)
oos.close();
if (ois != null)
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后再次运行得到如下结果
可以发现,即便我们初始为 accountPassword 赋值,但因为添加了 transient 关键字的缘故,他就不会参与序列化与反序列化,系统胡自动给一个默认值为 null。
各位小伙伴有些时候再看源码时的时候可能也见过 transient 关键字,就是用来做标记的,不让其被标记的变量参与序列化。
6.4 序列化流得到的文件不要修改
我们在通过序列化得到的文件,虽然是一堆乱码看不懂,但时不要轻易去修改,因为一旦修改,再次反序列化时,程序就读不出来了,会报错,这也是序列化流中需要注意的一个点哦!