Java开发时,有时需要实现序列化和反序列化操作。这里记录下序列化与反序列化的使用总结。
定义
序列化是将Java对象转换为字节序列的过程。在序列化过程中,Java对象被转换为一个字节流。
反序列化是将字节序列转换回Java对象的过程。在反序列化过程中,字节序列被读取并转换回原始的Java对象。
常见使用场景
在梳理序列化与反序列化的应用场景前,需要明确的一点是,序列化和反序列化针对的都是对象,不是类。以下两个场景是序列化与反序列化常用的场景:
(1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中,也即持久化对象;
(2) 在网络上传送对象的字节序列,也即网络传输对象;
此外,有时为提供访问性能,会将对象存放到缓存中。
使用示例
在Java中,如果一个对象要想实现序列化,必须要实现 Serializable 接口或 Externalizable 接口。Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类采用默认的序列化方式。
序列化和反序列化的三方件很多,常用的有阿里巴巴的fastjson、Spring默认的序列化组件 jackson等。基于fastjson或jackson的实现,还请自行学习使用。这里使用JDK自带的ObjectOutputStream和ObjectInputStream为例来实现,其中:
ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
接下来举例说明两个接口的实现。
基于 Serializable 接口实现序列化
基于 Serializable 接口实现序列化是实现序列化的常用方式,必须掌握。接下来介绍下使用示例。
// (1) 定义实体并实现Serializable接口
public class User implements Serializable {
// 通过IDEA等编辑器自动生成serialVersionUID
private static final long serialVersionUID = 5355682299368213966L;
private String username;
private transient String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
// (2) 序列化和反序列化上述实体类
public class TransientDemo {
public void testTransientByStream() throws IOException, ClassNotFoundException {
User user = new User();
user.setUsername("foo");
user.setPassword("123456789");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(user); // 序列化
oos.close();
System.out.println(bos.toByteArray());
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
User deserializedUser = (User) ois.readObject(); // 反序列化
ois.close();
System.out.println(deserializedUser.getUsername()); // 输出 "foo"
System.out.println(deserializedUser.getPassword()); // 输出 null,因为 password 属性没有被序列化
}
}
在基于 Serializable 接口实现序列化时,如无特殊说明,均需显式地定义serialVersionUID。顾名思义,serialVersionUID就是实体类的版本号。一般情况下,该值一旦设置后,均不需要调整,以保证软件版本的兼容性。需要说明的是,如果没有显式定义serialVersionUID,Java编辑器会自动生成serialVersionUID,但是还是推荐显式定义serialVersionUID。这是因为serialVersionUID的取值是Java运行时环境根据类的内部细节自动生成的。如果对类的源代码作了修改,再重新编译,新生成的类文件的serialVersionUID的取值有可能也会发生变化。此外,serialVersionUID的默认值完全依赖于Java编译器的实现,对于同一个类,用不同的Java编译器编译,有可能会导致不同的 serialVersionUID。为了提高serialVersionUID的独立性和确定性,强烈建议在一个可序列化类中显式的定义serialVersionUID并为其赋值。
此外,对于不期望序列化的字段,可以使用transient关键字修饰,更多transient关键字的使用,可以参考transient关键字使用说明一文。主流的序列化组件(如fastjson、jackson等)均支持基于 Serializable 接口实现序列化。
基于 Externalizable 接口实现序列化
Externalizable接口是Serializable接口的子类,开发者需要自行实现writeExternal()和readExternal()方法,用来决定哪些字段需要进行序列化和反序列化。注意,transient关键字在该场景下不再生效。
此外,需要说明的是,对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor的异常。因此Externalizable对象必须有默认构造函数,而且必须是public的。
// (1) 定义实体并实现Serializable接口
public class Person implements Externalizable {
private transient String name;
private int age;
// 必须定义访问权限是public的无参构造函数
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
// 必须实现writeExternal接口
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
// 必须实现readExternal接口
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
// (2) 序列化和反序列化上述实体类
public class TransientDemo {
public void testExternalWithTransient() throws IOException, ClassNotFoundException {
Person person = new Person("jack", 25);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(person);
oos.close();
System.out.println(bos.toByteArray());
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Person deserializedPerson = (Person) ois.readObject();
ois.close();
System.out.println(deserializedPerson.getName()); // 输出 "foo",即使已经使用transient关键字修饰了字段,
System.out.println(deserializedPerson.getAge()); // 输出 25
}
}
优缺点分析
Java序列化和反序列化的优点是它们提供了一种在不同平台之间传输和共享Java对象的方法。但是,Java序列化和反序列化的缺点是它们可能会破坏对象的封装性,因为序列化操作会将对象的所有字段都暴露给外部。在使用Java序列化和反序列化时,需要注意以下几点:
(1) 优先选择基于Serializable接口实现序列化:相比基于 Externalizable 接口实现序列化,主流序列化框架均支持基于Serializable接口的序列化。
(2) 在基于 Serializable 接口实现序列化时,需显式地定义serialVersionUID。serialVersionUID是实体类的版本号,用来实现版本控制。为了保证兼容性,强烈建议在一个可序列化类中显式的定义serialVersionUID并为其赋值。
(3) 对于不期望序列化的字段,可以使用transient关键字修饰。transient关键字可以用来标记某个字段不需要被序列化。如果一个对象包含一个使用了transient关键字的字段,那么该字段将无法序列化。
(4) 安全性问题:Java序列化和反序列化可能会暴露关键信息,因此需要确保安全性。对于一些关键信息,如果需要在网络间传输(以非安全协议传输时),需要使用加密来保护序列化的数据。
参考
https://www.zhihu.com/tardis/bd/ans/672095170?source_id=1001 java中什么是序列化和反序列化?
https://blog.csdn.net/qq_44543508/article/details/103232007 java中的transient关键字详解
https://www.baidu.com/ 百度AI搜索
https://www.cnblogs.com/lqmblog/p/8530108.html 序列化和反序列化的简单理解
https://www.cnblogs.com/huhx/p/5303024.html java基础---->Serializable的使用
https://www.cnblogs.com/huhx/p/sSerializableTheory.html java高级---->Serializable的过程分析