尽管我喜欢Java语言中的许多事物,但无符号类型的缺失一直让我感到困扰。
据Gosling所说,它们太复杂了
问任何C开发者关于无符号的,很快你就会发现,几乎没有任何C开发者真正理解无符号发生了什么,什么是无符号算术。类似的事情使C变得复杂。
讽刺的是,这种手把手的方式往往会引入其他更难处理的复杂性。在这种情况下,省略无符号类型并不能阻止处理无符号数据的需求。相反,它迫使开发人员以各种不寻常的方式绕过语言限制。
这个系统中第一个主要问题是Java中的byte是有符号的。在我的所有代码中,我只能想到少数几种需要有符号byte值的情况。在几乎所有情况下,我想要的都是无符号版本。
让我们看一个非常简单的例子,将byte初始化为0xFF(或255),以下将失败(注意:在C#中这可以工作,因为他们让byte无符号)
byte b = 0xFF;
Java不会为我们缩小这个值,因为它的值超出了有符号byte类型的范围(>127)。然而,我们可以使用强制类型转换来解决这个问题
byte b = (byte) 0xFF;
如果我们足够聪明,并且知道二进制补码,我们可以使用我们简单无符号值的负等效值
byte b = -1;
但这只是冰山一角。在搜索和压缩算法中常用的一个常见技术是,根据特定字节值的出现来预计算一个表。由于一个字节可以表示256个值,这通常使用一个以字节值为索引的数组来完成,这非常高效。所以你可能认为可以这样做
byte b = (byte) 0xFF; int table[] = new int[256]; table[b] = 1; // OOPS!
虽然这段代码在语法上可以编译通过,但会在运行时抛出异常。问题在于数组索引操作符需要一个整数。由于这里指定了一个字节类型,Java会将字节转换为整数,这会导致符号扩展。再次强调,对于有符号字节,0xFF 表示 -1,因此它会被转换为整数值 -1。当然,这是一个无效的数组索引。
为了解决这个问题,我们必须使用位与操作符来强制以正确(但不够直观)的方式转换,如下所示
table[b & 0xFF] = 1;
这种技术很快就会变得复杂。看看如何从4个字节组成一个int(唉!)
byte b1 = 1; byte b2 = (byte)0x82; byte b3 = (byte)0x83; byte b4 = (byte)0x84; int i = (b1 << 24) | ((b2 & 0xFF) << 16) | ((b3 & 0xFF) << 8) | (b4 & 0xFF);
这些问题反过来导致了奇特的API工作区。例如,看看InputStream.read(),根据其文档,它应该返回一个字节,但返回的是一个整数。为什么?这样它就可以为你执行 & 0xFF 操作。
我们还有DataOutput.writeShort()和DataOutput.writeByte(),它们接受整数而不是各自的数据类型。为什么?这样你就可以在网络上传送无符号值。在读取方面,我们得到了四个方法DataInput.readShort()、DataInput.readUnsignedShort()、DataInput.readByte()和DataInput.readUnsignedByte()。"无符号"版本返回转换后的整数而不是描述的类型名称。
更让人困惑的是,在这个只有有符号的混乱中,我们还有两个右移操作符。"无符号"右移操作符将类型视为无符号,而正常右移则保留符号(本质上相当于除以2)。如果我们想要获取整数的最高有效四位数,我们需要使用"无符号"版本。
int i = 0xF0000000; System.out.printf("%x\n", i >> 28); // Returns ffffffff! System.out.printf("%x\n", i >>> 28); // Returns f, as desired
所以我问大家,所有这些麻烦值得省略简单且易于理解的"无符号"关键字吗?我认为不是,我希望任何考虑在它们设计的另一种语言中做这件事的人都能从中学习。至少C#是这样做的。