为什么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个回答

6

我听说过一些故事,说它们应该被包含在原始的Java发布中。Oak是Java的前身,在一些规范文档中提到了无符号值。不幸的是,这些从未出现在Java语言中。据大家所知,它们只是由于时间限制而没有被实现。


这本来是没问题的……但根据Gosling采访的证据,无符号整数(除了char)被遗漏掉,是因为设计者认为它们与语言目标不相符,是个坏主意。 - Stephen C
如果有文件证据在手,最好不要过分相信目击证人的陈述。 - user7610

1

在“C”规范中有一些宝石,由于实用原因而被Java放弃,但随着开发者的需求,这些宝石正在慢慢回归(如闭包等)。

我提到第一个是因为它与本讨论相关;指针值对无符号整数算术的遵循性。并且,与此主题相关的是,在Java的有符号世界中维护无符号语义的困难。

我猜想,如果有人请Dennis Ritchie的替身来建议Gosling的设计团队,它会建议给Signed一个“无限的零”,这样所有地址偏移请求都会首先添加它们的代数环大小以消除负值。

这样,任何抛出到数组的偏移量都不会产生SEGFAULT。例如,在我称之为RingArray的双精度封装类中,需要无符号行为-在“自旋转循环”上下文中:

// ...
// 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永远不会从负索引中“获取”,即使恶意请求者试图这样做。请记住,还有许多合法的请求要求先前(负)索引值。
注:外部%模数解除合法请求,而内部%模数掩盖了比-模数更负面的明显恶意。如果这将出现在Java +..+9 || 8+..+规范中,那么问题将真正成为一个“无法“自旋”故障的程序员”。
我相信所谓的Java无符号int“缺陷”可以通过上述一行代码来弥补。
附言:为了给上述RingArray清理工作提供背景,这里是一个候选的“设置”操作,以匹配上述的“获取”元素操作:
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
}

1
“Closures”在哪个“C规范”中定义? - Remember Monica
抱歉我的速记太神秘了,而且我不幸选择了错误的例子。但你是对的,闭包作为内置功能随着C的面向对象后代而来;而不是C。C允许那些关心的人自行构建他们自己的“闭包”,因为C程序函数和内存地址命名空间可以从任何上下文中访问。我只是给出了一个“C”的例子,说明如何控制对动态内存的恶意引用,其中符号可以是中性的,并且对其进行的偏移量的行为类似于代数环。例如,始终模sizeof。 - MKhomo

0

因为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 ...
}

你注意到这个 bug 了吗?我承认只有在使用调试器后才看到它。

因为 n 是无符号类型的 size_t,整个表达式 n - (rays.size() - 1) / 2 的计算结果是 unsigned。该表达式旨在成为从中间射线开始的第 n 条射线的有符号位置:左侧第一条射线的位置为-1,右侧第一条射线的位置为+1,以此类推。取绝对值并乘以 delta 角度后,我将得到第 n 条射线与中间射线之间的角度。

不幸的是,上述表达式包含了邪恶的无符号数,而不是像-1这样的值,它的计算结果是 2^32-1。随后的转换为 double 密封了这个 bug。

由于滥用unsigned算术操作导致了一两个错误,我们不得不开始思考是否值得这多出来的一位所带来的额外麻烦。我正在尽可能地避免在算术运算中使用unsigned类型,尽管仍然会在非算术操作(如二进制掩码)中使用它。


