每一秒钟的时间都值得铭记

0%

一篇博客教会你写序列化工具

什么是序列化?

总所周知,在Java语言中,所有的数据都是以对象的形式存在Java堆中。

但是Java对象如果要存储在别的地方,那么单纯的Java对象就无法满足了,必须要将Java对象转为一种可以存储的格式,这个转换的过程就是序列化。

同理而言,将一种存储的格式转换为Java对象的过程,就是反序列化。

序列化格式

序列化是一种通用的称呼,对于序列化之后转成的数据格式并没有硬性的要求,但是一般都会将格式定为字节数组,或者字符串之类通用的数据格式。

例如在Java中存在这样一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class User implements Serializable {

private int id;
private String username;
private String password;
private float money;

public User() {
}

public User(int id, String username, String password, float money) {
this.id = id;
this.username = username;
this.password = password;
this.money = money;
}

// get、set、toString等方法省略……

使用JDK自带的序列化工具,可以将这样一个Java对象转为字节数组:

1
2
3
4
5
6
7
8
User user = new User(1, "次时代小羊", "222222", 99.44F);

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(user);
byte[] bytes = bos.toByteArray();
System.out.println("length:" + bytes.length);
System.out.println("bytes:" + Arrays.toString(bytes));

Java对象序列化后得到的字节数组可以写入数据库,或者文件系统中,以后如果需要使用这个Java对象,可以从数据库或者文件中将字节数组读取出来,重新反序列化为Java对象:

1
2
3
4
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
Object object = ois.readObject();
System.out.println("object:" + object);

但是JDK自带的序列化工具虽然可以将Java对象转换为字节数组,但是这个字节数组是完全按照Java的格式来序列化的,反序列化也需要使用JDK的工具才行。

所以这种序列化方式只能在Java语言中通用。

(虽然理论上别的语言平台也可以按照JDK反序列化的方式实现这个工具,但是别人可不会惯着你!)

即便是在Java平台内部,这种序列化方式也非常笨重,因为在这个序列化得到的字节数组中序列化了非常多的与用户数据无关的对象数据。

例如上述的user对象中,用户真正关心的数据只有四个,分别是idusernamepasswordmoney,至于Java对象内部是一些数据,并不是用户真正关心的。

这个时候,我们就追求一种简洁明了,而且跨平台通用的序列化格式。

JSON序列化

在Java的早起,XML作为一种可扩展标记语言,因为它的平台无关性、可扩展性、数据遵循严格的格式,人类可读等优点,得到了Java开发者的青睐。

早期XML在Java中大行其道,很多Java对象最终都会被序列化为XML文本存储或者转发。

因为其具有平台无关性,很多语言平台或第三方库也纷纷实现了XML的标准。

不过伴随着JSON格式的数据的崛起,JSON很快就取代了XML的地位,XML具有的优点JSON都具有,而且比XML更加简洁,文本更小。

使用第三方类库Jackson,将一个Java对象序列化为字符串或者字节数组:

1
2
3
4
5
6
7
8
9
10
User user = new User(1, "次时代小羊", "222222", 99.44F);

ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes1 = objectMapper.writeValueAsBytes(user);
System.out.println("length:" + bytes1.length);
System.out.println("bytes:" + Arrays.toString(bytes1));

String json = objectMapper.writeValueAsString(user);
System.out.println("length:" + json.length());
System.out.println("json:" + json);

Jackson序列化得到的字符串或者字节数组,同样可以存储到数据库,或者通过网络转发出去,并被支持JSON格式的语言平台解析。

Jackson序列化为字符串和字节数组本质上并没有区别,序列化为字节数组,其实就是将序列化得到的字符串转为字节数组。

1
2
3
4
User user1 = objectMapper.readValue(bytes1, User.class);
System.out.println("user1:" + user1);
User user2 = objectMapper.readValue(json, User.class);
System.out.println("user2:" + user2);

JSON序列化是目前一种比较理想的序列化方式,各种语言平台,甚至是数据库都对JSON格式的数据有支持。

精简序列化数据

我们先来看一下使用Jackson序列化得到的字符串和字节数组数据:

1
{"id":1,"username":"次时代小羊","password":"222222","money":99.44}
123 34 105 100 34 58 49 44 34 117
{ " i d " : 1 , " u
115 101 114 110 97 109 101 34 58 34
s e r n a m e " : "
-26 -84 -95 -26 -105 -74 -28 -69 -93 -27
-113 -84 -25 -66 -118 34 44 34 112 97
" , " p a
115 115 119 111 114 100 34 58 34 50
s s w o r d " : " 2
50 50 50 50 50 34 44 34 109 111
2 2 2 2 2 " , " m o
110 101 121 34 58 57 57 46 52 52
n e y " : 9 9 . 4 4
125
}

以上就是使用Jackson序列化得到的字符串,以及字节数组和字符串字符的对应表,其中中文字符使用三个字节表示。

按照上面的对照表,我们可以知道序列化为字节数组的时候,JSON格式的字符串都序列化了哪些内容。

而我们前面也说过,用户真正关心的数据只有四个,分别是idusernamepasswordmoney,而这四个数据的名称(字段名)对于数据本身而言,只是做一个定位的作用。

如果我们可以预先确定序列化数据的字段顺序,而后反序列化的时候也已同样的顺序进行解析,是否就能够抛弃JSON格式中的字段名称,只将数据本身进行序列化?

比如将JSON格式的字符串缩减成下面的形式:

1
1次时代小羊22222299.44

但是这也有一个问题,那就是我们无法确定每个数据的长度,比如username这个字段,它对应的值到底是次时代小羊,还是次时代小羊222222,甚至可能还是次时代小羊22222299.44

所以为了确定数据的长度,我们还必须加入数据的长度作为表示,因为数据的长度都可以使用整形类型的数据进行表示。

比如我们可以约定,字符开始的第一个小于等于9的数字为数据长度,我们这样就可以很清晰的定位并分隔数据。

1
115次时代小羊6222222599.44

当然,这只是字符串可以这样表示,如果使用字节数组,那么我们可以根据单个数据的最大字节数,约定byte或者int类型的数据来表示长度。

byte支持单个数据的字节数组长度为255(2 ^ 8-1),int支持单个数据的字节数组长度为4294967295(2 ^ 32-1),因为数据长度只可能为正整数,所以使用无符号数可以最大程度支持。

而且一些特定类型的数据长度我们可以不需要确定,一些语言平台已经规定了这些数据类型的字节长度,比如在Java语言中,intfloat类型的数据长度为4,那么我们只需要规定一些不确定的数据的字节长度即可,比如字符串类型,字节数组类型等等。

我们可以重新设计简化格式:

长度 数据 类型 是否需要确定数据长度 说明
4 1 int 语言平台规定,不需要 int类型的数据字节长度为4
15 次时代小羊 字符串 需要 UTF-8编码下一个中文字节长度为3(或者4)
6 222222 字符串 需要 UTF-8编码兼容ASCII编码,所以长度为6
4 99.44 float 语言平台规定,不需要 float类型的数据字节长度为4

数据总长度为29,加上一共四个数据,每个数据对应的字节数组长度各占一个int类型数据的字节长度,所以最终长度为37(29+4+4)。

最终得到序列化后的字节数组:

1 0 0 0 15 0 0 0
1 15
-26 -84 -95 -26 -105 -74 -28 -69
-93 -27 -80 -113 -25 -66 -118 6
6
0 0 0 50 50 50 50 50
2 2 2 2 2
50 72 -31 -58 66
2 99.44

得到序列化后的字节数组之后,反序列化只需要按照原定的顺序,即可正确读取数据。

比如:

  • 1、读取int类型的字段id数据,得到数据值:1

  • 2、读取字符串类型的字段username数据对应的字节数组长度,得到数据值:15

    • 2.1、向后读取长度为15的字节数组,得到数据值:次时代小羊
  • 3、读取字符串类型的字段password数据对应的字节数组长度,得到数据值:6

    • 3.1、向后读取长度为6的字节数组,得到数据值:222222
  • 4、读取float类型的字段money数据,得到数据值:99.44

至此,精简序列化数据的方式都可以正确序列化和反序列化,而且序列化得到的字节数组长度更小。

Google的ProtoBuf和开源的MessagePack其实都是使用了类似的精简序列化的方式,不过这些开源的序列化框架更加成熟可靠,内部的实现细节也更加全面。

总结

以上三种序列化方式,各有优点,也各有缺点,我们在这里总结一下:

序列化方式 JDK序列化 JSON序列化 精简序列化
序列化结果 字节数组 字符串或者字节数组 字节数组
是否支持跨平台 不支持 支持 支持
是否需要额外约定 不需要 不需要 需要
人类可读性 优秀
优点 JDK自带,无需第三方依赖,对Java语言开发者友好 全平台通用,序列化结果简洁工整,人类可读性强 全平台通用,序列化结果精简
缺点 只支持JDK平台,序列化结果笨重 一些语言平台不支持JSON格式,需要第三方库 扩展性较差,在需要改动序列化对象的时候,序列化和反序列化方式也需要同时改动

以上三种序列化方式的优缺点已经一一列名,我们可以根据自身需要进行选择。

如果你进行的是一些通信软件、游戏等等对网络性能要求高,且通信格式并不会发生重大改变的开发工作,那么可以考虑选择第三种精简序列化的方式,开源平台上也有很多这种类型的序列化框架的实现,比如前面提到过的ProtoBufMessagePack等等。

如果你进行是一些Web网站等一些扩展性要求较高的开发工作,那么建议选择JSON序列化的方式,即便是一些不支持JSON格式的语言平台,同样有很多优秀的第三方库对其进行了支持,比如Jackson等等。

至于JDK序列化的方式,如果你有兴趣,或者开发的项目本身不支持其他序列化方式,那么也是一个不错的选择~

源码

在文章的最后,我在这里附上一个本人使用Java写的简单的序列化工具,有兴趣的同学可以参考一下。

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
public class Bytes {

private static final int ONE_LENGTH = 1;
private static final int TWO_LENGTH = ONE_LENGTH << 1;
private static final int FOUR_LENGTH = ONE_LENGTH << 2;
private static final int EIGHT_LENGTH = ONE_LENGTH << 3;

private static final byte BOOLEAN_TRUE = 1;
private static final byte BOOLEAN_FALSE = 0;

private byte[] data;
private int readIndex;
private int writeIndex;

public Bytes() {
this.readIndex = 0;
this.writeIndex = 0;
}

public Bytes(byte[] bytes) {
this.data = bytes;
this.readIndex = 0;
this.writeIndex = bytes.length;
}

public byte[] getData() {
return data;
}

public byte readByte() {
byte value = data[readIndex];
readIndex += ONE_LENGTH;
return value;
}

public short readShort() {
short value = 0;
for (int i = 0; i < TWO_LENGTH; i++) {
value = (short) (value | (data[i + readIndex] & 0xFF) << i * 8);
}
readIndex += TWO_LENGTH;
return value;
}

public int readInt() {
int value = 0;
for (int i = 0; i < FOUR_LENGTH; i++) {
value = value | (data[i + readIndex] & 0xFF) << i * 8;
}
readIndex += FOUR_LENGTH;
return value;
}

public long readLong() {
long value = 0;
for (int i = 0; i < EIGHT_LENGTH; i++) {
value = value | (data[i + readIndex] & 0xFFL) << i * 8;
}
readIndex += EIGHT_LENGTH;
return value;
}

public boolean readBoolean() {
byte value = data[readIndex];
readIndex += ONE_LENGTH;
return value == BOOLEAN_TRUE;
}

public char readChar() {
char value = 0;
for (int i = 0; i < TWO_LENGTH; i++) {
value += data[i + readIndex] << i * 8;
}
readIndex += TWO_LENGTH;
return value;
}

public float readFloat() {
int intValue = readInt();
return Float.intBitsToFloat(intValue);
}

public double readDouble() {
long longValue = readLong();
return Double.longBitsToDouble(longValue);
}

public byte[] readBytes(int length) {
byte[] tempBytes = new byte[length];
System.arraycopy(data, readIndex, tempBytes, 0, length);
readIndex += length;
return tempBytes;
}

public void writeByte(byte value) {
expansion(ONE_LENGTH);
data[writeIndex] = value;
writeIndex += ONE_LENGTH;
}

public void writeShort(short value) {
expansion(TWO_LENGTH);
for (int i = 0; i < TWO_LENGTH; i++) {
data[i + writeIndex] = (byte) (value >> i * 8);
}
writeIndex += TWO_LENGTH;
}

public void writeInt(int value) {
expansion(FOUR_LENGTH);
for (int i = 0; i < FOUR_LENGTH; i++) {
data[i + writeIndex] = (byte) (value >> i * 8);
}
writeIndex += FOUR_LENGTH;
}

public void writeLong(long value) {
expansion(EIGHT_LENGTH);
for (int i = 0; i < EIGHT_LENGTH; i++) {
data[i + writeIndex] = (byte) (value >> i * 8);
}
writeIndex += EIGHT_LENGTH;
}

public void writeBoolean(boolean value) {
expansion(ONE_LENGTH);
data[writeIndex] = value ? BOOLEAN_TRUE : BOOLEAN_FALSE;
writeIndex += ONE_LENGTH;
}

public void writeChar(char value) {
expansion(TWO_LENGTH);
for (int i = 0; i < TWO_LENGTH; i++) {
data[i + writeIndex] = (byte) (value >> i * 8);
}
writeIndex += TWO_LENGTH;
}

public void writeFloat(float value) {
int intValue = Float.floatToIntBits(value);
writeInt(intValue);
}

public void writeDouble(double value) {
long longValue = Double.doubleToLongBits(value);
writeLong(longValue);
}

public void writeBytes(byte[] bytes) {
expansion(bytes.length);
System.arraycopy(bytes, 0, data, writeIndex, bytes.length);
writeIndex += bytes.length;
}

private void expansion(int length) {
if (this.data == null) {
data = new byte[length];
} else {
byte[] tempBytes = new byte[data.length + length];
System.arraycopy(data, 0, tempBytes, 0, data.length);
data = tempBytes;
}
}

}

再附带上一份序列化的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
User user = new User(1, "次时代小羊", "222222", 99.44F);

Bytes bytes3 = new Bytes();
bytes3.writeInt(user.getId());
byte[] usernameByte = user.getUsername().getBytes(StandardCharsets.UTF_8);
bytes3.writeInt(usernameByte.length);
bytes3.writeBytes(usernameByte);
byte[] passwordByte = user.getPassword().getBytes(StandardCharsets.UTF_8);
bytes3.writeInt(passwordByte.length);
bytes3.writeBytes(passwordByte);
bytes3.writeFloat(user.getMoney());
System.out.println("length:" + bytes3.getData().length);
System.out.println("bytes:" + Arrays.toString(bytes3.getData()));

User user3 = new User();
user3.setId(bytes3.readInt());
int usernameBytesLength = bytes3.readInt();
user3.setUsername(new String(bytes3.readBytes(usernameBytesLength)));
int passwordBytesLength = bytes3.readInt();
user3.setPassword(new String(bytes3.readBytes(passwordBytesLength)));
user3.setMoney(bytes3.readFloat());
System.out.println("user3:" + user3);

最后的最后,瑞思拜~

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------这是我的底线^_^-------------