1.简介
今天来学习一下Java序列化,相对来说:比较简单,初步了解为主;
2.背景介绍
2.1 什么是Java序列化
序列化,就是把对象改成二进制的过程,可以保存到磁盘或者网络发送; 反序列化:就是把二进制转换为对象的过程;
2.2 序列化应用场景
- 变成二进制,就可以把数据进行网络传输(json也是序列化协议的一种)
- 如果,内存中的数据不够用了,可以通过序列化先放到磁盘中进行保存
- 加密,如果传输的数据需要进行加密,序列化后,进行加密;接受数据后,解密;然后反序列化拿到数据;
3.代码演示
先来一个实现了Serializable接口的Student对象
3.1 基础功能实现
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
private int id;
private String name;
}
实现序列化
public class ObjectOutputStreamTest01 {
public static void main(String[] args) throws Exception {
// 1.创建java对象
Student s = new Student(1111, "zhangsan");
// 2.序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));
// 3.序列化对象
oos.writeObject(s);
// 4.刷新
oos.flush();
// 5.关闭
oos.close();
}
}
这个时候,在我的项目中,就出现了一个序列化完毕后的students文件
然后,我们想要读取这个文件,代码如下:
public class ObjectInputSteeamTest01 {
public static void main(String[] args) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));
// 开始反序列化,读
Object obj = ois.readObject();
// 反序列化回来是一个学生对象,所以会调用学生对象的toString方法
System.out.println(obj);
ois.close();
}
}
结果如下:
反序列化成功;
3.2 多个对象一起序列化
现在,有一个这样的需求,就是要把多个对象一起进行序列化;如果,调用两次oos.writeObject(s);是不行的;正确解决方法把对象放到list中,然后,序列化List;
User对象代码: 注意,User也需要实现Serializable接口
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private int id;
private String name;
}
序列化代码:
public class ObjectOutputStreamTest02 {
public static void main(String[] args) throws Exception{
ArrayList<User> userList = new ArrayList<>();
userList.add(new User(111,"zhangsan"));
userList.add(new User(222,"lisi"));
userList.add(new User(333,"wanwu"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users"));
// 序列化集合,集合对象中放了很多对象
oos.writeObject(userList);
}
}
反序列化代码:
public class ObjectInputSteeamTest02 {
public static void main(String[] args) throws Exception{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users"));
// 开始反序列化,读
List<User> obj = (List<User>)ois.readObject();
for (User user: obj
) {
System.out.println(user);
}
System.out.println(obj);
ois.close();
}
}
经过测试成功;
3.3 希望某些变量不序列化
如果,我们希望某些变量不序列化的话,就可以使用transient 来修饰该变量;
把Student类中name变量用transient来进行修饰,然后,重新进行序列化,再进行反序列化;(代码和3.1一样)结果如下:
Student(id=1111, name=null)
证明,在序列化的时候,没有把name的属性传入;
4.序列化版本号的作用
首先,Serializable 是一个标志接口,所谓的标志接口,就是让虚拟机知道;Java虚拟机看到这个接口后,如果,我们没有提供序列化版本号的话,会为该类自动生成一个序列化版本号。
如果,我们没有定义序列化版本号;该类发生了代码的修改;此时,之前的序列化文件就不能被反序列化了;因为,源代码修改后,需要重新编译,生成全新的字节码文件;并且,class文件再次运行的时候,Java虚拟机机会重新生成一个序列化版本号;这就导致之前的序列化版本号和现在版本号是不同的,所以,无法被反序列化;
注意:Java语言中,针对实现了Serializable接口的类,采用如下机制来区分类:
- 首先,通过类名进行对比,如果,类名不一样,肯定不是同一个类;
- 如果,类名一样,在判断序列化版本号是否一样
4.1 这种自动生成版本号有什么缺陷?
一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类(这样就不好了)
小结论:凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号;这样,以后这个类即使代码修改了,但是版本号不变,java虚拟机会认为是同一个类;
例如:我们的HashMap ArrayList 肯定随着版本的变化是会发生修改的,如果,修改后,之前的序列化文件不能被反序列化回来,就麻烦了 所以,一般JDK提供的类 如String、HashMap、ArrayList 都是可以序列化的。并且,序列化的ID都是固定的,这样,这算发生了版本升级,也不会导致之前数据无法被反序列化的问题;
测试如下:给Student类添加序列化ID,生成序列化文件;然后,对其增减字段看看能否进行反序列化;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = -6849794470754667733L;
private int id;
private transient String name;
}
序列化完毕后,对其字段进行修改,结果如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = -6849794470754667733L;
private int id;
// private transient String name;
private transient String phone;
}
然后,进行反序列化,结果如下: 符合预期;
Student(id=1111, phone=null)
5.Externalizable
如果,我们不想序列化的字段太多了,如果,每个字段都加transient关键字的话,会比较麻烦,为了解决这个问题,我们可以通过实现Externalizable接口来进行解决;
Externalizable继承Serializable ;对其使用时,只要声明一个空的构造方法,覆写writeExternal和readExternal方法即可;
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
5.1 测试
Externalizable实现类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person implements Externalizable {
private static final long serialVersionUID = -23537246523447182L;
private int age;
private String name;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(age);
out.writeObject(name);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
age = in.readInt();
Object o = in.readObject();
if (o instanceof String) {
name = (String)o;
}
}
}
测试类:
public class ExternalizableTest {
public static void main(String[] args) throws Exception{
// 1. 创建对象
Person p = new Person(11, "小卢");
// 2.序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));
// 3.序列化对象
oos.writeObject(p);
// 4.刷新
oos.flush();
// 5.关闭
oos.close();
// 开始反序列化,读
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));
Object obj = ois.readObject();
System.out.println(obj);
}
}
结果如下:
Person(age=11, name=小卢)
使用Externalizable注意点:
- 使用Externalizable序列化,必须要声明一个无参构造,否则会报no valid constructor的异常
- 使用Externalizable序列化,相当于所有的成员变量都添加了transient关键字,如果想要序列化或者反序列化哪些字段需要自己重写writeExternal和readExternal方法。
- 使用Externalizable序列化,transient关键字可以用,但没有任何效果。
6.其他的一些注意点
6.1 对象序列化后再次序列化
对一个对象序列化之后,为了节省空间,只会写入一次。再写入时,后面追加的是该对象的引用;
还是,以刚刚的例子:
public static void main(String[] args) throws Exception{
// 1. 创建对象
Person p = new Person(11, "小卢");
// 2.序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person"));
// 3.序列化对象
oos.writeObject(p);
p.setAge(22);
p.setName("小陈");
// 序列化失败
oos.writeObject(p);
// 4.刷新
oos.flush();
// 5.关闭
oos.close();
// 开始反序列化,读
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person"));
Object p1 = ois.readObject();
Object p2 = ois.readObject();
System.out.println(p1);
System.out.println(p2);
}
经过测试结果为:
Person(age=11, name=小卢)
Person(age=11, name=小卢)
解决方案:
- 修改对象变量后,不使用oos.writeObject(person);使用oos.writeUnshared(person);(不管,这个对象之前是否被序列化过,都会当作新出现的对象执行序列化)
- 修改对象变量后,仍然使用oos.writeObject(person);但在这之前要调用oos.reset();重置流。 输出结果:
Person(age=11, name=小卢)
Person(age=22, name=小陈)
6.2 子类实现Serilizable 接口 父类却为实现
如果,父类实现Serilizable接口,那么,子类肯定也是实现了它。所以也可以序列化;如果是子类实现了Serializable接口,而父类却没有,就会报错;所以,需要让父类实现该接口,或者,在父类中声明一个无参构造;
6.3 修改成员变量
我们之前测试,增加成员变量的值,或者删除成员变量的值,只要有序列化号,则可以进行序列化或者反序列化;那么,对原来成员变量的类型进行修改,那反序列化就会报错;(其实理解一下,就可以明白)
所以,我们在序列化类成员变量类型进行修改以后,也需要对serialVersionUID进行修改;
6.4 序列化会破坏单例模式
/**
* @auther:lgb
* @Date: 2021/5/12
*/
public class PersonUtils implements Serializable {
private static PersonUtils mInstance;
private PersonUtils() {
}
public static PersonUtils getInstance() {
if (mInstance == null) {
synchronized (PersonUtils.class) {
if (mInstance == null) {
mInstance = new PersonUtils();
}
}
}
return mInstance;
}
public static void main(String[] args) throws Exception{
PersonUtils instance = PersonUtils.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("single"));
// 3.序列化对象
oos.writeObject(instance);
// 4.刷新
oos.flush();
// 5.关闭
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("single"));
// 开始反序列化,读
Object obj = ois.readObject();
// 反序列化回来是一个学生对象,所以会调用学生对象的toString方法
System.out.println("单例获取到对象:" + instance.hashCode());
System.out.println("实例化获取到的对象:" + obj.hashCode());
ois.close();
}
}
注意:反射也会破坏单例,因为反射都可以拿到和修改一个类的所有信息;