Java 序列化

警告
本文最后更新于 2023-10-30,文中内容可能已过时。

Java 序列化

对应《Java 核心技术卷一 第 6 章 接口、lambda 表达式与内部类》中的序列化小节

序列化的本质是将对象转化为字节流,而字节流这种类型的数据非常容易通过网络传输,因此,当我们需要远程传递一个对象的时候,首选的方式就是将其序列化。

简单了解 Java 中的二进制表示

0b 开头表示的数,实际上描述的都是二进制位的情况,你可以用这种格式的表达式描述任意个数的二进制位的情况

0x 开头的为 16 进制,0 开头表示 8 进制,非常容易混淆,不常用,非 0 的数字开头就是 10 进制

例如,7 位二进制数,0b1111111,同时,这种,任意个数的二进制位的情况,可以按照规则转化为 Java 原始的数据类型,位数不够会自动补 0,位数超过会自动截断

1
2
3
4
5
6
7
8
int i = (int) 0b11;
// 被扩充为四个字节,不过还是表示 3
// 输出 3
System.out.println(i);
byte x = (byte) 0x00ff;
// 0x00ff 被截断为 0b11111111 表示 -1
// 输出 -1
System.out.println(x);

此外,同一个数字,在不同的进制下有不同的表示方法。比如一个 8 个二进制位:可以用二进制表示:0b10101010,也可以用 16 进制表示:0xaa,他们是相等的

1
2
3
int a = 0xaa;
int b = 0b10101010;
System.out.println(a == b);

尤其要注意的一点是,数字在占空间小的类型转化成占用空间大的类型的时候,比如从 int 转化为 long,正数会在前面补 0,负数的情况会更加复杂,需要计算补码,大部分的空位都会补 1。此时如果你需要对数字占用的每个字节进行单独的处理,就一定要注意这个补上的 1 的影响。

关于补码的计算,请看《Java 核心技术卷一 第 3 章 Java 基本程序设计结构》中的原码、反码、补码小节

在表示数字的时候,还可以添加 _ 来提升可读性

1
int dashInt = 1212_121;

常见类型的序列化和反序列化

JDK 中的原始数据类型的包装类并没有自带将实际类型的数据转化为字节数组的方法,因此,我们需要手写。

基础数据类型的包装类都实现了Number接口,shortValueintValuelongValuefloatValuedoubleValuebyteValue、这几个方法用于基础数据类型之间的相互转化,可能会出现精度丢失

Short

序列化和反序列化方法,在反序列化方法中为什么要这么写:(bytes[0] & 0xff) << 8,具体原因请看Long小节的解释分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static byte[] short2Bytes(Short shortObj) {
    if (shortObj == null) {
        return null;
    }
    short sh = shortObj;
    byte[] bytes = new byte[2];
    // 0xff 为 0b1111_1111,与这个数字做与运算是为了只获取此数字的最后 8 位,也就是最后一个字节
    bytes[1] = (byte) (sh & 0xff);
    // 通过右移动 8 位同时跟 0xff 做与运算获取倒数第二个字节
    bytes[0] = (byte) (sh >> 8 & 0xff);
    return bytes;
}

public static Short bytes2Short(byte[] bytes) {
    if (bytes == null || bytes.length < 2) {
        return null;
    }
    short sh = (short) ((bytes[0] & 0xff) << 8 | (bytes[1] & 0xff));
    return sh;
}

测试代码

1
2
3
4
5
6
7
8
9
System.out.println("------------------------------- short -------------------------------");
// short 占用 2 个字节。这里设置每个字节都是 15
short sh = (short) 0b00001111_00001111;
// 兼容负数
// short sh = (short) -1541;
System.out.println("原数值(十进制):" + sh);
byte[] bytes4short = short2Bytes(sh);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4short));
System.out.println("反序列化为原类型:" + bytes2Short(bytes4short));

输出

1
2
3
4
------------------------------- short -------------------------------
原数值(十进制):3855
序列化为字节数组:[15, 15]
反序列化为原类型:3855

Integer

序列化和反序列化方法,在反序列化方法中为什么要这么写:(bytes[0] & 0xff) << 24,具体原因请看Long小节的解释分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static byte[] integer2Bytes(Integer intObj) {
    if (intObj == null) {
        return null;
    }
    int in = intObj;
    byte[] bytes = new byte[4];
    // 0xff 为 0b1111_1111,与这个数字做与运算是为了只获取此数字的最后 8 位,也就是最后一个字节
    bytes[3] = (byte) (in & 0xff);
    // 通过右移动 8 位同时跟 0xff 做与运算获取倒数第二个字节
    bytes[2] = (byte) (in >> 8 & 0xff);
    // 以此类推
    bytes[1] = (byte) (in >> 16 & 0xff);
    bytes[0] = (byte) (in >> 24 & 0xff);
    return bytes;
}

