为什么Java不支持无符号整数?

406
为什么Java不支持无符号整数?
这对我来说似乎是一个奇怪的疏漏,因为它允许编写的代码更不容易在意外大输入时产生溢出。
此外,使用无符号整数可以作为自我说明的形式,因为它们表明无符号整数所表示的值不应该是负数。
最后,在某些情况下,无符号整数可以更有效地进行某些操作,例如除法。
包括无符号整数有何不利之处?

152
我不知道,但这让我非常烦恼;例如用这种方式编写网络代码会更加困难。 - Tamas Czinege
23
希望语言/数据库等领域只有两种类型:数字和字符串 :) - Liao
7
编写网络代码并不比其他代码更难。顺便提一下,例如InputStream.read()返回的是无符号字节(unsigned byte),而不是有符号字节(signed byte),因此在这个网络示例中存在混淆的可能性。只有当你认为编写有符号值和编写无符号值不同才会感到困惑,也就是说,如果你实际上对字节级别发生的事情不了解。 - Peter Lawrey
19
当我看到一位语言设计师发表这样的言论时,也感到惊讶。没有比无符号整数更简单的了。有符号整数是复杂的,特别是当你考虑到晶体管层面的位操作时。而且有符号整数如何进行移位? 我不得不得出结论:Java的设计者在理解布尔逻辑方面存在严重问题。 - PP.
8
对我来说,若图像的“字节”不能直接给出灰度级为“140”,而是给出了“-116”,就会让进行任何图像处理变得更加困难,你需要使用“& 0xff”操作来获取正确的值。 - Matthieu
显示剩余11条评论
17个回答

210

这是一篇关于简洁性的与Gosling和其他人的采访

Gosling:对我来说,作为一名语言设计师(虽然我现在并不认为自己是),“简单”真正意味着我是否可以期望J. Random Developer能够将规范记在脑中。这个定义表明,例如,Java并不是一个简单的语言——实际上,很多这样的语言最终都会有很多边角情况,没有人真正理解。询问任何C开发人员关于无符号数的问题,很快你就会发现几乎没有C开发人员真正了解无符号数以及无符号算术是什么。这些事情使得C变得复杂。Java的语言部分,我认为相当简单。你需要查找的是库。


245
我必须在这里与Gosling不同意,举一个具体的例子(来自CLR)。将有符号整数长度值或无符号长度值赋给数组更加令人困惑?数组不可能具有负长度,但我们的API表明这是可能的。 - JaredPar
21
Java 简化的论点是导致我们因缺乏模板而陷入麻烦的一部分,最终他们不得不将其引入语言中,因为其他替代方案太过繁琐。然而,我认为可以通过相应的类来支持无符号整数,而不需要基本类型。 - Uri
61
如果 Java 需要无符号整数是因为数组索引不能为负数,那么它也需要子范围(类似于 Pascal),因为数组索引不能大于数组大小。 - Wayne Conrad
91
好的,他刚刚说了不使用无符号类型的优点。现在让我们来数一数缺点... - Zippo
96
我更喜欢代码的简洁而不是语言的简单。这就是为什么我讨厌Java的原因。 - Pijusn
显示剩余22条评论

55

从字里行间看,我认为这个逻辑大概是这样的:

  • 通常,Java 设计师想要简化可用数据类型的库
  • 对于日常使用,他们认为最常见的需求是有符号数据类型
  • 对于实现某些算法,无符号算术有时是必要的,但实现这些算法的程序员也会有知识来“绕过”使用有符号数据类型执行无符号算术。

总的来说,我觉得这是个合理的决定。可能,我会:

  • 使 byte 无符号,或者至少为该数据类型提供有符号/无符号的替代方案,可能使用不同的名称(使它有符号对一致性有好处,但你何时需要有符号的 Byte?)
  • 取消 'short'(你上次使用 16 位有符号算术是什么时候?)

尽管需要一些折腾,但对于最多 32 位的无符号值的操作并不太糟糕,大多数人不需要 64 位无符号除法或比较。


76
对于日常使用,他们认为最常见的需求是有符号数据类型。在我的 C++ 代码中,我经常会想:“为什么我要在这里使用有符号整数而不是无符号整数?!”我有一种感觉,“有符号”是例外而不是规则(当然,这取决于领域,但正整数被称为“自然数”肯定是有原因的;-))。 - Luc Touraille
19
支持对未签名字节的调用,特别是在进行图像处理时,假设字节为无符号(应该是这样),让我花了几个小时进行调试。 - Helin Wang
10
你会惊讶于short的使用频率 - deflate/gzip/inflate算法使用16位数据,因此它们在很大程度上依赖于short... 或至少是short[]数组[尽管它们是本地化的,但Java实现的算法处理着以太字节的数据]. 相对于int[]而言,short[]具有显著的优势,因为它占用的内存空间只有一半,并且更少的内存=更好的缓存属性,表现更出色。 - bestsss
10
在特定的应用中,你应该测量使用shorts是否可以提供更好的性能,而不是假设它是正确的。在某些情况下,操作shorts需要额外的技巧,而不是整数(通常是处理器“喜欢使用”的类型),这可能实际上对特定应用的性能有害。并非总是如此,但你应该进行测试,而不是假设。 - Neil Coffey
2
说得没错,但目前为止,64位处理器仍然给32位宽度一定的“特权”地位。如果我们转向例如128位处理器,那么看到在那个时候32位是否开始变得更加过时将会很有趣。 - Neil Coffey
显示剩余9条评论

