深入理解 Java 中的类型转换

1 概述

本文将从二进制角度深入理解 Java 中的类型转换问题,涉及的内容如下

  1. int 转成成字节数组
  2. 字节数组转成 int
  3. Java 中的类型转换问题
  4. ByteBuffer 对象的使用介绍

2 int 转成成字节数组

2.1 & 与运算

int 转成成字节数组过程中需要用到与运算,其使用规律为:两个操作数中位都为1,结果才为1,否则结果为0

2.2 转换过程

int 占用 4 个字节,下面是具体的转换过程

  1. 原始二进制内容如下
  • 00001100 11001100 11001011 01100000
  1. 向右移动 24 位,左边用 0 替换,右边的 11001100 11001011 01100000 去掉,具体如下,
  • 00000000 00000000 00000000 00001100
  1. 右移动 24 位后跟 0XFF 进行与运算,最后结果为 int 类型,由于高 24 位全都是 0,所以可以强制转成 byte,最后放到 byte 数组中,具体运算过程如下,
  • 00000000 00000000 00000000 00001100 : 右移动 24 位
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 00001100 : 与运算结果
  • byte[0] = (byte) ((data >> 24) & 0xFF)
  1. 向右移动 16 位,左边用 0 替换,右边的 11001011 01100000 去掉,具体如下,
  • 00000000 00000000 00001100 11001100
  1. 右移动 16 位后跟 0XFF 进行与运算
  • 00000000 00000000 00001100 11001100 : 右移动 16 位
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 11001100 : 与运算结果
  • byte[1] = (byte) ((data >> 16) & 0xFF)
  1. 向右移动 8 位,左边用 0 替换,右边的 01100000 去掉,具体如下,
  • 00000000 00001100 11001100 11001011
  1. 右移动 16 位后跟 0XFF 进行与运算
  • 00000000 00001100 11001100 11001011 : 右移动 8 位
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 11001011 : 与运算结果
  • byte[2] = (byte) ((data >> 8) & 0xFF)
  1. 最后低 8 位跟 0XFF 进行与运算
  • 00001100 11001100 11001011 01100000 : 最后低 8 位
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 01100000 : 与运算结果
  • byte[3] = (byte) (data & 0xFF)

2.3 int2Bytes 方法封装

将上面的计算过程封装成方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 将 int 转成长度为 4 的字节数组
*
* @param data int
* @return 长度为 4 的字节数组
*/
public static byte[] int2Bytes(int data) {
return new byte[] {
(byte) ((data >> 24) & 0xFF),
(byte) ((data >> 16) & 0xFF),
(byte) ((data >> 8) & 0xFF),
(byte) (data & 0xFF)
};
}

3 字节数组转成 int

3.1 | 或运算

字节数组转成 int 过程中需要用到或运算,其使用规律为:两个位只要有一个为1,那么结果就是1,否则就为0

3.2 转换过程

在上面将 int 转成字节数组的过程中,字节数组中索引 0 存储的是最高位 8 位,索引 3 存储的是最低位 8 位。

  1. 最低位 8 位的字节为 byte[3],通过跟 0XFF 与运算后转成 int 类型,具体如下
  • b[3] & 0xFF
  • 00000000 00000000 00000000 01100000
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 01100000 : 与运算结果
  1. byte[2],通过跟 0XFF 与运算后转成 int 类型,然后再向左移动 8 位,具体如下
  • (b[2] & 0xFF) << 8
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 11001011 : 与运算结果
  • 00000000 00000000 11001011 00000000 :向左移动 8 位,用 0 填补
  1. byte[1],通过跟 0XFF 与运算后转成 int 类型,然后再向左移动 16 位,具体如下
  • (b[1] & 0xFF) << 16
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 11001100 : 与运算结果
  • 00000000 11001100 00000000 00000000 :向左移动 16 位,用 0 填补
  1. byte[0],通过跟 0XFF 与运算后转成 int 类型,然后再向左移动 24 位,具体如下
  • (b[0] & 0xFF) << 24
  • 00000000 00000000 00000000 11111111 : OXFF
  • 00000000 00000000 00000000 00001100 : 与运算结果
  • 00001100 00000000 00000000 00000000 :向左移动 24 位,用 0 填补
  1. 最后进行或运算,具体如下
  • 00000000 00000000 00000000 01100000 : b[3] & 0xFF
  • 00000000 00000000 11001011 00000000 :(b[2] & 0xFF) << 8
  • 00000000 11001100 00000000 00000000 :(b[1] & 0xFF) << 16
  • 00001100 00000000 00000000 00000000 :(b[0] & 0xFF) << 24
  • 00001100 11001100 11001011 01100000 :或运算结果

3.3 bytesToInt 方法封装

具体如下

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 将长度为 4 的字节数组转成 int
*
* @param bytes 长度为 4 的字节数组
* @return int
*/
public static int bytesToInt(byte[] bytes) {
return bytes[3] & 0xFF |
(bytes[2] & 0xFF) << 8 |
(bytes[1] & 0xFF) << 16 |
(bytes[0] & 0xFF) << 24;
}

4 Java 中的类型转换问题

4.1 字节丢失的问题

从上面的 int 转成字节数组以及字节数组转成 int 的过程中可以了解到 Java 类型转换其实就是不同字节数组之间的转换。

  • 一个 byte 转成 int 没有任何问题,可以直接转,因为 byte 的高 24 位全是 0;int 转成 byte 就不行了,因为 int 有 4 个字节,高 24 位不全是 0
  • short 转成 int 也没有任何问题,因为 short 有 2 个字节, 高 16 位全是 0