public static Integer bytes2Integer(byte[] bytes) {
    if (bytes == null || bytes.length < 4) {
        return null;
    }
    int sh = (int) ((bytes[0] & 0xff) << 24 | (bytes[1] & 0xff) << 16 | (bytes[2] & 0xff) << 8 | (bytes[3] & 0xff));
    return sh;
}

测试代码

1
2
3
4
5
6
7
8
9
System.out.println("------------------------------- int -------------------------------");
// int 占用 4 个字节。这里设置每个字节都是 15
int in = (int) 0b00001111_00001111_00001111_00001111;
// 兼容负数
// int in = (int) -214845647;
System.out.println("原数值(十进制):" + in);
byte[] bytes4int = integer2Bytes(in);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4int));
System.out.println("反序列化为原类型:" + bytes2Integer(bytes4int));

输出

1
2
3
4
------------------------------- int -------------------------------
原数值(十进制):252645135
序列化为字节数组:[15, 15, 15, 15]
反序列化为原类型:252645135

Long

序列化和反序列化方法

在反序列化方法中为什么要这么写:(long) bytes[0] & 0xff) << 56

为什么要进行强制类型转化为 (long),这是因为左移超过 3 个字节的长度,就超过了 int 的长度,因此需要转化为 long

为什么要 & 0xff,这主要是为了兼容 long 为负数的情况。

比如,以 1 个字节的 -16 为例,假设 bytes[0] 为 -16,对应的二进制位补码 0b11110000,强制转化为 long 类型之后,的补码为

1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 0000

而不是

0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 0000

可见,同一个负数,在不同的类型中,其补码情况也不一样,正数是一样的

(long) bytes[0]  << 56,左移 7 个字节,前面的 1 刚好全部超出范围,不会有影响,但是 (long) bytes[1] << 48 的时候,左移 6 个字节,变成

1111 1111 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

前面的一个字节的 1111 1111 就会影响结果

而我们期望的左移 48 位的结果是

0000 0000 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

归根结底,还是因为 (long) bytes[1] 之后,与一个字节的 bytes[1] 相比,前面多出了很多为 1  的二进制位,而正数的时候没有这个问题,

因此,为了保证结果正确,在强制转化为 long 类型之后,需要格式化一下  ((long) bytes[1] & 0xff) ,去掉前面多出的为 1 的二进制位

 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
public static byte[] long2Bytes(Long longObj) {
    if (longObj == null) {
        return null;
    }
    long lo = longObj;
    byte[] bytes = new byte[8];
    // 0xff 为 0b1111_1111,与这个数字做与运算是为了只获取此数字的最后 8 位,也就是最后一个字节
    bytes[7] = (byte) (lo & 0xff);
    // 通过右移动 8 位同时跟 0xff 做与运算获取倒数第二个字节
    bytes[6] = (byte) (lo >> 8 & 0xff);
    // 以此类推
    bytes[5] = (byte) (lo >> 16 & 0xff);
    bytes[4] = (byte) (lo >> 24 & 0xff);
    bytes[3] = (byte) (lo >> 32 & 0xff);
    bytes[2] = (byte) (lo >> 40 & 0xff);
    bytes[1] = (byte) (lo >> 48 & 0xff);
    bytes[0] = (byte) (lo >> 56 & 0xff);
    return bytes;
}

