Fork me on GitHub

Java序列化—Externalizable

1. 原理

之前有一篇文章 transient关键字 ,是讲基于 Serializable 的序列化,这篇文章主要讲基于 Externalizable 的序列化。如果类实现 Serializable ,会自动序列化,但是如果实现 Externalizable ,需要我们自己手动序列化。使用很简单,只需要实现 Externalizable 接口,重写其中的 writeExternal 、readExternal 两个方法即可。看看 Externalizable 接口的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* The object implements the writeExternal method to save its contents
* by calling the methods of DataOutput for its primitive values or
* calling the writeObject method of ObjectOutput for objects, strings,
* and arrays.
*
* @serialData Overriding methods should use this tag to describe
* the data layout of this Externalizable object.
* List the sequence of element types and, if possible,
* relate the element to a public/protected field and/or
* method of this Externalizable class.
*
* @param out the stream to write the object to
* @exception IOException Includes any I/O exceptions that may occur
*/
void writeExternal(ObjectOutput out) throws IOException;

/**
* The object implements the readExternal method to restore its
* contents by calling the methods of DataInput for primitive
* types and readObject for objects, strings and arrays. The
* readExternal method must read the values in the same sequence
* and with the same types as were written by writeExternal.
*
* @param in the stream to read data from in order to restore the object
* @exception IOException if I/O errors occur
* @exception ClassNotFoundException If the class for an object being
* restored cannot be found.
*/
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

接口有两个方法,writeExternal 序列化,readExternal 反序列化。

翻译一下 writeExternal 的注释:

一个类通过实现 writeExternal 方法来保存它的内容(即序列化)。如果是基本数据类型,调用 DataOutput 的方法(有兴趣的同学可以看一下 DataOutput 接口中定义了各种基本类型的 write 方法);如果是对象、字符串、数组,则调用 ObjectOutput 的 writeObject 方法。

翻译一下 readExternal 的注释:

一个类通过实现 readExternal 方法来恢复它的内容(即反序列化)。如果是基本数据类型,调用 DataInput 的方法;如果是对象、字符串、数组,则调用 readObject 方法。这里的注释没有写清楚 readObject 是哪个类的方法,应该是 ObjectInput 的 readObject 方法。readExternal 方法读取数据时,顺序类型必须与 writeExternal 方法保持一致(这一句很重要)。

2. 入门测试

writeExternal 和 readExternal 方法底层是调用了什么方法去进行序列化以及反序列化并不是这篇文章所关心的。我们只需要知道这两个方法能帮我们实现序列化/反序列化就行。下面写个类测试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.hema.carloan;

import java.io.*;

/**
* @desc:
* @Author: zhaoxiaofa
* @Date: 2019-09-19 19:43
*/
public class ExternalizableUser implements Externalizable {

private static final long serialVersionUID = -99357512800431678L;

private String name;

private static String mobile;

private transient String password;


public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getMobile() {
return mobile;
}

public void setMobile(String mobile) {
this.mobile = mobile;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
", mobile='" + mobile + '\'' +
'}';
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeObject(mobile);
out.writeObject(password);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readObject().toString();
mobile = in.readObject().toString();
// password = in.readObject().toString();
}


}

如上述代码所示,反序列化的时候,将 password 的序列化注释掉。下面写个单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void userWriteTest() throws Exception {
ExternalizableUser user = new ExternalizableUser();
user.setName("zhaoxiaofa");
user.setMobile("13812345678");
user.setPassword("123456");
ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("/user.txt"));
o.writeObject(user);
o.close();
}

@Test
public void userReadTest() throws Exception{
ObjectInputStream in = new ObjectInputStream(new FileInputStream("/user.txt"));
ExternalizableUser object = (ExternalizableUser) in.readObject();
System.out.println("读取磁盘的对象:" + object.toString());
in.close();
}

执行结果如下:

读取磁盘的对象:User{name=’zhaoxiaofa’, password=’null‘, mobile=’13812345678’}

结果很容易想出来,因为在 readExternal 方法中并没有加上 password 。所以反序列化之后为 null 。

readExternal 方法的注释上有一句,readExternal 读取数据时,顺序类型必须与 writeExternal 方法保持一致。为什么要强调顺序?如果顺序不一致会怎样?下面来测试一下,修改 readExternal 的方法,只反序列化 mobile 。代码如下:

1
2
3
4
5
6
 @Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// name = in.readObject().toString();
mobile = in.readObject().toString();
// password = in.readObject().toString();
}

执行结果:

读取磁盘的对象:User{name=’null’, password=’null’, mobile=’zhaoxiaofa’}

从结果中我们看到,在反序列化时将 name 的值赋值给了 mobile 。name 是第一个被序列化的变量,mobile 是第一个也是唯一一个被反序列化的变量,所以把 name 的值赋值给了 mobile 。这就是 readExternal 强调的,顺序要保持一致。

3. 一定要有默认的无参构造函数

在 ExternalizableUser 类中加入如下构造方法,再执行序列化和反序列化,最后执行单元测试会报错。

1
2
3
4
public ExternalizableUser(String name, String password) {
this.name = name;
this.password = password;
}

java.io.InvalidClassException: no valid constructor

小结:

必须有权限为 public 的默认的构造器(如果有非默认的带参数的构造函数,那么必须显示的写出默认的构造函数),这样 Externalizable 才能正确执行序列化和反序列化。

本文标题:Java序列化—Externalizable

原始链接:https://zhaoxiaofa.com/2019/01/07/Java序列化—Externalizable/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。