根据上面的结论和下面 Java 中基本类型,就能明白为什么 long 不能转成 int,而 int 可以转成 long

  • byte:1个字节,8位,最大存储数据量是255,存放的数据范围是-128~127之间。
  • short:2个字节,16位,最大数据存储量是65536,数据范围是-32768~32767之间。
  • int:4个字节,32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。
  • long:8个字节,64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。
  • float:4个字节,32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。
  • double:8个字节,64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。
  • boolean:1个字节,8位,只有true (0000 0001) 和false(0000 0000)两个取值
  • char:2个字节,16位,存储Unicode码,用单引号赋值

4.2 举例说明

下面以 long 转成 int 为例说明转换后字节丢失的问题

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
/**
* 获取 long 类型数据的二进制字符串 <br>
*
* long 115 byte:00000000 00000000 00000000 00000000 00000000 00000000 00000000 01110011 <br>
* long -115 byte:11111111 11111111 11111111 11111111 11111111 11111111 11111111 10001101 <br>
*
* @param a long 类型
* @return
*/
public static String getLongByteString(long a) {
String byteString = Long.toBinaryString(a);
if (byteString.length() < 64) {
String temp = "";
for (int i = 0; i < 64 - byteString.length(); i++) {
temp += "0";
}
byteString = temp.concat(byteString);
}
String[] byteArr = { byteString.substring(0, 8), byteString.substring(8, 16), byteString.substring(16, 24), byteString.substring(24, 32), byteString.substring(32, 40), byteString.substring(40, 48), byteString.substring(48, 56), byteString.substring(56, 64) };
return ArrayUtils.join(byteArr, " ");
}

@Test
public void test_long2int() {
long a = 122214748000L;
String tempByteString = getLongByteString(a);
System.out.println(String.format("long a=%s 二进制序列如下", a));
System.out.println(tempByteString);

int b = (int) a;
a = (long) b;
tempByteString = getLongByteString(a);
System.out.println(String.format("先强制转成 int,再强制转成 long a=%s 后二进制序列如下", a));
System.out.println(tempByteString);
System.out.println("从上面的转换过程可以发现 int 转成 long 后,高 32 位全部变成了 0");
}
  • 输出如下
1
2
3
4
5
long a=122214748000 二进制序列如下
00000000 00000000 00000000 00011100 01110100 10010001 00001111 01100000
先强制转成 int,再强制转成 long a=1955663712 后二进制序列如下
00000000 00000000 00000000 00000000 01110100 10010001 00001111 01100000
从上面的转换过程可以发现 int 转成 long 后,高 32 位全部变成了 0,低 32 位不变

5 ByteBuffer 对象的使用介绍

5.1 ByteBuffer 原理概述

ByteBuffer 内部有一个数组,可以将 int,long 等类型转成字节序列并存储在数组中,通过 putXXX 和 getXXX 方法进行存储和读取操作。

image

  • java.nio.ByteBuffer : 抽象类
  • java.nio.DirectByteBuffer :具体类
  • java.nio.Bits :工具类,底层通过 sun.misc.Unsafe 中的 getByte 和 putByte 方法进行字节的读取和存储

完整的方法列表如下

image

5.2 使用介绍

下面以 putInt 和 getInt 方法举例说明,具体如下

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

/**
* 获取 int 类型数据的二进制字符串 <br>
*
* int 115 byte:00000000 00000000 00000000 01110011 <br>
* int -115 byte:11111111 11111111 11111111 10001101 <br>
*
* @param a int 类型
* @return 字符串
*/
public static String getIntByteString(int a) {
String byteString = Integer.toBinaryString(a);
if (byteString.length() < 32) {
String temp = "";
for (int i = 0; i < 32 - byteString.length(); i++) {
temp += "0";
}
byteString = temp.concat(byteString);
}
String[] byteArr = { byteString.substring(0, 8), byteString.substring(8, 16), byteString.substring(16, 24), byteString.substring(24, 32) };
return ArrayUtils.join(byteArr, " ");
}

@Test
public void test_ByteBuffer() {
int a = 214748000;
System.out.println(getIntByteString(a));
ByteBuffer byteBuffer = ByteBuffer.allocate(8);
byteBuffer.putInt(4, a);

int b = byteBuffer.getInt(4);
Assert.assertTrue(a == b);
byte[] bytes = byteBuffer.array();
for (int i = 0; i < bytes.length; i++) {
System.out.println(getIntByteString(bytes[i] & 0xff));
}
}

输出如下

1
2
3
4
5
6
7
8
9
00001100 11001100 11001011 01100000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00001100
00000000 00000000 00000000 11001100
00000000 00000000 00000000 11001011
00000000 00000000 00000000 01100000
  1. 通过 allocate 方法创建一个内部含有 8 个字节长度的数组的 ByteBuffer 对象
  2. putInt 方法中的第一个参数 4 表示将 a 转成字节序列后第一个字节存储在数组中的索引
  3. getInt 方法中的参数 4 表示从数组中的索引开始位置读取字节序列,然后返回 int
  4. array 方法返回 ByteBuffer 对象的内部字节数组,通过遍历发现:int 转成字节序列后,高位字节从索引 4 开始一一放到字节数组中
Buy me a cup of coffee