public static Long bytes2Long(byte[] bytes) {
    if (bytes == null || bytes.length < 8) {
        return null;
    }
    // (long) bytes[0] & 0xff) << 56
    // 为什么要进行强制类型转化为 (long),这是因为左移超过 3 个字节的长度,就超过了 int 的长度,因此需要转化为 long
    // 为什么要 & 0xff,这主要是为了兼容 long 为负数的情况
    // 以 1 个字节的 -16 为例,假设 bytes[0] 为 -16,对应的二进制位补码 0b11110000,强制转化为 long 类型之后,的补码为
    // 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 0000
    // 而不是
    // 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111 0000
    // 可见,同一个负数,在不同的类型中,其补码情况也不一样,正数是一样的
    // (long) bytes[0]  << 56,左移 7 个字节,前面的 1 刚好全部超出范围,不会有影响,但是 (long) bytes[1] << 48 的时候,左移 6 个字节,变成
    // 1111 1111 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
    // 前面的一个字节的 1111 1111 就会影响结果
    // 而我们期望的左移 48 位的结果是
    // 0000 0000 1111 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
    // 归根结底,还是因为 (long) bytes[1] 之后,与一个字节的 bytes[1] 相比,前面多出了很多为 1  的二进制位,而正数的时候没有这个问题,
    // 因此,为了保证结果正确,在强制转化为 long 类型之后,需要格式化一下  ((long) bytes[1] & 0xff) ,去掉前面多出的为 1 的二进制位
    return ((long) bytes[0] & 0xff) << 56 | ((long) bytes[1] & 0xff) << 48 | ((long) bytes[2] & 0xff) << 40 | ((long) bytes[3] & 0xff) << 32 | ((long) bytes[4] & 0xff) << 24 | ((long) bytes[5] & 0xff) << 16 | ((long) bytes[6] & 0xff) << 8 | ((long) bytes[7] & 0xff);
}

测试代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
System.out.println("------------------------------- long -------------------------------");
// long 占用 8 个字节。这里设置每个字节都是 15,注意最后要加上 L,不然会被当成 int 类型
// 1085102592571150095L
long lo = (long) 0b00001111_00001111_00001111_00001111_00001111_00001111_00001111_00001111L;
// 兼容负数
// long lo = -1085102592571150095L;
System.out.println("原数值(十进制):" + lo);
byte[] bytes4long = long2Bytes(lo);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4long));
System.out.println("反序列化为原类型:" + bytes2Long(bytes4long));

输出

1
2
3
4
------------------------------- long -------------------------------
原数值(十进制):1085102592571150095
序列化为字节数组:[15, 15, 15, 15, 15, 15, 15, 15]
反序列化为原类型:1085102592571150095

Float

float 占用 4 个字节,

  • 最高位也就是第 31 位为符号位 (掩码 0x80000000 选择的位) 表示浮点数的符号,0 为正 1 为负。

  • 第 30-23 位 (掩码 0x7f800000 选择的为) 表示指数。

  • 第 22-0 位 (由掩码 0x007fffff 选择的位) 表示浮点数的有效位数 (有时称为尾数)。

因此,一个浮点数可以用一个 int 类型的数字表示,实际上我们也是这么干的。

序列化和反序列化方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static byte[] float2Bytes(Float floatObj) {
    if (floatObj == null) {
        return null;
    }
    float flo = floatObj;
    int i = Float.floatToIntBits(flo);
    byte[] bytes = integer2Bytes(i);
    return bytes;
}

public static Float bytes2Float(byte[] bytes) {
    int i = bytes2Integer(bytes);
    float f = Float.intBitsToFloat(i);
    return f;
}

测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
System.out.println("------------------------------- float -------------------------------");
// float 占用 4 个字节,
// 最高位也就是第 31 位为符号位 (掩码 0x80000000 选择的位) 表示浮点数的符号,0 为正 1 为负。
// 第 30-23 位 (掩码 0x7f800000 选择的为) 表示指数。
// 第 22-0 位 (由掩码 0x007fffff 选择的位) 表示浮点数的有效位数 (有时称为尾数)。
// 因此,一个浮点数可以用一个 int 类型的数字表示,实际上我们也是这么干的。
float flo = 21212.23555f;
// 兼容负数
// float flo = -21212.23555f;
System.out.println("原数值(十进制):" + flo);
byte[] bytes4float = float2Bytes(flo);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4float));
System.out.println("反序列化为原类型:" + bytes2Float(bytes4float));

输出:

1
2
3
4
------------------------------- float -------------------------------
原数值(十进制):21212.236
序列化为字节数组:[70, -91, -72, 121]
反序列化为原类型:21212.236

Double

double 占用 8 个字节,

  • 第 63 位 (掩码 0x8000000000000000L 选择的位) 表示浮点数的符号。

  • 第 62-52 位 (掩码 0x7ff0000000000000L 选择的位) 表示指数。

  • 第 51-0 位 (掩码 0x000fffffffffffffffL 选择的位) 表示浮点数的有效位数 (有时称为尾数)。

因此,一个双精度浮点数可以用一个 long 类型的数字表示,实际上我们也是这么干的。