23

这是一个较旧的问题,Pat在简要提及char的同时,我认为我应该为其他将来查看此问题的人扩展一下。让我们仔细看一下Java原始类型:

byte - 8位有符号整数

short - 16位有符号整数

int - 32位有符号整数

long - 64位有符号整数

char - 16位字符(无符号整数)

虽然char不支持unsigned算术运算,但它基本上可以被视为一个unsigned整数。您需要显式地将算术操作强制转换回char,但它为您提供了一种指定unsigned数字的方式。

char a = 0;
char b = 6;
a += 1;
a = (char) (a * b);
a = (char) (a + b);
a = (char) (a - 16);
b = (char) (b % 3);
b = (char) (b / a);
//a = -1; // Generates complier error, must be cast to char
System.out.println(a); // Prints ? 
System.out.println((int) a); // Prints 65532
System.out.println((short) a); // Prints -4
short c = -4;
System.out.println((int) c); // Prints -4, notice the difference with char
a *= 2;
a -= 6;
a /= 3;
a %= 7;
a++;
a--;

显然,Java没有直接支持无符号整数(否则,如果有直接支持,我就不必将大部分操作强制转换为char类型)。但是,肯定存在一种无符号原始数据类型。我希望也能看到无符号字节,但我想双倍的内存成本和使用char代替是一个可行的选择。


编辑

JDK8提供了新的API用于longint值如同无符号值处理时提供帮助的LongInteger

  • compareUnsigned
  • divideUnsigned
  • parseUnsignedInt
  • parseUnsignedLong
  • remainderUnsigned
  • toUnsignedLong
  • toUnsignedString

此外,Guava 提供了许多辅助方法来执行类似于整数类型的操作,这有助于弥补缺乏本地支持的 unsigned整数的差距。


2
但是,char 太小了,无法支持 long 算术运算,例如。 - user586399
3
Java可能存在这样的劣势。 - user586399
希望它们支持字节的无符号值。这会让事情更容易。 - mixturez
2
当我尝试从硬盘中读取由C程序编写的C结构规格的数据时,我遇到了这个问题。我不仅被迫处理字节序差异,而且更加恶劣的是,要使用64位整数读取所有内容,然后应用移位操作以获得正确的数据,只因为有人决定不想实现无符号数字。如果他们可以说出像“为了保持Java简单”这样的话,那么我会说这是一个懒惰的理由。 - Dan Chase

17

Java确实有无符号类型,或者至少有一个:char是无符号的短整型。所以不管Gosling提出什么借口,为什么没有其他无符号类型,那都只是他无知的表现。

另外,短整型(Short)也经常用于多媒体中。原因是你可以在单个32位无符号长整型中放置2个样本,并向量化许多操作。对于8位数据和无符号字节,同理。你可以将4个或8个样本放入寄存器以进行向量化处理。


43
是的,我相信与您相比,高斯林在Java方面非常无知。 - jakeboxer
Java是否允许直接对无符号字节进行算术运算,还是值总是会被提升?使用无符号类型进行存储,但始终对足以容纳它的有符号类型执行算术运算在语义上效果很好,但会导致与“普通”整数大小相同的无符号类型的操作更加昂贵。 - supercat
3
在编程中,除了字符以外,使用“char”表示其他东西是不好的风格。 - starblue
7
当然是这样,但这只是一种绕过语言限制的技巧。 - Basic

15

一旦在表达式中混合使用有符号和无符号整数,情况就变得混乱了,您可能会丢失信息。将Java限制为仅使用有符号整数确实清理了事情。虽然我有时会想念字节中的第8位,但我很高兴不必担心整个有符号/无符号业务。


16
关于混合使用有符号/无符号类型:您可以使用无符号类型,但禁止混合使用(或要求显式转换)。不过,目前还不清楚是否必要。 - sleske
2
在 C++ 中,你必须大量使用 static_cast,这样做确实很混乱。 - Raedwald
4
第 8 位在那里,它只是试图隐藏自己作为符号。 - starblue
只有类型为32位或更大的时候才会变得混乱。我认为Java没有理由不像Pascal一样将“byte”设为有符号的。 - supercat
14
当你在使用Java进行图像处理时,遇到了希望将字节转为无符号的问题,请来找我。这时你会发现,对每个字节强制转换为整数后进行& 0xFF运算只会让代码变得更加混乱。 - bit2shift

13

