一篇博客教会你写序列化工具
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
什么是序列化
总所周知在Java语言中所有的数据都是以对象的形式存在Java堆中。
但是Java对象如果要存储在别的地方那么单纯的Java对象就无法满足了必须要将Java对象转为一种可以存储的格式这个转换的过程就是序列化。
同理而言将一种存储的格式转换为Java对象的过程就是反序列化。
序列化格式
序列化是一种通用的称呼对于序列化之后转成的数据格式并没有硬性的要求但是一般都会将格式定为字节数组或者字符串之类通用的数据格式。
例如在Java中存在这样一个类
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对象转为字节数组
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对象
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
对象中用户真正关心的数据只有四个分别是id
、username
、password
、money
至于Java对象内部是一些数据并不是用户真正关心的。
这个时候我们就追求一种简洁明了而且跨平台通用的序列化格式。
JSON序列化
在Java的早起XML作为一种可扩展标记语言因为它的平台无关性、可扩展性、数据遵循严格的格式人类可读等优点得到了Java开发者的青睐。
早期XML在Java中大行其道很多Java对象最终都会被序列化为XML文本存储或者转发。
因为其具有平台无关性很多语言平台或第三方库也纷纷实现了XML的标准。
不过伴随着JSON格式的数据的崛起JSON很快就取代了XML的地位XML具有的优点JSON都具有而且比XML更加简洁文本更小。
使用第三方类库Jackson将一个Java对象序列化为字符串或者字节数组
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序列化为字符串和字节数组本质上并没有区别序列化为字节数组其实就是将序列化得到的字符串转为字节数组。
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序列化得到的字符串和字节数组数据
{"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格式的字符串都序列化了哪些内容。
而我们前面也说过用户真正关心的数据只有四个分别是id
、username
、password
、money
而这四个数据的名称字段名对于数据本身而言只是做一个定位的作用。
如果我们可以预先确定序列化数据的字段顺序而后反序列化的时候也已同样的顺序进行解析是否就能够抛弃JSON格式中的字段名称只将数据本身进行序列化
比如将JSON格式的字符串缩减成下面的形式
1次时代小羊22222299.44
但是这也有一个问题那就是我们无法确定每个数据的长度比如username
这个字段它对应的值到底是次时代小羊
还是次时代小羊222222
甚至可能还是次时代小羊22222299.44
所以为了确定数据的长度我们还必须加入数据的长度作为表示因为数据的长度都可以使用整形类型的数据进行表示。
比如我们可以约定字符开始的第一个小于等于9的数字为数据长度我们这样就可以很清晰的定位并分隔数据。
115次时代小羊6222222599.44
当然这只是字符串可以这样表示如果使用字节数组那么我们可以根据单个数据的最大字节数约定byte
或者int
类型的数据来表示长度。
byte
支持单个数据的字节数组长度为2552 ^ 8-1int
支持单个数据的字节数组长度为42949672952 ^ 32-1因为数据长度只可能为正整数所以使用无符号数可以最大程度支持。
而且一些特定类型的数据长度我们可以不需要确定一些语言平台已经规定了这些数据类型的字节长度比如在Java语言中int
、float
类型的数据长度为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
类型数据的字节长度所以最终长度为3729+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格式需要第三方库 | 扩展性较差在需要改动序列化对象的时候序列化和反序列化方式也需要同时改动 |
以上三种序列化方式的优缺点已经一一列名我们可以根据自身需要进行选择。
如果你进行的是一些通信软件、游戏等等对网络性能要求高且通信格式并不会发生重大改变的开发工作那么可以考虑选择第三种精简序列化的方式开源平台上也有很多这种类型的序列化框架的实现比如前面提到过的ProtoBuf
和MessagePack
等等。
如果你进行是一些Web网站等一些扩展性要求较高的开发工作那么建议选择JSON序列化的方式即便是一些不支持JSON
格式的语言平台同样有很多优秀的第三方库对其进行了支持比如Jackson
等等。
至于JDK序列化的方式如果你有兴趣或者开发的项目本身不支持其他序列化方式那么也是一个不错的选择~~~
源码
在文章的最后我在这里附上一个本人使用Java
写的简单的序列化工具有兴趣的同学可以参考一下。
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 += data[i + readIndex] << i * 8;
}
readIndex += TWO_LENGTH;
return value;
}
public int readInt() {
int value = 0;
for (int i = 0; i < FOUR_LENGTH; i++) {
value += data[i + readIndex] << i * 8;
}
readIndex += FOUR_LENGTH;
return value;
}
public long readLong() {
long value = 0;
for (int i = 0; i < EIGHT_LENGTH; i++) {
value += (long) data[i + readIndex] << 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;
}
}
}
再附带上一份序列化的代码。
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);
最后的最后瑞思拜~~~