前言
java的序列化和反序列化内容是java学习的基础之一,java的序列化常见于网络中的对象传输以及内存对象持久化,本篇文章将主要对java中如何使用序列化,transient关键字作用和序列化中的serialVersionUID的作用三个部分的内容进行讲解,希望对这方面不熟悉的读者有所帮助。
一、什么是序列化(反序列化)
我们不妨设想这样一个场景,假如你用java开发了一款格斗类游戏,角色可以随着通关不断升级和获取装备,运行起来似乎也很不错,但你发现角色的数据都是存储在内存中的,一旦电脑关机或者结束掉游戏后,后面再打开游戏的时候角色的数据都丢失了,不得不需要重头开始玩。
上述的问题本质上是因为数据存储在内存中,而没有持久化到硬盘中,如果能够实现这一点,那么上述的问题也就迎刃而解。
在Java中,序列化和反序列化就能够帮助我们来实现这种需求。
序列化:把对象转换为字节序列的过程称为对象的序列化
反序列化:把字节序列恢复为对象的过程称为对象的反序列化
序列化除了可以将内存中的对象持久化到硬盘上之外,主要还用于网络中的对象字节序列传输。
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
二、在代码中实现序列化和反序列化
定义POJO类
public class Hero implements Serializable {
// 角色名称
private String name;
// 角色等级
private String level;
// 攻击力
private Integer damage;
// 生命值
private Integer healthPoint;
// 最后一次停留点
private String lastPoint;
...
}
需要注意的是,需要序列化的类需要实现Serializable 接口,表明该类可以支持序列化。
(一)序列化
定义测试类
public class SerializableTest {
public static void main(String[] args) throws Exception {
// 创建角色
Hero hero = new Hero("战士","5",150,3000,"新手村");
serializeHero(hero);
}
/**
* 序列化英雄角色到硬盘中
*/
public static void serializeHero(Hero hero) throws IOException {
// 创建对象输出流,并指定输出的位置
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\MyCode\\hero.txt")));
// 将对象输出到本地文本中
oos.writeObject(hero);
System.out.println("序列化结束...");
// 关闭输出流
oos.close();
}
}
在序列化方法中,我们通过ObjectOutputStream
对象将Hero
对象持久化到硬盘中,我们可以在本地的文件夹中找到这份文件。

(二)反序列化
我们可以用ObjectOutputStream
对象将硬盘中的字节序列重新转换为内存中的对象:
public class SerializableTest {
public static void main(String[] args) throws Exception {
// 创建角色
Hero hero = deSerializeHero("D:\\MyCode\\hero.txt");
System.out.println(hero);
}
/**
* 从硬盘中读取角色数据到内存中
* @return
*/
public static Hero deSerializeHero(String filename) throws IOException, ClassNotFoundException {
// 创建对象输入流,根据指定的文件位置读取数据
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(filename)));
Hero hero = (Hero)ois.readObject();
System.out.println("反序列化结束...");
// 关闭输入流
ois.close();
return hero;
}
}
反序列化的结果如下,我们可以看到对象被成功的读取到内存中了

三、transient关键字的使用
讲到这里,我们现在已经可以实现游戏中的存档功能了,但需要存档的时候,我们只需要将角色的数据持久化到硬盘中,等下次游戏的时候再通过反序列化重新读取到内存中即可。
但这里有一个小问题,在实际的角色设计中,需要被序列化的类中可能有大量的属性,有些属性比如HP值,等级这些属性是必须要记录的,但有些属性比如角色说过的话,角色的快捷键自定义之类的信息,有些是不太重要,可以不需要持久化的。如果将这些非必要的字段排除在外的话,可以更高效地实现对象的序列化和反序列化工作。
java中为我们提供了transient
关键字来实现排除序列化的范围中的指定字段
以上面的例子为基础,我们看如何使用transient
关键字来实现Hero
对象中lastPoint
属性的忽略序列化。
步骤一:给lastPoint
属性加上transient
关键字进行修饰
public class Hero implements Serializable {
// 角色名称
private String name;
// 角色等级
private String level;
// 攻击力
private Integer damage;
// 生命值
private Integer healthPoint;
// 最后一次停留点
private transient String lastPoint;
...
}
步骤二:将对象序列化到本地后,再反序列化看对象的结果:
public static void main(String[] args) throws Exception {
// 创建角色
//Hero hero = new Hero("战士","5",150,3000,"新手村");
//serializeHero(hero);
Hero hero = deSerializeHero("D:\\MyCode\\hero.txt");
System.out.println(hero);
}

我们可以看到,此时Hero对象的lastPoint属性是空的。
四、SerialVersionUID的作用
相信在网上很多讲解Java序列化(反序列化)的文章中,都有提到关于显式地写明SerialVersionUID
的作用,要求凡是实现Serializable接口的类中最好都有一个表示序列化版本标识符的静态变量serialVersionUID
,那么这个SerialVersionUID
到底是什么呢?
我们不妨先来想一个问题:java中是如何确定反序列化获得的对象就是我们想要的对象呢?
其实它依靠的就是这个SerialVersionUID
来确定的。那这样就又有一个问题了,我们在之前的案例中,并没有在Hero
类中指定系列版本ID,为什么反序列还可以成功呢?
这是因为没有指定Customer类的serialVersionUID的,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件 多一个空格,得到的UID就会截然不同的,可以保证在这么多类中,这个编号是唯一的。
我们可以试验一下,我们先序列化对象,然后给实体类中加入weight
属性,看能不能反序列化成功。
步骤一:系列化对象(过程忽略)
步骤二:给实体类对象加入新的属性
public class Hero implements Serializable {
...
// 角色体重
private Double weight;
...
}
步骤三:执行反序列化,查看结果

我们可以看到,此时反序列化的时候代码报错了,说反序列化得到的序列话版本号和当前类的序列版本号不一致,无法反序列化成功。本质上是因为没有显式地指明序列化版本号的前提下,如果修改了类的内容,那么java自动生成的序列化版本号就会自动发生变化,而后无法和反序列化得到的对象相对应。
既然声明序列化版本号这么重要,那我们可以怎么样来进行声明呢?
常见的声明方式有两种:
(1)使用默认的serialVersionUID
生成方式
public class Hero implements Serializable {
private static final long serialVersionUID = 1L;
...
}
(2)使用IDEA中自带的serialVersionUID
生成器来生成
笔者用的是intellij idea
所以这里的话是以这款IDEA来进行序列化版本号生成方式来介绍的:
Setting -> Editor ->Inspections-> Java -> Serialization issues->Serializable class without ’serialVersionUID’



通过这两种方法中的任意一种,我们就可以实现多版本实体类之间的反序列化兼容了。
至此,本篇文章对于java中的序列和反序列化内容就讲解结束了。在实际应用中,序列化除了应用在数据持久化上,还应用在网络通信上,虽然用途不同,但实际的原理是一样的,更多细节大家可以自己再去研究一下。有疑问也欢迎留言。
网友评论