序列化和反序列化方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static byte[] double2Bytes(Double doubleObj) {
    if (doubleObj == null) {
        return null;
    }
    double dou = doubleObj;
    long i = Double.doubleToLongBits(dou);
    byte[] bytes = long2Bytes(i);
    return bytes;
}

public static Double bytes2Double(byte[] bytes) {
    long i = bytes2Long(bytes);
    double dou = Double.longBitsToDouble(i);
    return dou;
}

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
System.out.println("------------------------------- double -------------------------------");
// double 占用 8 个字节,
// 第 63 位 (掩码 0x8000000000000000L 选择的位) 表示浮点数的符号。
// 第 62-52 位 (掩码 0x7ff0000000000000L 选择的位) 表示指数。
// 第 51-0 位 (掩码 0x000fffffffffffffffl 选择的位) 表示浮点数的有效位数 (有时称为尾数)。
// 因此,一个双精度浮点数可以用一个 long 类型的数字表示,实际上我们也是这么干的。
double dou = 45444565678978.445d;
// 兼容负数
// float flo = -21212.23555f;
System.out.println("原数值(十进制):" + dou);
byte[] bytes4double = double2Bytes(dou);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4double));
System.out.println("反序列化为原类型:" + bytes2Double(bytes4double));

输出:

1
2
3
4
------------------------------- double -------------------------------
原数值(十进制):4.5444565678978445E13
序列化为字节数组:[66, -60, -86, 113, -104, -35, -63, 57]
反序列化为原类型:4.5444565678978445E13

Boolean

直接转化为 1 或者 0 来进行序列化。然后参考 Integer 的序列化方式即可。占用四个字节,实际的 Boolean 也是这样干的

序列化和反序列化方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static byte[] boolean2Bytes(Boolean booleanObj) {
    if (booleanObj == null) {
        return null;
    }
    boolean boo = booleanObj;
    int i = boo ? 1 : 0;
    byte[] bytes = integer2Bytes(i);
    return bytes;
}

public static Boolean bytes2Boolean(byte[] bytes) {
    int i = bytes2Integer(bytes);
    boolean b = i == 1 ? true : false;
    return b;
}

测试

1
2
3
4
5
6
System.out.println("------------------------------- boolean -------------------------------");
Boolean boo = true;
byte[] booBytes = boolean2Bytes(boo);
System.out.println("原数值(十进制):" + boo);
System.out.println("序列化为字节数组:" + Arrays.toString(booBytes));
System.out.println("反序列化为原类型:" + bytes2Boolean(booBytes));

输出

1
2
3
4
------------------------------- boolean -------------------------------
原数值(十进制):true
序列化为字节数组:[0, 0, 0, 1]
反序列化为原类型:true

Date

通过获取从1970-01-01T00:00:00Z到现在的毫秒数来确定日期,因此,我们只需要存这个 long 型的毫秒数即可。占用 8 个字节。

序列化方法和反序列化方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static byte[] dateBytes(Date dateObj) {
    if (dateObj == null) {
        return null;
    }
    // 精度只能到毫秒
    long i = dateObj.getTime();
    byte[] bytes = long2Bytes(i);
    return bytes;
}

public static Date bytes2Date(byte[] bytes) {
    long i = bytes2Long(bytes);
    Date dou = new Date();
    // 精度只能到毫秒
    dou.setTime(i);
    return dou;
}

测试

1
2
3
4
5
6
7
System.out.println("------------------------------- Date -------------------------------");
// 只能到毫秒级别
Date now = new Date();
System.out.println("原数值(十进制):" + now.toString());
byte[] bytes4date = dateBytes(now);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4date));
System.out.println("反序列化为原类型:" + bytes2Date(bytes4date));

输出

1
2
3
4
------------------------------- Date -------------------------------
原数值(十进制):Mon Oct 30 11:05:54 CST 2023
序列化为字节数组:[0, 0, 1, -117, 126, -117, -124, 127]
反序列化为原类型:Mon Oct 30 11:05:54 CST 2023

新的日期 API

LocalDateTimeZonedDateTimeInstance对象序列化的格式都是将秒存储为 long,将纳秒获取为 int,精度为纳秒,存储空间为 8+4=12 了。

这里以Instance为例,实际上Instance也是用了秒和纳秒这两个字段来保存时间信息的。

