在 C 和 C++ 这样的语言中,都提供了不同长度的整数类型:char
, short
, int
, long
(实际上,char
并不是真正的整数,但是可以把它当成整数来用。在实际应用场景中,很多人在 C 语言中用 char
来存储较小的整数)。
在大部分的 32 位操作系统上,这些类型分别对应 1 字节,2 字节,4 字节和 8 字节。但是需要注意的是,这些整数类型所对应的字节长度在不同的平台上是不一样的。相对而言,由于 Java 是针对跨平台来设计的,所以无论运行在什么平台上,Java 中的 byte
永远是 1 字节,short
是 2 字节,int
是 4 字节,long
是 8 字节。
C 语言中的整数类型都提供了对应的“无符号”版本,但是 Java 中就没有这个特性了。我觉得 Java 不支持无符号类型这个事儿实在是太不爽了,你想想,大量的硬件接口、网络协议以及文件格式都会用到无符号类型!(Java 中提供的 char
类型和 C 中的 char
有所不同,在 Java 中,char
是用 2 个字节来表示 Unicode 值,在 C 中,char
是用 1 个字节来表示 ASCII 值。虽然可以在 Java 中把 char
当做无符号短整型来使用,用来表示 0 到 2^16 的整数。但是这样来用可能产生各种诡异的事情,比如当你要打印这个数值的时候,实际上打印出来的是这个数值对应的字符,而不是这个数值本身的字符串表示)。
使用比要用的无符号类型更大的有符号类型。
例如:使用 short
来处理无符号的字节,使用 long
来处理无符号整数等(甚至可以使用 char
来处理无符号短整型)。
确实,这样看起来很浪费,因为你使用了 2 倍的存储空间,但是也没有更好的办法了。另外,需要提醒的是,对于 long
类型变量的访问不是原子性操作,所以,如果在多线程场景中,你得自己去处理同步的问题。
首先,把有符号的 byte
提升成 int
类型,然后对这个 int
进行按位与操作,仅保留最后 8 个比特位。因为 Java 中的 byte
是有符号的,所以当一个 byte
的无符号值大于 127 的时候,表示符号的二进制位将被设置为 1(严格来说,这个不能算是符号位,因为在计算机中数字是按照补码方式编码的),对于 Java 来说,这个就是负数。当将负数数值对应的 byte
提升为 int
类型的时候,0 到 7 比特位将会被保留,8 到 31 比特位会被设置为 1。然后将其与 0x000000FF
进行按位与操作来擦除 8 到 31 比特位的 1。上面这句代码可以简短的写作:
xFF & (int)buf[index]
Java 自动填充 0xFF
的前导的 0 ,并且在 Java 中,位操作符 &
会导致 byte
自动提升为 int
。
接下来你看到的是很多的按位左移运算符 <<
。 这个操作符会对左操作数按位左移右操作数指定的比特位。所以,如果你有一个 int foo = 0x000000FF
,那么 foo << 8
会得到 0x0000FF00
,foo << 16
会得到 0x00FF0000
。
最后是按位或操作符 |
。假设你现在把一个无符号短整型的 2 个字节加载到了对应的整数中,你会得到 0x00000012
和 0x00000034
两个整数。现在你把第一个字节左移 8 位得到 0x00001200
和 0x00000034
,然后你需要把他们再拼合回去。所以需要进行按位或操作。0x00001200 | 0x00000034
会得到 0x00001234
,这样就可以存储到 Java 中的 char
类型。
这些都是基础操作。但是对于无符号 int
,你需要把它存储到 long
类型中。其他操作和前面类似,只是你需要把 int
提升为 long
然后和 0xFFFFFFFFL
进行按位与操作。最后的 L
用来告诉 Java 请把这个常量视为 long
来处理。