3
我不喜欢这个答案,因为它使用了“无符号整数是邪恶的,不应该存在,因为它们永远不能被标记为有符号”的论点。任何试图从无符号整数中减去另一个数的人都应该已经知道这一点了。至于可读性,C 也不是以易于阅读而闻名的语言。此外,“额外的位不值得额外的麻烦”这个(半)论点也非常薄弱。相比之下,使用错误处理而不是exit(1)真的“值得额外的麻烦”吗?不能打开大文件真的值得保证不熟悉 Java 的程序员不会在使用“unsigned”时出错吗? - yyny
2
我在这段代码中唯一看到的问题是 n - (rays.size() - 1) / 2。你应该总是给二元运算符加上括号,因为代码的读者不应该假设计算机程序中的操作顺序。即使我们惯例上说 a+bc = a+(bc),但这并不意味着在阅读代码时可以假设这一点。此外,计算应该在循环之外定义,以便可以在没有循环的情况下进行测试。这是一个类型不匹配的错误,而不是无符号整数的问题。在 C 中,你需要确保你的类型匹配。 - Dmytro
@Dmitry:这是其中之一:class Point { float x, y; public: virtual ~Point() {} }; 你能发现商业软件使用该类时为什么会比具有完全相同功能的竞争对手多用2倍的内存吗?没错:将 Point::~Point() 设为虚函数使其更符合面向对象编程(在那种情况下是不必要的),但这也为8字节的结构体添加了额外的8个字节(这对于内存占用来说至关重要)。 - Michael
@Michael,我不确定你在说什么。我不是在谈论结构;我是在谈论你在单个公式中使用不同的非显式类型,并期望得到正确的结果。如果您添加了完全不同类型的三个变量,则不能惊讶结果毫无意义,您必须首先将它们放入相同的类型空间(例如双精度)。不要对三个不同类型的变量执行二进制(a-> a-> a)操作,首先使它们的类型相同。即使语言进行强制转换,您也希望控制此强制转换。 - Dmytro
@Michael,我不确定你说的让析构函数变成虚函数更符合面向对象(OOP)的意思是什么。OOP 不是关于子类化,而是通过在源代码依赖之间策略性地放置接口来控制依赖关系的方向,使源代码依赖成为系统内部的插件而不是紧耦合到系统中。只有当您期望该类被子类化时才需要虚拟析构函数,而 Point 类没有必要被任何合理的系统子类化,但如果您希望可以委派它。 - Dmytro
显示剩余11条评论

0
作为一个处理无符号算术的人,我可以向您保证,在Java中真的没有必要使用无符号数字。
以C语言为例。让我们写一些类似于:
unsigned int num = -7;
printf("%d", num);

你能猜出打印的内容吗?

-7

哇!一个无符号整数是负数!确实如此。真正的正整数并不存在。无符号整数只是一个n字节(取决于C语言架构)的值,它不为符号分配MSB。它不检查所分配或读取的数字的实际符号。


1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community
假设我正在接收一个无符号8位整数值(来自网络或磁盘)。这个值自然会在范围[0,255]内。但是我不能使用Java的byte类型来保存这个值;我必须使用更高级别的类型short。要保存一个无符号64位整数,我必须使用BigInteger?呸! - Harry
如果您需要保存8位整数值,可以使用char数据类型。虽然听起来有些愚蠢,但是char允许您存储0到255范围内的数字。不过,打印可能会有问题。您还可以使用byte通过一些技巧来存储0到255。由于Java本身就是面向对象的,因此在其中使用一些对象魔法也没有问题。 - Thorfinn

0

你的问题是“为什么Java不支持无符号整数”?

我的回答是,Java希望它所有的原始类型:byte、char、short、int和long都应该像汇编一样被视为byte、word、dword和qword,而Java运算符是对所有原始类型进行有符号操作,除了char之外,但只有在char上它们才是无符号的16位。

因此,静态方法应该是32位和64位的无符号操作。

你需要一个最终类,它的静态方法可以用于无符号操作。

你可以创建这个最终类,给它起任何你想要的名字,并实现它的静态方法。

如果你不知道如何实现静态方法,那么这个link可能会帮助你。

在我看来,Java与C++完全不同,如果它既不支持无符号类型也不支持运算符重载,因此我认为Java应该被视为与C++和C完全不同的语言。
顺便说一下,它们的名称也完全不同。
因此,我不建议在Java中输入类似于C的代码,也不建议完全输入类似于C++的代码,因为在Java中,您将无法像在C++中那样继续执行下一个操作,即代码将完全不像C++,对我来说这是不好的,改变风格。
我建议编写和使用静态方法来进行有符号操作,这样您就不会在代码中看到混合使用操作符和静态方法进行有符号和无符号操作,除非您只需要在代码中使用有符号操作,而且仅使用操作符也可以。

我建议避免使用 shortintlong 原始类型,而是分别使用 worddwordqword,并调用无符号操作和/或有符号操作的静态方法,而不是使用运算符。

如果你只进行有符号操作,并且在代码中仅使用运算符,则可以使用这些原始类型 shortintlong

实际上,worddwordqword 在语言中并不存在,但你可以为每个创建新的类,它们的实现应该非常容易:

word仅包含原始类型short,类dword仅包含原始类型int,类qword仅包含原始类型long。现在,所有无符号和有符号的方法都可以作为静态或非静态方法实现在每个类中,即通过在word类上赋予有意义的名称来实现所有16位操作(无符号和有符号),通过在dword类上赋予有意义的名称来实现所有32位操作(无符号和有符号),通过在qword类上赋予有意义的名称来实现所有64位操作(无符号和有符号)。

如果您不喜欢为每个方法提供太多不同的名称,则可以在Java中使用重载,很高兴看到Java没有删除这个功能!

如果您想要8位有符号操作的方法而不是运算符,并且没有任何运算符的8位无符号操作的方法,则可以创建Byte类(请注意,第一个字母'B'是大写的,因此这不是原始类型byte),并在该类中实现这些方法。

关于按值传递和按引用传递:
如果我没记错的话,就像在C#中一样,原始对象自然地按值传递,但类对象自然地按引用传递,这意味着类型为Byteworddwordqword的对象将默认按引用而不是按值传递。我希望Java也有像C#一样的struct对象,这样所有的Byteworddwordqword都可以被实现为struct而不是class,这样它们就可以默认按值而不是按引用传递,就像C#中的任何结构体对象(如原始类型)默认按值而不是按引用传递一样。但因为Java比C#差,我们必须处理这个问题,所以只有类和接口,默认情况下按引用而不是按值传递。因此,如果您想像Java和C#中的任何其他类对象一样按值而不是按引用传递Byteworddwordqword对象,您只需使用复制构造函数即可。

那是我能想到的唯一解决方案。我只希望我可以将原始类型typedef为word、dword和qword,但Java不支持typedef或者using,不像C#支持using,这与C的typedef等效。

关于输出:

对于相同的比特序列,你可以用很多种方式来打印它们:二进制、十进制(例如C printf中%u的含义)、八进制(例如C printf中%o的含义)、十六进制(例如C printf中%x的含义)和整数(例如C printf中%d的含义)。

注意,C printf不知道传递给函数作为参数的变量的类型,因此printf仅从传递给函数第一个参数的char *对象中知道每个变量的类型。

因此,在每个类中:Byteworddwordqword,您都可以实现打印方法并获得printf的功能,即使类的基本类型是有符号的,您仍然可以按照某些涉及逻辑和移位操作的算法将其打印为无符号数以获取要打印到输出的数字。

很遗憾,我给你的链接并没有展示如何实现这些打印方法,但我相信你可以通过谷歌搜索需要实现这些打印方法的算法。

这就是我能回答你问题和建议的全部了。


2
MASM(Microsoft汇编器)和Windows定义BYTE,WORD,DWORD,QWORD为无符号类型。对于MASM,SBYTE,SWORD,SDWORD,SQWORD是有符号类型。 - rcgldr
2
这个回答读起来像是一锅杂烩,没有任何意义。首先,为什么你要把“word”和“dword”这样的术语带入到Java的讨论中呢? - Nayuki

-2

我能想到一个不幸的副作用。在Java嵌入式数据库中,使用32位ID字段可以拥有的ID数量为2^31,而不是2^32(约20亿,而不是约40亿)。


1
他可能在考虑数组,不能使用负整数作为索引。很有可能。 - Ken
2
当数据库中的自增字段溢出时,它们经常会变得不稳定。 - Joshua

-9

我认为的原因是因为他们太懒了,不想去实现/修正那个错误。 暗示C/C++程序员不理解无符号、结构体、联合体、位标志...这简直荒谬。

要么你正在与一个基本的/bash/java程序员交谈,即将开始学习C语言,没有任何真正的知识,要么你只是在胡说八道。 ;)

当你每天处理来自文件或硬件的格式时,你会开始质疑,他们到底在想什么。

一个很好的例子是尝试使用无符号字节作为自旋转循环。 对于那些不理解最后一句话的人,你怎么能称自己为程序员呢。

DC


36
为了好玩,用谷歌搜索“自旋转环”这个词组。显然,丹尼斯·科(Denis Co)是全世界唯一一个称得上自己是程序员的人 :-) - Stephen C
10
这个回答太糟糕了,以至于让人感到有趣。 - Nayuki

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