Instance的序列化和反序列化方法,其他的类的序列化参考Instance即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static byte[] instantBytes(Instant instant) {
    if (instant == null) {
        return null;
    }
    long second = instant.getEpochSecond();
    int nano = instant.getNano();
    byte[] secondBytes = long2Bytes(second);
    byte[] nanoBytes = integer2Bytes(nano);
    byte[] all = new byte[12];
    System.arraycopy(secondBytes, 0, all, 0, secondBytes.length);
    System.arraycopy(nanoBytes, 0, all, secondBytes.length, nanoBytes.length);
    return all;
}

public static Instant bytes2Instant(byte[] bytes) {
    byte[] seconds = new byte[8];
    System.arraycopy(bytes, 0, seconds, 0, 8);
    byte[] nanos = new byte[4];
    System.arraycopy(bytes, 8, nanos, 0, 4);
    long i0 = bytes2Long(seconds);
    int i1 = bytes2Integer(nanos);
    Instant instant = Instant.ofEpochSecond(i0, i1);
    return instant;
}

测试

1
2
3
4
5
6
7
System.out.println("------------------------------- Instant -------------------------------");
// 精度更高,占用字节也更多,占用 12 个字节,一个 long,一个 int
Instant nowInstance = Instant.now();
System.out.println("原数值(十进制):" + nowInstance.toString());
byte[] bytes4instant = instantBytes(nowInstance);
System.out.println("序列化为字节数组:" + Arrays.toString(bytes4instant));
System.out.println("反序列化为原类型:" + bytes2Instant(bytes4instant));

输出

1
2
3
4
------------------------------- Instant -------------------------------
原数值(十进制):2023-10-30T03:05:54.569Z
序列化为字节数组:[0, 0, 0, 0, 101, 63, 29, -110, 33, -22, 64, 64]
反序列化为原类型:2023-10-30T03:05:54.569Z

String

字符串的序列化 JDK 中提供了专门的方法,字符串转换成的字节数组的长度不固定,由字符串的内容决定。

1
2
3
4
5
6
7
8
System.out.println("------------------------------- String -------------------------------");
// 字符串转换成的字节数组的长度不固定,由字符串的内容决定
String str = "我爱我的祖国";
byte[] strBytes = str.getBytes(StandardCharsets.UTF_8);
String newString = new String(strBytes, StandardCharsets.UTF_8);
System.out.println("原数值(十进制):" + str);
System.out.println("序列化为字节数组:" + Arrays.toString(strBytes));
System.out.println("反序列化为原类型:" + newString);

输出

1
2
3
4
------------------------------- String -------------------------------
原数值(十进制):我爱我的祖国
序列化为字节数组:[-26, -120, -111, -25, -120, -79, -26, -120, -111, -25, -102, -124, -25, -91, -106, -27, -101, -67]
反序列化为原类型:我爱我的祖国

Object 自定义类型

兼容任意对象的序列化方法,可以序列化任意对象,

优点是万能,适用于所有类型的数据,缺点是不能做到高效的压缩,例如前面通过专门的工具序列化 Instant 只占了 12 个字节,但是通过此方法却占了 50 个字节,差不多是 4 倍。

因此能用特定的序列化方法就用特定类型的序列化方法,没有办法进行特化的处理,再考虑使用此通用方法进行序列化。

序列化和反序列化方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public static byte[] object2Bytes(Object obj) {
    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
         ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {

        // 将 out 对象进行序列化
        objectOutputStream.writeObject(obj);
        return byteArrayOutputStream.toByteArray();
    } catch (Throwable t) {
        throw new RuntimeException(t.getMessage(), t);
    }
}

public static Object bytes2Object(byte[] bs) {
    try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bs, 0, bs.length);
         ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);) {
        return objectInputStream.readObject();
    } catch (Throwable t) {
        throw new RuntimeException(t.getMessage(), t);
    } finally {
    }
}

测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
System.out.println("------------------------------- Object -------------------------------");
// 任意对象的序列化方法,可以序列化任意对象,
// 优点是万能,适用于所有类型的数据
// 缺点是不能做到高效的压缩,例如前面通过专门的工具序列化 Instant 只占了 12 个字节,但是通过此方法却占了 50 个字节,差不多是 4 倍。
// 因此能用特定的序列化方法就用特定类型的序列化方法,没有办法进行特化的处理,再考虑使用此通用方法进行序列化
Instant instanceObj = Instant.now();
byte[] objBytes = object2Bytes(instanceObj);
System.out.println("原数值(十进制):" + instanceObj);
System.out.println("序列化为字节数组:" + Arrays.toString(objBytes));
System.out.println("反序列化为原类型:" + bytes2Object(objBytes));

输出