37
Java有符号整数也会循环。我不明白你的观点。 - foo
10
@foo:有符号整数必须变得很大才会引起问题。相比之下,在 C 中,人们可能会在任何负整数(甚至是-1)与任何无符号数量(甚至是零)进行比较时出现问题。 - supercat
很遗憾Java没有包括无符号类型,但使用有限的转换和混合操作符(类似于在C语言中可以将5加到指针上,但不能将指针与5进行比较)的方式,这是无法改变的。当存在隐式转换时,在混合类型上使用运算符的想法应该强制使用该转换(并使用相应的类型作为结果类型),这是.NET和Java中很多可疑设计决策的核心。 - supercat
5
不想在你的回答上大发牢骚,但是把“-1”作为“未知”年龄(如文章所示)是“代码异味”的经典例子之一。例如,如果你想计算“爱丽丝比鲍勃大多少岁?”,而A=25和B=-1,你会得到一个错误的答案±26。对于未知值的正确处理是使用某种形式的Option<TArg>,其中 Some(25) - None将返回 None - Be Brave Be Like Ukraine

13

我认为Java目前的状态很好,添加无符号整数将会使它变得更复杂,收益并不大。即使采用简化的整数模型,大多数Java程序员也不知道基本数字类型的行为 - 只需阅读Java Puzzlers这本书,就能看到你可能持有的一些误解。

至于实际建议:

  • 如果你的值具有任意大小并且不适合int,请使用long。如果它们不适合long,请使用BigInteger

  • 仅在需要节省空间时才使用较小的类型来处理数组。

  • 如果你需要精确的64/32/16/8位,请使用long/int/short/byte,并停止担心符号位,除了除法、比较、右移和强制类型转换之外。

另请参阅关于“从C到Java移植随机数生成器”的答案。


5
对于右移操作,您需要分别选择带符号的 >> 和无符号的 >>>。左移操作没有问题。 - starblue
1
@starblue 实际上 >>> 对于 shortbyte 不起作用。例如,(byte)0xff>>>1 的结果是 0x7fffffff 而不是 0x7f。另一个例子:byte b=(byte)0xff; b>>>=1; 的结果将是 b==(byte)0xff。当然,你可以使用 b=(byte)(b & 0xff >> 1); 但这会增加一次操作(按位与)。 - CITBL
9
即使使用简化模型,大部分Java程序员也不清楚基本的数字类型的行为。对我来说,只是讨厌以最低公共分母为目标的语言。 - Basic
你回答中的开头一句话,关于更多的复杂性和少量的收益,恰好是我在6年后的文章中详细阐述的:https://www.nayuki.io/page/unsigned-int-considered-harmful-for-java - Nayuki
1
@Nayuki,你的文章真的很好。只有一个小建议,我会使用0x80000000的加法来代替XOR进行比较运算,因为这样可以解释它为什么有效,它将比较发生的连续区域从-MAXINT移动到0。在位运算方面,它的效果完全相同。 - starblue
@starblue 感谢夸奖。顺便提一下,我稍微修改了您的答案,以改善事实和措辞,希望您会喜欢。关于0x80000000,为什么要使用加法?XOR是一个更简单的操作,没有进位。此外,我在其他一些用于compareUnsigned()的代码中看到了等效的减法。至于解释,我没有详细解释任何其他低级运算符,所以为了保持一致性,我不能解释比较黑客技巧... - Nayuki

9

我知道这篇文章已经很老了,但是对于你的兴趣,从Java 8开始,你可以使用int数据类型来表示无符号32位整数,它的最小值为0,最大值为232−1。使用Integer类将int数据类型用作无符号整数,并且像compareUnsigned()divideUnsigned()等静态方法已被添加到Integer类中,以支持无符号整数的算术运算。


6

JDK8中,它确实支持了一些无符号类型。

尽管Gosling有所担忧,但我们可能会看到Java对无符号类型的完全支持。


13
人们确实在使用它,我们一开始没有将其包含进来是错误的。但我们仍然不太信任Java开发人员能否知道一个变量是否有符号,因此我们不会在虚拟机中实现它们,也不会将它们作为与有符号变量等价的类型。 - Basic

6
我曾经和C++标准委员会的某位人士一起上了一门C++课程,他暗示Java避免使用无符号整数是正确的决定,因为(1)大多数使用无符号整数的程序可以使用有符号整数同样好,并且这更符合人们的思考方式;(2)使用无符号整数会导致许多易于创建但难以调试的问题,例如整数算术溢出和在有符号和无符号类型之间转换时丢失重要位。如果您错误地使用有符号整数将1从0中减去,它通常更快地导致程序崩溃并使查找错误变得更加容易,而不是绕到2^32-1,编译器、静态分析工具和运行时检查必须假设您知道自己在做什么,因为您选择使用了无符号算术。此外,像-1这样的负数通常可以表示一些有用的东西,例如字段被忽略/默认/未设置,而如果您使用无符号,则必须保留类似于2^32-1或类似的特殊值。
很久以前,当内存有限且处理器不能自动一次操作64位时,每个位都更重要,因此有符号与无符号字节或shorts实际上更为重要,这显然是正确的设计决策。今天,在几乎所有常规编程情况下,仅使用有符号整数就已经足够了,如果您的程序确实需要使用大于2^31-1的值,则通常只需要一个长整型。一旦进入使用longs的领域,甚至更难找到您真正无法通过2^63-1个正整数来解决的原因。每当我们转向128位处理器时,这将变得不那么重要。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接