长表示与 Java 中正负零的双重表示



我想知道不同数值类型中正零和负零之间的差异。
我了解IEEE-754的浮点运算和双精度位表示,因此以下内容并不令人意外

double posz = 0.0;
double negz = -0.0;
System.out.println(Long.toBinaryString(Double.doubleToLongBits(posz)));
System.out.println(Long.toBinaryString(Double.doubleToLongBits(negz)));
// output
>>> 0
>>> 1000000000000000000000000000000000000000000000000000000000000000

让我感到惊讶并向我展示我对 java 中long类型的位表示一无所知的是,即使我向右移动(无符号>>>),那么正零和负零的二进制表示是相同的

long posz = 0L;
long negz = -0L;
for (int i = 63; i >= 0; i--) {
System.out.print((posz >>> i) & 1);
}
System.out.println();
for (int i = 63; i >= 0; i--) {
System.out.print((negz >>> i) & 1);
}
// output
>>> 0000000000000000000000000000000000000000000000000000000000000000
>>> 0000000000000000000000000000000000000000000000000000000000000000

所以我想知道当我写以下内容时,Java 从位表示中做了什么

long posz = 0L;
long negz = -0L;

编译器是否理解它们都是零并忽略符号(因此将 0 分配给符号位)还是这里有其他魔法?

还是这里有其他魔法?

是的,2的补语。

2的补语有点神奇。它实现了两个主要目标。在开始之前,让我们先先讨论一下负零的概念。

负零有点奇怪。它为什么存在?

负零实际上不是一回事。问任何一个数学家"嘿,那么负零是怎么回事?"他们只会困惑地看着你。这不是一个东西。在数学上,0 和 -0 是完全相同的。不仅"几乎相同",而且100%,完全,以各种可能的方式,相同。我们通常不希望我们的数字能够代表5.05.00- 因为这两者是完全 100% 相同的。如果你不认为一个价值体系应该浪费一些时间来区分5.05.00,那么想要将-0.0+0.0表示为不同实体的能力同样是奇怪的。

