深入理解Java 序列化(一)
PS:本文章重点从Java的序列化体系出发进行学习(后期也会持续更新,不断完善)
内容借鉴张洪亮老师的 《深入理解Java核心核心技术》 - 第十二章:序列化
目录导航
- 深入理解Java 序列化(一)
- 一、序列化和反序列化
- 1、相关接口及类
- 2、Serializable接口
- 3、Externalizable接口
- 二、什么是transient
- 三、序列化底层原理
- 1、writeObject 和 readObject 方法
- 2、为什么使用transient
- 3、为什么重写writeObject和readObject
- 4、ObjectOutPutStream
- 四、总结
一、序列化和反序列化
在Java中,我们可以通过多种方式来创建对象,只要对象没被JVM回收,我们都可以复用该对象。但是,创建出来的这些对象都是存在于JVM堆中,也就是内存当中,一旦JVM停止运行,这些对象也就随之丢失了。
在真实的应用场景中,我们需要将这些对象持久化,并且在需要时重新读取对象,例如:数据库存储、网络传输(RMI
和RPC
都需要这样的操作)。
序列化是将对象转换为可存储或传输的形式的过程,一般都是以字节码或者XML、JSON
等格式传输对象的。而将这些格式还原为对象的过程成为反序列化。
Java内置了对象序列化机制(Object Serialization),这是Java内置的一种对象持久化方式,通过对象序列化,可以把对象状态保存为字节数组,并且在有需要时将这个字节数组通过反序列化的方式再转换为对象。对象序列化/反序列化可以很容易的在JVM中的活动对象和字节数组之间进行转换。
1、相关接口及类
- java.io.Serializable
- java.io.Externalizable
- ObjectOutput
- ObjectInput
- ObjectOutPutStream
- ObjectInputStream
2、Serializable接口
类通过实现Serializable
接口以启用其序列化功能,未实现此接口的类将无法使用序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅仅用于标识可序列化的语义。
- 当视图对一个对象进行序列化时,如果遇到不支持
Serializable
接口的对象,那么将抛出NotSerializableException
。 - 如果序列化的类有父类,同时要持久化在父类中定义过的变量,那么父类也应该实现
java.io.Serializable
接口。
下面是一个实现了java.io.Serializable
接口的类
/**
* 实现Serializable接口
*
* @author baijiechong
* @since 2023/5/6 23:47
**/
public class User1 implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User1{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
通过下面的代码对User1
对象进行序列化以及反序列化
/**
* 使用java内置序列化机制实现对象的序列化以及反序列化
*
* @author baijiechong
* @since 2023/5/6 23:49
**/
public class SerializableDemo1 {
private static final String FILE_PATH = "H:\\user\\desktop\\serialization.txt";
public static void main(String[] args) {
final User1 user1 = new User1();
user1.setName("baijiechong");
user1.setAge("22");
//序列化
SerializableDemo1 demo1 = new SerializableDemo1();
demo1.serialization(user1);
//反序列化
demo1.deserialization();
}
/**
* 序列化
* @param user1 序列化的对象
*/
public void serialization(User1 user1) {
ObjectOutputStream oos = null;
try {
OutputStream outputStream = Files.newOutputStream(Paths.get(FILE_PATH));
oos = new ObjectOutputStream(outputStream);
oos.writeObject(user1);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//org.apache.tomcat.util.http.fileupload.IOUtils
IOUtils.closeQuietly(oos);
}
}
/**
* 反序列化
*/
public void deserialization() {
File file = new File(FILE_PATH);
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(Files.newInputStream(file.toPath()));
User1 user1 = (User1) ois.readObject();
System.out.println(user1);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
//org.apache.tomcat.util.http.fileupload.FileUtils
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行后的输出结果为:
User1{name='baijiechong', age='22'}
另外如果在反序列化前,也就是 demo1.deserialization();
这行代码打上断点,可以发现序列化后的字节码文件,打开文件及如下
aced 0005 7372 0022 636f 6d2e 7374 7564
792e 7365 7269 616c 697a 6162 6c65 2e6d
6f64 656c 2e55 7365 7231 5621 5096 a96b
b379 0200 024c 0003 6167 6574 0013 4c6a
6176 612f 6c61 6e67 2f49 6e74 6567 6572
3b4c 0004 6e61 6d65 7400 124c 6a61 7661
2f6c 616e 672f 5374 7269 6e67 3b78 7073
7200 116a 6176 612e 6c61 6e67 2e49 6e74
6567 6572 12e2 a0a4 f781 8738 0200 0149
0005 7661 6c75 6578 7200 106a 6176 612e
6c61 6e67 2e4e 756d 6265 7286 ac95 1d0b
94e0 8b02 0000 7870 0000 0016 7400 0b62
6169 6a69 6563 686f 6e67
3、Externalizable接口
除了Serializable
接口,Java还提供了另一个序列化接口Externalizable
。
为了了解Externalizable
接口和Serializable
接口的区别,我们把上面的代码改成使用Externalizable
接口的形式:
/**
* 实现Externalizable接口
*
* @author baijiechong
* @since 2023/5/6 23:47
**/
public class User2 implements Externalizable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//nothing to do
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
//nothing to do
}
@Override
public String toString() {
return "User2{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
通过下面的代码对User2
对象进行序列化以及反序列化
/**
* 直接对实现了Externalizable接口的类进行持久化
*
* @author baijiechong
* @since 2023/5/7 0:28
**/
public class ExternalizableDemo1 {
private static final String FILE_PATH = "H:\\user\\desktop\\serialization.txt";
public static void main(String[] args) {
User2 user2 = new User2();
user2.setName("baijiechong");
user2.setAge(23);
ExternalizableDemo1 externalizableDemo1 = new ExternalizableDemo1();
//序列化
externalizableDemo1.externalization(user2);
//反序列化
externalizableDemo1.deExternalization();
}
/**
* 序列化
*
* @param user2 序列化的对象
*/
public void externalization(User2 user2) {
ObjectOutputStream oos = null;
try {
OutputStream outputStream = Files.newOutputStream(Paths.get(FILE_PATH));
oos = new ObjectOutputStream(outputStream);
oos.writeObject(user2);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
//org.apache.tomcat.util.http.fileupload.IOUtils
IOUtils.closeQuietly(oos);
}
}
/**
* 反序列化
*/
public void deExternalization() {
File file = new File(FILE_PATH);
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(Files.newInputStream(file.toPath()));
User2 user2 = (User2) ois.readObject();
System.out.println(user2);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
IOUtils.closeQuietly(ois);
try {
FileUtils.forceDelete(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
执行后的输出结果为:
User2{name='null', age='null'}
通过上面的实例可以发现,对User2
类进行序列化以及反序列化后得到对象的所有字段值都变成了默认值。也就是说,User2对象的状态并没有被持久化,这就是Exernalizable
接口和Serializable
接口的区别:Externalizable
接口继承了Serializable
接口,该接口中定义了writeExternal()
和readExternal()
两个抽象方法。
当使用Externalizable
接口进行序列化和反序列化时,我们需要重写writeExternal()
和readExternal()
方法。
由于上面的代码在这两个方法中没有定义序列化的细节,所以输出内容为空。
还有一点值得注意:在使用Externalizable
接口进行序列化时,在读取对象时,会调用序列化类的无参构造器去创建一个新的对象,然后将保存对象的字段值分别填充到新对象中,所以实现了Externalizable
接口的类必须提供一个public
的无参构造方法。
按照要求更改User2
的代码如下:
/**
* 实现Externalizable接口
*
* @author baijiechong
* @since 2023/5/6 23:47
**/
public class User2 implements Externalizable {
private String name;
private Integer age;
//无参构造 如果User2类中没有无参构造函数,那么再运行时会抛出java.io.InvalidClassException
public User2() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = (Integer) in.readObject();
}
@Override
public String toString() {
return "User2{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
执行后的输出结果为:
User2{name='baijiechong', age='23'}
这样就把User2
对象的状态持久化下来了。
二、什么是transient
如果看过ArrayList
或者Vector
的源码,会发现它们虽然都是使用数组实现的,但是在定义数组时稍微有些不同,那就是ArrayList
使用了transient
关键字
private transient Object[] elementData; //ArrayList
protected Object[] elementData; //Vector
下面看一看transient的
关键字的作用是什么?
transient
是Java
的关键字、变量修饰符、如果用transient
声明一个实例变量,那么当对象存储时,它的值不需要维持。
这里的对象存储是指Java
的serialization
提供的一种持久化对象实例的机制,当一个对象被序列化时,transient
修饰的变量值不包括在序列化范围内,然而非transient
的变量是被包括进去的。
使用情况是:当持久化对象时,可能有一个特殊的对象数据成员,我们不想用serializable
机制来保存它,为了在一个特定对象的一个域上关闭serializable
,可以在这个域前加上关键字transient
简单的说,就是被transient修饰的成员变量,在序列化时时会被忽略,在被反序列化后,transient变量的值被设置为初始值。如int的初始值为0,对象型为null。
三、序列化底层原理
在了解了如何使用Java
中的序列化之后,我们深入分析下Java
序列化以及反序列化的原理。为了方便理解,我们围绕ArrayList
来展开介绍Java
是如何实现序列化以及反序列化的。
在介绍ArrayList
之前,我们先思考一个问题,如何自定义序列化和反序列化呢?
带着这个问题我们看下java.util.ArrayList
的源码
上面的代码中忽略了其他成员变量,ArrayList
实现了serializable
接口,其中elementData
被定义为transient
类型,而被transient
修饰的成员变量不会被序列化留下来。
我们写一个demo验证一下猜想:
public static void main(String[] args) {
String filePath = "H:\\user\\desktop\\ArrayList - serializable.txt";
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
try {
//序列化
OutputStream ops = Files.newOutputStream(Paths.get(filePath));
ObjectOutputStream oops = new ObjectOutputStream(ops);
oops.writeObject(list);
IOUtils.closeQuietly(ops);
IOUtils.closeQuietly(oops);
//反序列化
InputStream is = Files.newInputStream(Paths.get(filePath));
ObjectInputStream ois = new ObjectInputStream(is);
List<Integer> newList = (List<Integer>) ois.readObject();
System.out.println(newList);
IOUtils.closeQuietly(ops);
IOUtils.closeQuietly(oops);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
init list :[1, 2, 3]
new list :[1, 2, 3]
从结果我们可以看出,集合中的列表都正常的进行了序列化和反序列化,核心数组elemetnData
被transient
修饰的,按道理说elementData
是不会被序列化的,而且在反序列化时应该赋值为null
才对。那为什么上面的代码却把集合中的元素保留下来了呢?
1、writeObject 和 readObject 方法
在ArrayList
中定义了两个方法:writeObject()
和readObject()
,这里先给出结论:
在序列化过程中,如果被序列化的类中定义了writeObject()
和readObject()
方法,那么虚拟机会试图调用对象类中的writeObject()
和readObject()
实现进行用户自定义的序列化和反序列化。
如果没有这两个方法,则默认调用的是ObjectOutPutStream
的defaultWriteObject()
方法和ObjectInputStream
的defaultReadObject()
方法。
用户自定义的writeObject()
和readObject()
方法允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值,很灵活。
下面看看ArrayList中这两个方法的具体实现:
为什么ArrayList要用这种方式来实现序列化呢?
2、为什么使用transient
我们都知道ArrayList
实际上是动态数组,每次放入的元素达到阈值后,自动增长设置的长度值,如果数组自动增长的长度设为100
,而实际之存放1
个元素,那么就会序列化99个null
元素,为了保证不对null
元素进行序列化,所以ArrayList
把元素数组elementData
设置为transient
。
3、为什么重写writeObject和readObject
前面说过,为了防止包含大量空对象的数组被序列化,以及优化存储、速度,ArrayList
使用transient
来修饰elementData
。
但是,作为一个集合,在序列化过程中还必须保证其中不为null
的元素可以被持久化下来,所以通过重写writeObject()
和readObject()
方法的方法把其中的元素保留下来。
-
writeObject()
方法把elementData
数组中的元素遍历保存到输出流(ObjectOutPutStream)
中。 -
readObject()
方法从输入流(ObjectInputStream)
中读取出对象并保存赋值到elementDtat
中从而还原数组的状态。
至此,我们回答上面的问题:如何自定义序列化和反序列化的策略。
答:可以在被序列化的类中重写writeObject()和readObject()方法
问题又来了:
虽然ArrayList中写了writeObject()方法和readObject()方法,但是这两个方法并没有被显式的调用,那readObject和writeObject是何时被调用的呢?
4、ObjectOutPutStream
对象的序列化过程是通过ObjectOutPutStream
和ObjectInputStream
实现的,带着刚才的问题,我们分析一下ArrayList
中的writeObject()
和readObject()
方法到底是如何被调用的。
这里直接给出ObjectOutPutStream
的writeObject()
方法的调用栈:
wriiteObject() ---> writeObject0() ---> writeOrdinaryObject() ---> writeSerialData() ---> invokeWriteObject()
其中writeObjectMethod.invoke(obj,new Object[]{ out })
是关键,通过反射的方式调用writeObjectMethod()
方法。
官方的解释是这样的:
class-defined writeObject method , or null if none // 类定义的writeObject方法,如果没有则为null
所以说,这个方法就是ArrayList
中定义的writeObject()
方法,然后通过反射的方式被调用了。
至此,我们回答刚提出的问题:
如果一个类中包含writeObject和readObject方法,那么这两个方法是怎么被调用的呢?
答:在使用ObjectOutPutStream
的writeObject()
和ObjectInputStream
的readObject()
方法时,会通过反射的方式调用。
有的小伙伴可能会有疑问?
Serializable明明就是一个空接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化呢?
Serializable
接口的定义如下:
public interface Serializable {
}
当尝试对一个为实现Serializable
或者Externalizable
接口的对象进行序列化时,会抛出java.io.NotSerializableException
异常。
其实这个问题也很简单,我们再回到刚刚writeObject()
方法的调用栈:
wriiteObject() ---> writeObject0() ---> writeOrdinaryObject() ---> writeSerialData() ---> invokeWriteObject()
writeObject()
方法中有如下一段代码:
在进行序列化时,会判断对象是否为String、Array、Enum
和Serializable
类型,如果都不是则直接抛出NotSerializableException
异常。
四、总结
- 如果一个类想被序列化,则必须实现
Serializable
接口,否则将抛出NotSerializableException
异常,这是因为在序列化操作过程中会对类的类型进行检查,要求被序列化的类必须属于String、Array、Enum
和Serializable
中的一种。 - 在变量声明前加上关键字
transient
,可以阻止该变量被序列化到文件中,同时也可以参考ArrayList
为了不序列化无意义对象的做法,就是transient + 自定义writeObject()
。 - 在类中增加
writeObject()
和readObject()
方法可以实现自定义序列化和反序列化的策略。