1
2
3
4
------------------------------- Object -------------------------------
原数值(十进制):2023-10-30T03:05:54.601Z
序列化为字节数组:[-84, -19, 0, 5, 115, 114, 0, 13, 106, 97, 118, 97, 46, 116, 105, 109, 101, 46, 83, 101, 114, -107, 93, -124, -70, 27, 34, 72, -78, 12, 0, 0, 120, 112, 119, 13, 2, 0, 0, 0, 0, 101, 63, 29, -110, 35, -46, -120, 64, 120]
反序列化为原类型:2023-10-30T03:05:54.601Z

通过自定义序列化的方式进行空间压缩

原始类型的包装类和对象类型的数据序列化成二进制其实是没有什么可以优化的空间的,只有一些具有特定特征的数据,才有进一步优化的空间。

比如将数据库的所有数据都查出来,每一行都是一个 map,key 为列名,value 为列值,将 map 放到 list 里面,然后我们需要将其序列化为字符串,常规的手段是通过 JSON 工具转成 JSON 字符串,这样的格式易于阅读,但是在输出传输的过程中并不在意可读性,而是在乎空间,JSON 字符串为了保证可读性对空间造成了极大的浪费,浪费的重点在于表的数据是有特征的,即每一列列名不需要在 list 的每一个元素中都存下来,我们只需要存一次列名,然后在 list 中只按照列的排列顺序存数据即可,反序列化的时候按照数据的位置就可以找到对应的列名,这样是就节省了空间。

此外,通过对各个数据类型的使用定制的(本文前面提到的)序列化方法和反序列化方法,可以对数据进行进一步的压缩。

对每一个数据的序列化结果的设计如下:数据标识 + 实际的数据长度 + 数据本身

  • 数据标识:长度为一个字节,前四个字节为数据的 Java 类型,因此总共支持 15 中类型,够用了,包含定制序列化和不定制序列化两种类型,定制序列化类型主要针对一些大小固定的对象,比如原始类型的包装类,如果是动态大小的对象,则为不定制化序列化类型,定制序列化类型有自己的序列化/反序列化方法,不定制序列化走的是对象的通用序列化方法,参考Object 自定义类型小节,后四个字节保存的是:数据占用的字节数也就是数据的长度,这个长度数字本身占用的字节数,这样通过读取数据标识的后四位,就可以知道再往后读几个字节,就能知道数据的具体长度,知道数据的具体长度之后,再往后读这个长度的字节数就能读出来完整的数据。数据也就读取完毕了。固定大小的对象后四位为 0,因为数据的长度是固定的,就不用浪费空间来保存。

    相比于 http 头通过定长确定请求头大小的做法,(《用电信号传输 TCP/IP 数据 —— 探索协议栈和网卡》的TCP 报文头详解小节中对于数据偏移位的解释),这种做法是一种更为精确的做法。

  • 数据(以字节为单位)的长度数字占用的字节,根据,固定大小的对象没有这部分空间,因为占用的字节数固定,就不需要占用空间来保存。

  • 数据本身的字节

依照这个设计,假设 int 类型代表的数据标识的前四位为0011,一个 int 类型的数字 25,会被保存为00110000_00011001;假设 String 类型代表的数据标识的前四位为1001,字符串我爱我的祖国,会被保存为10010010_00010010_11100110_10001000_10010001_11100111_10001000_10110001_11100110_10001000_10010001_11100111_10011010_10000100_11100111_10100101_10010110_11100101_10011011_10111101,分析如下,字符串我爱我的祖国总共占用 18 个字节,18 用一个 2 字节的大小就能保存,因此其长度为 2,因为数据标识的后四位为0010,整个数据标识为10010010,后面紧跟着表示 18 个两个字节00010010,再后面就是字符串我爱我的祖国的序列化结果了。

这样,反序列化的时候,先读取数据标识,看前四位,确定反序列化的方法,看后四位,如果是 0,则直接根据类型的固定大小往后读固定长度即可,如果不是 0,则还要往后读取后四位表示的长度的字节,读取出来的数字为数据本身的长度,再根据这个长度读取出数据的本身,再进行反序列化。

最后完整代码如下:

需要引入依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.12.5</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jsr310</artifactId>
        <version>2.12.5</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.12.5</version>
    </dependency>

    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>16.0.1</version>
    </dependency>

</dependencies>

JSON 相关工具类JsonUtilsJsonUtils.java

字节数组相关操作工具类:ByteUtils ByteUtils.java

消息序列化类MessageUtilsMessageUtils.java


0%