为什么 Java 不包含对无符号整数的支持?
在我看来,这是一个奇怪的遗漏,因为它们允许人们编写不太可能在意外大输入时产生溢出的代码。
此外,使用无符号整数可以是一种自我证明的形式,因为它们表明无符号整数打算保存的值永远不会是负数。
最后,在某些情况下,无符号整数对于某些操作(例如除法)可能更有效。
包含这些有什么缺点?
byte
无法提供直接的 140
灰度级但需要 & 0xff
才能获得正确值的 -116
进行任何图像处理变得更加困难。
这是来自 interview with Gosling and others,关于简单性:
Gosling:作为一名语言设计师,我现在并不认为自己是这样的,“简单”最终的真正含义是我能否期望 J. Random Developer 将规范牢牢记在脑海中。该定义表明,例如,Java 不是——事实上,这些语言中的许多最终都会出现很多极端情况,即没有人真正理解的事情。向任何 C 开发人员询问有关无符号的问题,很快你就会发现几乎没有 C 开发人员真正了解无符号是怎么回事,什么是无符号算术。这样的事情使 C 变得复杂。我认为 Java 的语言部分非常简单。您必须查找的库。
在字里行间阅读,我认为逻辑是这样的:
一般来说,Java 设计者希望简化可用数据类型的全部内容
对于日常目的,他们认为最常见的需求是签名数据类型
为了实现某些算法,有时需要无符号算术,但是将实现此类算法的程序员也将具备“工作”的知识,以使用有符号数据类型进行无符号算术
大多数情况下,我会说这是一个合理的决定。可能,我会:
使字节无符号,或者至少为这种数据类型提供了可能具有不同名称的有符号/无符号替代方案(使其有符号有利于一致性,但是您何时需要有符号字节?)
取消了“short”(你最后一次使用 16 位有符号算术是什么时候?)
尽管如此,对不超过 32 位的无符号值的操作还算不错,而且大多数人不需要无符号的 64 位除法或比较。
short
的使用频率 - defltate/gzip/inflate 算法是 16 位的,并且它们严重依赖短裤......或者至少 short[]
[诚然它们是原生的 - 但算法的 java impl 携带 TB数据]。后者 (short[]
) 比 int[]
具有显着优势,因为它占用更少的内存和更少的内存 = 更好的缓存属性,更好的性能。
这是一个较老的问题,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--;
是的,没有对无符号整数的直接支持(显然,如果有直接支持,我不必将大部分操作转换回 char )。但是,肯定存在无符号原始数据类型。我也希望看到一个无符号字节,但我想将内存成本加倍,而使用 char 是一个可行的选择。
编辑
对于 JDK8,有用于 Long
和 Integer
的新 API,它们在将 long
和 int
值视为无符号值时提供帮助方法。
比较无符号
除无符号
parseUnsignedInt
parseUnsignedLong
余数无符号
toUnsignedLong
toUnsignedString
此外,Guava 提供了许多帮助方法来对整数类型执行类似的操作,这有助于缩小因缺乏对 unsigned
整数的本机支持而留下的差距。
char
太小而无法支持 long
算术。
Java 确实有无符号类型,或者至少有一种: char 是无符号短类型。所以无论高斯林抛出什么借口,这实际上只是他的无知,为什么没有其他无符号类型。
也短类型:短裤一直用于多媒体。原因是您可以在单个 32 位无符号长整数中拟合 2 个样本并矢量化许多操作。 8 位数据和无符号字节也是如此。您可以将 4 或 8 个样本放入寄存器中以进行矢量化。
char
用于除字符之外的任何内容都是不好的风格。
一旦有符号和无符号整数在表达式中混合,事情就会开始变得混乱,您可能会丢失信息。将 Java 限制为已签名的整数只会真正解决问题。我很高兴我不必担心整个已签名/未签名的业务,尽管我有时确实会错过一个字节中的第 8 位。
static_cast
来混合它们。确实很乱。
byte
进行签名。
& 0xFF
执行每个字节到整数的提升会使代码更加混乱。
http://skeletoncoder.blogspot.com/2006/09/java-tutorials-why-no-unsigned.html
这家伙说,因为 C 标准定义了涉及无符号和有符号整数的操作被视为无符号。这可能会导致负符号整数滚动到一个大的无符号整数,从而可能导致错误。
-1
)与任何无符号量(甚至零)进行比较都可能会遇到问题。
-1
作为“未知”年龄(如文章所示)是“代码异味”的经典示例之一。例如,如果您想计算“Alice 比 Bob 大多少?”,并且 A=25 和 B=-1,您将得到 ±26
的答案,这完全是错误的。当 Some(25) - None
将返回 None
时,对未知值的正确处理是某种 Option<TArg>
。
我认为 Java 本身就很好,添加 unsigned 会使它复杂化而没有太多收获。即使使用简化的整数模型,大多数 Java 程序员也不知道基本数字类型的行为方式 - 只需阅读本书 Java Puzzlers 以了解您可能持有哪些误解。
至于实用建议:
如果您的值有些随意大小并且不适合 int,请使用 long。如果它们不适合长期使用 BigInteger。
当您需要节省空间时,仅对数组使用较小的类型。
如果您恰好需要 64/32/16/8 位,请使用 long/int/short/byte 并停止担心符号位,除法、比较、右移和强制转换除外。
另请参阅有关“将随机数生成器从 C 移植到 Java”的 this 答案。
>>
和 >>>
之间选择有符号和无符号。左移没有问题。
>>>
不适用于 short
和 byte
。例如,(byte)0xff>>>1
产生 0x7fffffff
而不是 0x7f
。另一个示例:byte b=(byte)0xff; b>>>=1;
将导致 b==(byte)0xff
。当然,您可以执行 b=(byte)(b & 0xff >> 1);
,但这会增加一项操作(按位 &)。
我知道这篇文章太旧了;但是为了您的兴趣,在 Java 8 及更高版本中,您可以使用 int
数据类型来表示一个无符号的 32 位整数,它的最小值为 0,最大值为 232 -1。使用 Integer
类将 int
数据类型用作无符号整数,compareUnsigned()
、divideUnsigned()
等静态方法已添加到 Integer
类以支持无符号整数的算术运算。
使用 JDK8,它确实对它们有一些支持。
尽管 Gosling 担心,但我们可能会看到 Java 完全支持无符号类型。
我听说它们将被包含在原始 Java 版本附近。 Oak 是 Java 的前身,在一些规范文档中提到了 usigned 值。不幸的是,这些从未进入 Java 语言。据任何人都能够弄清楚他们只是没有得到实施,可能是由于时间限制。
char
)被排除在外,因为设计者认为它们是一个坏主意......考虑到语言的目标。
我曾经和 C++ 标准委员会的某个人一起上过 C++ 课程,他暗示 Java 做出了正确的决定来避免使用无符号整数,因为 (1) 大多数使用无符号整数的程序在使用有符号整数时也能做得很好,这在就人们的思维方式而言,以及 (2) 使用无符号整数会导致许多易于创建但难以调试的问题,例如整数算术溢出和在有符号和无符号类型之间转换时丢失重要位。如果你错误地使用有符号整数从 0 中减去 1,它通常会更快地导致你的程序崩溃,并且比如果它环绕到 2^32 - 1 更容易找到错误,并且编译器和静态分析工具和运行时检查必须假设您知道自己在做什么,因为您选择使用无符号算术。此外,像 -1 这样的负数通常可以表示一些有用的东西,比如一个被忽略/默认/取消设置的字段,而如果你使用 unsigned 你必须保留一个特殊的值,比如 2^32 - 1 或类似的东西。
很久以前,当内存有限且处理器不能一次自动在 64 位上运行时,每一位都更重要,因此有符号字节与无符号字节或短路实际上更重要,显然是正确的设计决策。今天,在几乎所有常规编程情况下,仅使用带符号的 int 就绰绰有余了,如果您的程序确实需要使用大于 2^31 - 1 的值,那么您通常只需要一个 long 值。一旦你进入了使用 long 的领域,就更难想出一个你真的无法使用 2^63 - 1 个正整数的原因。每当我们使用 128 位处理器时,问题就更小了。
您的问题是“为什么 Java 不支持无符号整数”?
我对你的问题的回答是 Java 希望它的所有原始类型:byte、char、short、int 和 long 应该分别被视为 byte、word、dword 和 qword,就像在汇编中一样,并且 Java 运算符是有符号的对除 char 之外的所有原始类型的操作,但仅在 char 上,它们仅是无符号的 16 位。
所以静态方法也假设是 32 位和 64 位的无符号操作。
您需要最终类,可以为无符号操作调用其静态方法。
您可以创建这个最终类,将其命名为您想要的任何名称并实现它的静态方法。
如果您不知道如何实现静态方法,那么此 link 可能会对您有所帮助。
在我看来,如果 Java 既不支持无符号类型也不支持运算符重载,Java 与 C++ 一点也不相似,所以我认为 Java 应该被视为与 C++ 和 C 完全不同的语言。
顺便说一句,语言的名称也完全不同。
所以我不建议在 Java 中键入类似于 C 的代码,我根本不建议键入类似于 C++ 的代码,因为那样在 Java 中你将无法在 C++ 中做你接下来想做的事情,即代码不会继续像 C++ 一样,对我来说,这样的代码是不好的,改变中间的样式。
我建议也为签名操作编写和使用静态方法,因此您不会在代码中看到用于签名和未签名操作的运算符和静态方法的混合,除非您只需要代码中的签名操作,并且可以仅使用运算符。
此外,我建议避免使用 short、int 和 long 原始类型,而分别使用 word、dword 和 qword,并且您要为无符号操作和/或有符号操作调用静态方法,而不是使用运算符。
如果您打算只进行有符号操作并且只在代码中使用运算符,那么可以使用这些原始类型 short、int 和 long。
实际上 word、dword 和 qword 在语言中不存在,但是您可以为每个创建新类,并且每个的实现应该非常容易:
类 word 仅包含原始类型 short,类 dword 仅包含原始类型 int,类 qword 仅包含原始类型 long。现在所有的无符号和有符号方法都是静态的或者不是你的选择,你可以在每个类中实现,即所有的 16 位操作都是无符号的和有符号的,通过在单词类上给出含义名称,所有的 32 位操作都是无符号的和通过在 dword 类上给出含义名称进行签名,所有 64 位操作都通过在 qword 类上给出含义名称来进行无符号和有符号操作。
如果您不喜欢为每个方法提供太多不同的名称,您总是可以在 Java 中使用重载,很高兴看到 Java 也没有删除它!
如果您想要方法而不是用于 8 位有符号操作的操作符和用于完全没有操作符的 8 位无符号操作的方法,那么您可以创建 Byte 类(请注意,第一个字母 'B' 是大写的,所以这不是原始类型字节)并实现此类中的方法。
关于值传递和引用传递:
如果我没记错的话,就像在 C# 中一样,原始对象自然是按值传递的,而类对象自然是按引用传递的,这意味着 Byte、word、dword 和 qword 类型的对象将按引用而不是按值传递默认。我希望 Java 有 C# 的 struct 对象,所以所有 Byte、word、dword 和 qword 都可以实现为 struct 而不是类,所以默认情况下它们是按值传递而不是按引用传递,就像 C# 中的任何 struct 对象一样和原始类型一样,默认情况下是按值传递而不是按引用传递,但是因为 Java 比 C# 差,我们必须处理它,所以只有类和接口,它们通过引用而不是按值传递默认。因此,如果您想通过值而不是通过引用传递 Byte、word、dword 和 qword 对象,就像 Java 和 C# 中的任何其他类对象一样,您将不得不简单地使用复制构造函数,仅此而已。
这是我能想到的唯一解决方案。我只是希望我可以将原始类型 typedef 到 word、dword 和 qword,但是 Java 既不支持 typedef 也不支持 using,这与支持 using 的 C# 不同,这相当于 C 的 typedef。
关于输出:
对于相同的位序列,您可以通过多种方式打印它们:二进制、十进制(如 C printf 中 %u 的含义)、八进制(如 C printf 中 %o 的含义)、十六进制(如C printf 中 %x 的含义)和整数(类似于 C printf 中 %d 的含义)。
请注意,C printf 不知道作为参数传递给函数的变量的类型,因此 printf 仅从传递给函数的第一个参数的 char* 对象中知道每个变量的类型。
所以在每个类中:Byte、word、dword 和 qword,您可以实现 print 方法并获得 printf 的功能,即使该类的原始类型是有符号的,您仍然可以通过遵循一些涉及的算法将其打印为无符号逻辑和移位操作以获取要打印到输出的数字。
不幸的是,我给您的链接没有显示如何实现这些打印方法,但我相信您可以通过谷歌搜索实现这些打印方法所需的算法。
这就是我能回答你的问题并建议你的全部内容。
因为 unsigned
类型是纯粹的邪恶。
在 C 中 unsigned - int
产生 unsigned
的事实更加邪恶。
这是不止一次让我着迷的问题的快照:
// We have odd positive number of rays,
// consecutive ones at angle delta from each other.
assert( rays.size() > 0 && rays.size() % 2 == 1 );
// Get a set of ray at delta angle between them.
for( size_t n = 0; n < rays.size(); ++n )
{
// Compute the angle between nth ray and the middle one.
// The index of the middle one is (rays.size() - 1) / 2,
// the rays are evenly spaced at angle delta, therefore
// the magnitude of the angle between nth ray and the
// middle one is:
double angle = delta * fabs( n - (rays.size() - 1) / 2 );
// Do something else ...
}
你注意到这个错误了吗?我承认我只是在使用调试器后才看到它。
因为 n
是无符号类型 size_t
,所以整个表达式 n - (rays.size() - 1) / 2
的计算结果为 unsigned
。该表达式旨在成为n
中间光线的 有符号 位置:左侧中间光线的第一条光线的位置为 -1,第一条光线在左侧右边的位置 +1 等。在取 abs 值并乘以 delta
角度后,我将得到 n
光线和中间光线之间的角度。
对我来说不幸的是,上面的表达式包含邪恶的无符号,而不是评估为 -1,而是评估为 2^32-1。随后转换为 double
密封了该错误。
在因滥用 unsigned
算术而导致的一两个错误之后,人们不得不开始怀疑获得的额外位是否值得额外的麻烦。我正在尽可能地避免在算术中使用任何 unsigned
类型,尽管仍然将它用于非算术运算,例如二进制掩码。
unsigned
在每次操作中都转换为 int
,那么 unsigned
有什么用?它没有任何与 short
不同的功能。而如果你只在混合操作上转换成int
,例如unsigned+int
或unsigned+float
,那么你仍然会遇到((unsigned)25-(unsigned)30)*1.0 > 0
的问题,这是与unsigned
相关的错误的主要原因。
exit(1);
真的“值得额外的麻烦”吗?不能打开大文件真的值得缺乏经验的 Java 程序员使用 unsigned
不会搞砸的安全性吗?
n - (rays.size() - 1) / 2
。您应该始终将二元运算符括起来,因为代码的读者不需要对计算机程序中的操作顺序做出任何假设。仅仅因为我们通常说 a+bc = a+(bc) 并不意味着您可以在阅读代码时假设这一点。此外,计算应该在循环之外定义,以便可以在没有循环的情况下对其进行测试。这是一个错误,无法确保您的类型对齐,而不是无符号整数的问题。在 C 中,您可以确保您的类型对齐。
Java 出于务实的原因放弃了“C”规范中的一些宝石,但随着开发人员的需求(闭包等),它们正在慢慢回升。
我提到第一个是因为它与这个讨论有关;指针值对无符号整数算术的遵守。并且,关于这个线程主题,在 Java 的签名世界中维护无符号语义的困难。
我猜如果有人要让 Dennis Ritchie 改变自我来建议 Gosling 的设计团队,它会建议给 Signed 一个“无穷处为零”,这样所有地址偏移请求都会首先添加它们的 ALGEBRAIC RING SIZE 以避免负值。
这样,在数组中抛出的任何偏移量都不会生成 SEGFAULT。例如,在一个封装类中,我称之为需要无符号行为的双精度环数组 - 在“自旋转循环”上下文中:
// ...
// Housekeeping state variable
long entrycount; // A sequence number
int cycle; // Number of loops cycled
int size; // Active size of the array because size<modulus during cycle 0
int modulus; // Maximal size of the array
// Ring state variables
private int head; // The 'head' of the Ring
private int tail; // The ring iterator 'cursor'
// tail may get the current cursor position
// and head gets the old tail value
// there are other semantic variations possible
// The Array state variable
double [] darray; // The array of doubles
// somewhere in constructor
public RingArray(int modulus) {
super();
this.modulus = modulus;
tail = head = cycle = 0;
darray = new double[modulus];
// ...
}
// ...
double getElementAt(int offset){
return darray[(tail+modulus+offset%modulus)%modulus];
}
// remember, the above is treating steady-state where size==modulus
// ...
上面的 RingArray 永远不会从负索引中“获取”,即使恶意请求者试图这样做。请记住,还有许多合法请求要求提供先前(负)索引值。
注意:外部 %modulus 取消引用合法请求,而内部 %modulus 从比 -modulus 更负面的负面中掩盖了公然的恶意。如果这曾经出现在 Java +..+9 || 8+..+ 规范,那么问题将真正成为“无法“自转”故障的程序员”。
我确信所谓的Java unsigned int '缺陷'可以用上面的单线来弥补。
PS:只是为了给上面的 RingArray 管家提供上下文,这里有一个候选的 'set' 操作来匹配上面的 'get' 元素操作:
void addElement(long entrycount,double value){ // to be called only by the keeper of entrycount
this.entrycount= entrycount;
cycle = (int)entrycount/modulus;
if(cycle==0){ // start-up is when the ring is being populated the first time around
size = (int)entrycount; // during start-up, size is less than modulus so use modulo size arithmetic
tail = (int)entrycount%size; // during start-up
}
else {
size = modulus;
head = tail;
tail = (int)entrycount%modulus; // after start-up
}
darray[head] = value; // always overwrite old tail
}
我能想到一个不幸的副作用。在 Java 嵌入式数据库中,32 位 id 字段可以拥有的 id 数量是 2^31,而不是 2^32(约 20 亿,而不是约 40 亿)。
恕我直言的原因是因为他们/太懒惰而无法实施/纠正该错误。建议 C/C++ 程序员不懂无符号、结构、联合、位标志……简直是荒谬。
以太您正在与即将开始编写 la C 语言的基本/bash/java 程序员交谈,没有任何真正的语言知识,或者您只是在胡说八道。 ;)
当您每天处理来自文件或硬件的格式时,您开始质疑,他们到底在想什么。
一个很好的例子是尝试使用无符号字节作为自旋转循环。对于那些不理解最后一句话的人,你到底是如何称自己为程序员的。
直流