所以,一开始想要-0有点奇怪。所有的数字基元(longintshortbyte,我想char技术上也是数字)都不能表示这个数字。相反,long z = -0归结为:

  • 取常量"0"。
  • 将"否定"运算应用于此数字(-是一元运算符。就像2+5使系统计算元素 2 和 5 上的"加法"的二进制运算一样,-x使系统计算元素x上的"否定"的一元运算。将否定运算应用于 0 将生成 0。这与写作没有什么不同,比如说,int x = 5 + 0;.+0部分不做任何事情。-0面前的-什么也没做。与-0.0相反,它确实做了一些事情(让你得到负零,double值,而不是正零)。
  • 将此结果存储在z中(因此,只有 0)。

没有办法判断减号是否存在。它们都会导致所有零位,因此,计算机无法判断您是使用表达式-0还是使用+0初始化该变量。再次与double形成鲜明对比,正如您注意到的那样,它有点不同。

那么double为什么会有呢?

让我们来谈谈双打和IEEE-754数学的概念。

double需要 64 位。从基本的纯数学原理来看,双精度不能表示超过 2^64 个不同的可能值,你能够打破光速或制造1+1=3

然而,double旨在代表所有数字。0 到 1 之间的数字比 2^64 个选项多得多(事实上,0 到 1 之间存在无限数量的数字),而这只是 0 到 1。

所以,双打的实际运作方式是不同的。从整个数字行中选择几个小于 2^64 的数字。让我们称这些为祝福的数字

受祝福的数字分布不均。你离1越近,祝福的数字就越多。换句话说,当您远离 1 时,2 个祝福数字之间的距离会增加。例如,如果你从1e100(一个带有一百个零的1)开始,想找到下一个祝福的数字,这是相当不错的方法。它实际上高于1.0!-1e100+1实际上又是1e100,因为数学double的工作方式是,在你做完每一个数学运算后,最终结果被四舍五入到最接近的祝福数字

让我们试试吧!

double d = 1e100;
System.out.println(d);
System.out.println(d + 1);
// prints: 1.0E100
//         1.0E100

但这意味着..double实际上并不代表单个数字!!!任何给定的双精度表示的实际上是这个概念:

一个未知数,其值介于 [D - , D + ] 之间,其中 D 是接近此值表示的未知数的祝福数,并且是 D 与两侧下一个最近的祝福数之间距离的一半。

鉴于这通常非常小,这是"足够好"的。但这种怪异确实解释了为什么你真的,真的根本不想要任何double业务,如果准确性很重要(例如货币)。永远不要把它们存放在双打中!

鉴于此,-0.0代表什么?实际上不仅仅是0。它具体表示:一个未知数,其值位于[-,0]之间,其中0是实零(而this没有符号),并且Double.MIN_VALUE:可以用double表示的最小非零正数。

这就是为什么-0.0+0.0都存在:它们实际上是不同的概念。很少相关,但有时是相关的。与例如long5只是意味着5而不是"在 4.5 到 5.5 之间",因为多头从根本上不承认分数部分的存在。鉴于5只是意味着5,那么0只是意味着0并且首先没有负零这样的东西

现在我们得到 2 的补码

2的补充是一个很酷的系统。它有两个简洁的属性:

  • 它只有一个零。
  • 对于以下操作而言,将位序列视为按 2s 补码方式有符号还是无符号处理并不重要:加法、减法、递增、递减、零点检查。为实现这些操作而对位所做的修改是相同的。

大于、小于和除法确实很重要。

2 的补码是这样的:要否定一个数字,取所有位并翻转它们(即对位执行 NOT 操作)。然后,添加 1。

让我们试试吧!

int x = 5;
int y = -x;
for (int i = 31; i >= 0; i--) {
System.out.print((x >>> i) & 1);
}
System.out.println();
for (int i = 31; i >= 0; i--) {
System.out.print((y >>> i) & 1);
}
System.out.println();
// prints 00000000000000000000000000000101
//        11111111111111111111111111111011

如我们所见,应用了"翻转所有位并加 1"算法。

当然,2s补码是可逆的:如果你连续两次"翻转所有位并加1",你会得到相同的数字。

现在让我们试试-0. 0 是 32 个 0 位,然后将它们全部翻转,然后添加 1:

00000000000000000000000000000000
11111111111111111111111111111111 // flip all
100000000000000000000000000000000 // add 1
00000000000000000000000000000000 // that 1 fell off

而且因为整数只能存储 32 位,所以最后的"1"从末尾掉下来。我们又剩下零了。

现在让我们使用字节(更小),并尝试将 200 和 50 相加。

11001000 // 200 in binary
00110010 // 50 in binary
-------- +
11111010 // 250 in binary.

现在让我们去吧:哦,等等,哎呀,这是一个错误,实际上这些数字是 2s 补码。那不是200,诺诺。11001000 是一个位序列,实际上意味着(让我们应用"翻转所有位,添加 1"方案:00111000 - 它实际上是 -56。因此,该操作旨在表示"-56 + 50"。这是 -6。二进制中的 -6 是(写出 6,翻转位,加 1):

00000110
11111001
11111010

嘿,现在,看看那个,什么都没有改变!结果是一样的!所以,当计算机做x + y,其中x和y是数字时,计算机并不关心。无论 x 是"无符号数"还是"带 2s 补码数的有符号数",操作都是相同的。

这就是应用2s补码的原因。它使数学速度更快。CPU 不必为处理符号位而烦恼分支。

从这个意义上说,更正确的说法是,在java中,intlongcharbyteshort既不是有符号的,也不是无符号的,它们只是有符号。至少出于 +、-、++ 和--.的目的不,int签名的想法基本上是一种属性,例如System.out.println(int)- 该方法选择将位序列11111111111111111111111111111111呈现为"-1"而不是4294967296。

long没有负零这样的东西。 只有floatdouble具有不同的正零和负零表示形式。

最新更新