Java 与无符号那些事儿
最近在使用 Java 作为 WebSocket 客户端连接 Node.js 的 WebSocket 服务器的时候,由于使用的客户端库比较老,所以遇到了字节符号的问题,上网查了一下,看到这篇文章写的很有意思,就翻译一下。
原文地址:http://www.darksleep.com/player/JavaAndUnsignedTypes.html
原文作者:Sean R. Owens
以下是正文
Java 中的无符号类型是怎么回事儿?
在 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 中,chat
是用 2 个字节来表示 Unicode 值,在 C 中,char
是用 1 个字节来表示 ASCII 值。虽然可以在 Java 中把 char
当做无符号短整型来使用,用来表示 0 到 2^16 的整数。但是这样来用可能产生各种诡异的事情,比如当你要打印这个数值的时候实际上打印出来的是这个数值对应的字符而不是这个数值本身的字符串表示)。
那么,如何应对 Java 中无符号类型的缺失?
好吧,对于我给出的这种方案,你可能会不喜欢……
答案就是:使用比要用的无符号类型更大的有符号类型。
例如:使用 short
来处理无符号的字节,使用 long
来处理无符号整数等(甚至可以使用 char
来处理无符号短整型)。确实,这样看起来很浪费,因为你使用了 2 倍的存储空间,但是也没有更好的办法了。另外,需要提醒的是,对于 long
类型变量的访问不是原子性操作,所以,如果在多线程场景中,你得自己去处理同步的问题。
如何以无符号的形式存储和读取数据?
如果有人从网络上给你发送了一堆包含无符号数值的字节(或者从文件中读取的字节),那么你需要进行一些额外的处理才能把他们转换到 Java 中的更大的数值类型。
还有一个就是字节序问题。但是现在我们先不管它,就当它是“网络字节序”,也就是“高位优先”,这也是 Java 中的标准字节序。
从网络字节序中读取
假设我们开始处理一个字节数组,我们希望从中读取一个无符号的字节,一个无符号短整型和一个无符号整数。
short anUnsignedByte = 0;
char anUnsignedShort = 0;
long anUnsignedInt = 0;
int firstByte = 0;
int secondByte = 0;
int thirdByte = 0;
int fourthByte = 0;
byte buf[] = getMeSomeData();
// Check to make sure we have enough bytes
if(buf.length < (1 + 2 + 4)) doSomeErrorHandling();
int index = 0;
firstByte = (0x000000FF & ((int)buf[index]));
index++;
anUnsignedByte = (short)firstByte;
firstByte = (0x000000FF & ((int)buf[index]));
secondByte = (0x000000FF & ((int)buf[index+1]));
index = index+2;
anUnsignedShort = (char) (firstByte << 8 | secondByte);
firstByte = (0x000000FF & ((int)buf[index]));
secondByte = (0x000000FF & ((int)buf[index+1]));
thirdByte = (0x000000FF & ((int)buf[index+2]));
fourthByte = (0x000000FF & ((int)buf[index+3]));
index = index+4;
anUnsignedInt = ((long) (firstByte << 24
| secondByte << 16
| thirdByte << 8
| fourthByte))
& 0xFFFFFFFFL;
好吧,现在看起来有一点儿复杂。但是实际上很直观。首先,你看到很多这样的东东:
0x000000FF & (int)buf[index]
首先,把有符号的 byte
提升成 int
类型,然后对这个 int
进行按位与操作,仅保留最后 8 个比特位。因为 Java 中的 byte
是有符号的,所以当一个 byte
的无符号值大于 127 的时候,表示符号的二进制位将被设置为 1(严格来说,这个不能算是符号位,因为在计算机中数字是按照补码方式编码的),对于 Java 来说,这个就是负数。当将负数数值对应的 byte
提升为 int
类型的时候,0 到 7 比特位将会被保留,8 到 31 比特位会被设置为 1。然后将其与 0x000000FF
进行按位与操作来擦除 8 到 31 比特位的 1。上面这句代码可以简短的写作:
0xFF & (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
来处理。
向网络写入字节序
假设现在我们要把上面步骤中我们读取到的数值写入到缓冲区。我们当时是按照无符号 byte
,无符号 short
和无符号 int
的顺序读取的,现在,甭管什么原因吧,我们打算按照无符号 int
,无符号 short
和无符号 byte
的顺序来写出。
buf[0] = (anUnsignedInt & 0xFF000000L) >> 24;
buf[1] = (anUnsignedInt & 0x00FF0000L) >> 16;
buf[2] = (anUnsignedInt & 0x0000FF00L) >> 8;
buf[3] = (anUnsignedInt & 0x000000FFL);
buf[4] = (anUnsignedShort & 0xFF00) >> 8;
buf[5] = (anUnsignedShort & 0x00FF);
buf[6] = (anUnsignedByte & 0xFF);
字节序到底是怎么回事儿?
这是什么意思?我需要关注吗?以及,网络字节序什么样的?
Java 中所使用的“高位优先”字节序又被称为“网络字节序”。Intel x86 处理器是“低位优先”字节序(除非你在上面运行 Java 程序)。x86 系统创建的数据文件通常是(但不是必须的)低位优先的,而 Java 程序创建的数据文件通常是(但不是必须的)高位优先的。任何系统都可以按照自己需要的字节序来输出数据。
字节序是什么意思?
“字节序”是指计算机是按照何种顺序在内存中存储数值的。常见的无非是高位优先和低位优先两种模式。你当然需要关注字节序的问题了,否则,如果你按照高位优先的字节序去读取一个低位优先字节序存储的数据文件,很可能就只能得到乱七八糟的数据了,反之亦然。
任何数值,无论是何种表达方式,比如 5000,000,007
或者它的 16 进制格式 0x1DCD6507
,都可以看做是数字字符串。对于一个数字字符串,我们可以认为它有开始(最左),有结束(最右)。在英语中,第一个数字就是最高位数字,例如 5000,000,007
中的 5
实际上表示的是 500,000,000
。最后一位数字是最低位数字,例如 500,000,007
中的 7
对应的值是 7
。
当我们说到字节序的时候,我们是参照我们写数字时候的顺序。我们总是从高位开始写,然后是次高位,直到最低位,是不是这样啊?
在上面的例子中,数值 500,000,007
,对应 16 进制表示方式是 0x1DCD6507
,我们把它分成 4 个独立的字节:0x1D
, 0xDC
, 0x65
和 0x07
,对应 10 进制的值 29, 205, 101 和 7。最高位字节 29 表示 29 *256 * 256 * 256 = 486539264
,接下来是 205,表示 205 * 256 * 256 = 13434880
,然后是 101,表示 101 * 256 = 25856
,最后一个 7 就是 7 * 1 = 7
。它们的值:
486539264 + 13434880 + 25856 + 7 = 500,000,007
当计算机在它的内存中存储这 4 个字节的时候,假设存储到内存的地址是 2056, 2057, 2058 和 2059。那么问题来了:到底在哪个内存地址上存储哪个字节呢?它可能是在地址 2056 存储 29, 2057 存储 205,2058 存储 101,2059 存储 7,就像你写下这个数字的顺序一样,我们称之为高位优先。但是,其他的计算机架构可能是在 2056 存储 7,2057 存储 101, 2058 存储 205, 2059 存储 29,这样的顺序我们称之为低位优先。
针对 2 个字节的以及 8 个字节的存储方式,也是同样的。最高位字节称为 MSB,最低位字节称为 LSB。
好吧,那么我为什么要关心字节序的问题?
这个视情况而定了。通常情况下你不需要关心这个问题。无论你在什么平台运行 Java 程序,它的字节序都是一样的,所以你就无需关心字节序的问题。
但是,当你要处理其他语言产生的数据呢?那么,字节序就是一个大问题了。你必须得保证你按照数据被编码的顺序来进行解码,反之亦然。如果你足够幸运,通常在 API 或者协议规范、文件格式说明中找到关于字节序的说明。如果不巧……祝你好运吧!
最重要的是,你需要清晰的了解你所使用的字节序是什么样的以及你需要处理的数据的字节序是什么样的。如果二者不同,你需要进行额外的处理来保证正确性。还有就是,如果你需要处理无符号数值,你需要确保将正确的字节放到对应 integer/short/long
类型的正确位置。
网络字节序又是什么?
当设计 IP 协议的时候,高位优先字节序被设计为网络字节序。在 IP 报文中德数值类型都是按照网络字节序存储的。产生报文的计算机所使用的字节序称为“宿主机字节序”,可能和网络字节序一样,也可能不一样。和网络字节序一样,Java 中的字节序是高位优先的。
为什么没有无符号类型?
为什么 Java 不提供无符号类型呢?好问题!我也常常觉得这个事情非常诡异,尤其是当时已经有很多网络协议都使用无符号类型了。在 1999 年,我在 Web 上也找了很久(那个时候 google 还没有这么棒),因为我总是觉得这事儿不应该是这样。直到有一天我采访 Java 发明者中的一位(是 Gosling 吗?不太记得了,要是我保存了当时的网页就好了),这位设计者说了一段话,大意是:“嘿!无符号类型把事情搞复杂了,没有人真正需要无符号类型,所以我们把它赶出去了”。
这里有一个页面,是记录了一次对 James Gosling 的采访,看看能否收到一些启发:
http://www.gotw.ca/publications/c_family_interview.htm
问:程序员经常讨论使用“简单语言”编程的优点和缺点。你怎么看待这个问题?你觉得 C/C++/Java 算是简单语言吗?
Ritchie: 略
Stroustrup:略
Gosling:作为一个语言设计者,我不太理解所谓的“简单”结束了是什么意思,我希望 Java 开发者把这个概念留在他自己脑海里就好啦。举例来说,按照那个定义,Java 不算是简单语言。实际上很多语言都会在极端案例下完蛋,那些极端案例是人们都不会理解的。你去问 C 语言开发人员关于无符号的问题,你很快就会发现没有几个 C 语言开发人员真正理解无符号类型到底发生了些什么,什么是无符号运算。这些事情让 C 语言变得复杂。我觉得 Java 语言是非常简单的。
另外,参考:
http://www.artima.com/weblogs/viewpost.jsp?thread=7555
Oak 往事……
by Heinz Kabutz
2003 年 7 月 15
为了丰富我对 Java 历史的了解,我开始研究 Sun 的网站,无意间发现了 Oak 0.2 的语言规范书。Oak 是 Java 语言最早使用的名称,这份文档算是现存的最古老的关于 Oak 的文档了。
……
无符号整数(3.1 节)
规范书说:“8 比特,16 比特,32 比特,64 比特的,这 4 种不同宽度的整数类型都是有符号的,除非在前面加上
unsigned
修饰符”。
在侧栏中又说:“无符号类型尚未实现;可能永远也不会实现了。” 好吧,就是这样了。
Oak 语言规范可以从 https://duke.dev.java.net/green/OakSpec0.2.ps 下载 PostScript 版本,或者从 http://www.me.umn.edu/~shivane/blogs/cafefeed/resources/14-jun-2007/OakSpec0.2.zip 下载压缩的 PDF 版本。