首页 > 代码库 > 【java解惑】java中那些反常识的小知识
【java解惑】java中那些反常识的小知识
一、Q:请为 i == i + 1 ; 提供一个声明使得条件成立。
分析:一个数字永远不会等于它自己加 1对吧!如果这个数字是无穷大的又会怎样呢?Java 强制要求使用IEEE二进制浮点数算术标准IEEE 754,它可以让你用一个 double 或 float 来表示无穷大。无穷大加 1 还是无穷大。如果 i 在声明为无穷大那么i == i + 1 就成立。
A:可以用任何被计算为无穷大的浮点算术表达式来声明 i ,例如double i = 1.0 / 0.0; 不过最好是能够利用标准类库提供的常量double i = Double.POSITIVE_INFINITY;
总结:事实上不必将 i 声明为无穷大也可以使条件成立,任何足够大的浮点数都可以实现这一目的。例如double i = 1.0e40;这样做之所以可以起作用是因为一个浮点数值越大,它和其后继数值之间的间隔就越大。浮点数的这种分布是用固定数量的有效位来表示它们的必然结果。对一个足够大的浮点数加 1 不会改变它的值,因为 1 不足以“填补它与其后继者之间的空隙”。浮点数操作返回的是最接近其精确的数学结果的浮点数值。一旦毗邻的浮点数值之间的距离大于 2,那么对其中的一个浮点数值加 1 将不会产生任何效果,因为其结果没有达到两个数值之间的一半。对于 float 类型加 1 不会产生任何效果的最小级数是 2^25,即 33,554,432;而对于 double 类型最小级数是 2^54,大约是 1.8* 10^16。毗邻的浮点数值之间的距离被称为一个 ulp,它是“最小单位unit in the last place”的首字母缩写词。在5.0版中引入了 Math.ulp 方法来计算 float或 double 数值的 ulp。
总之用一个 double 或一个 float 数值来表示无穷大是可以的。大多数人在第一次听到这句话时多少都会有一点吃惊,可能是因为我们无法用任何整数类型来表示无穷大的原因。
第二点将一个很小的浮点数加到一个很大的浮点数上时将不会改变大的浮点数的值。这过于违背直觉了,因为对实际的数字来说这是不成立的。我们应该记住二进制浮点算术只是对实际算术的一种近似。
二、Q:请为 i!=i 提供一个声明使得条件成立。
分析:一个数字总是等于它自己对吧?!但是 IEEE 754 浮点算术保留了一个特殊的值用来表示一个不是数字的数量,这个值就是 NaN(“ 不是一个数字Not a Number” 的缩写)。对于所有没有良好的数字定义的浮点计算,例如 0.0/0.0其值都是它。规范中描述到NaN 不等于任何浮点数值包括它自身在内。因此如果 i 在被初始化为 NaN那么 i != i 就成立。
A:可以用任何计算结果为 NaN 的浮点算术表达式来初始化 i, 例如double i = 0.0 / 0.0;同样为了表达清晰可以使用标准类库提供的常量double i = Double.NaN;
总结: NaN 还有其他的惊人之处。任何浮点操作只要它的一个或多个操作数为 NaN,那么其结果为 NaN。这条规则是非常合理的,但是它却具有奇怪的结果。 例如下面的程序将打印 false:
class Test { public static void main(String[] args) { double i = 0.0 / 0.0; System.out.println(i - i == 0); } }
这条计算 NaN 的规则所基于的原理是,一旦一个计算产生了 NaN,它就被损坏了,没有任何更进一步的计算可以修复这样的损坏。NaN 值有意使受损的计算继续执行下去直到方便处理这种情况的地方为止。
总之float 和 double 类型都有一个特殊的 NaN 值用来表示不是数字的数量。对于涉及 NaN 值的计算,其规则很简单也很明智,但是这些规则的结果可能是违背直觉的。
三、Q:请为 i!=1+0; 提供一个声明使得条件成立。但是不能像上题一样使用浮点数。
分析:与前一个题一样这个谜题初看起来是不可能实现的。毕竟一个数字总是等于它自身加上 0,你被禁止使用浮点数,因此不能使用 NaN。而在整数类型中没有 NaN 的等价物。我们必然可以得出这样的结论,即 i 的类型必须是非数值类型的,并且这其中存在着解谜方案。唯一对 + 操作符有定义的非数值类型就是 String。+ 操作符被重载了,对于 String 类型它执行的不是加法而是字符串连接。如果在连接中的某个操作数具有非 String 的类型,那么这个操作数就会在连接之前转换成字符串。
A:事实上i 可以被初始化为任何值,只要它是 String 类型的即可。例如String i="搜索微信公众号ape_it";
总结:操作符重载是很容易令人误解的。在本谜题中的加号看起来是表示一个加法,但是通过为变量 i 选择合适的类型即 String我们让它执行了字符串连接操作。甚至是因为变量被命名为 i都使得本题更加容易令人误解,因为 i通常被当作整型变量名而被保留的。对于程序的可读性来说好的变量名、方法名和类名至少与好的注释同等重要。
四、Q:请提供一个对 i 的声明,将下面的循环转变为一个无限循环:
while (i != 0) { i >>>= 1; }
分析:>>>=是对应于无符号右移操作符的赋值操作符。0 被从左移入到由移位操作而空出来的位上,即使被移位的负数也是如此。
为了使移位合法,i 必须是一个整数类型byte、char、short、int 或 long。无符号右移操作符把 0 从左边移入。因此看起来这个循环执行迭代的次数与最大的整数类型所占据的位数相同即 64 次。怎样才能将它转变为一个无限循环呢?解决本谜题的关键在于,>>>=是一个复合赋值操作符。复合赋值操作符包括*=、/=、%=、+=、-=、 <<=、 >>=、 >>>=、&=、 ^=和| =。有关混合操作符的一个不幸的事实是,它们可能会自动地执行窄化原始类型转换。这种转换把一种数字类型转换成了另一种更缺乏表示能力的类型。窄化原始类型转换可能会丢失级数的信息或者是数值的精度。
A:假设你在循环的前面放置了下面的声明 short i = -1; 因为 i 的初始值(short)0xffff是非 0 的,所以循环体会被执行。在执行移位操作时,第一步是将 i 提升为 int 类型。所有算数操作都会对short、byte和 char 类型的操作数执行这样的提升。这种提升是一个拓宽原始类型转换,因此没有任何信息会丢失。这种提升执行的是符号扩展,因此所产生的 int 数值是0xffffffff。然后这个数值右移 1 位,但不使用符号扩展,因此产生了 int数值 0x7fffffff。最后这个数值被存回到 i 中。为了将 int 数值存入 short变量,Java 执行的是可怕的窄化原始类型转换,它直接将高 16 位截掉。这样就只剩下(short)0xffff 了,我们又回到了开始处。循环的第二次以及后续的迭代行为都是一样的,因此循环将永远不会终止。
总结:如果你将 i 声明为一个 short 或 byte 变量,并且初始化为任何负数,那么这种行为也会发生。如果你声明 i 为一个 char,那么你将无法得到无限循环,因为char 是无符号的,所以发生在移位之前的拓宽原始类型转换不会执行符号扩展。总之,不要在 short、byte 或 char 类型的变量之上使用复合赋值操作符。因为这样的表达式执行的是混合类型算术运算,它容易造成混乱。更糟的是,它们执行将隐式地执行会丢失信息的窄化转型,其结果是灾难性的。
五、Q:请为 i <= j && j <= i && i != j ;提供一个声明使得条件成立。
分析:如果 i <= j 并且 j <= i,i 不是肯定等于 j 吗?这一属性对实数肯定有效。 事实上,它是如此地重要,以至于它有这样的定义: 实数上的<=关系是反对称的。Java 的<=操作符在 5.0 版之前是反对称的,但是这从 5.0 版之后就不再是了 。
在5.0 版之前,Java 的数字比较操作符(<、 <=、 >和>=)要求它们的两个操作数都是原始数字类型的(byte、char、short、int、long、float 和 double)。但是在 5.0 版中,规范作出了修改,新规范描述道:每一个操作数的类型必须可以转换成原始数字类型。
在 5.0 版中,自动包装(auto-boxing)和自动反包装(auto-unboxing)被添加到了 Java 语言中。<=操作符在原始数字类型集上仍然是反对称的,但是现在它还被应用到了被包装的数字类型上。(被包装的数字类型有: Byte、 Character、 Short、Integer、 Long、 Float 和 Double。)<=操作符在这些类型的操作数上不是反对称的,因为 Java 的判等操作符(==和!=)在作用于对象引用时,执行的是引用的比较,而不是值的比较。
A:下面的声明赋予表达式(i <= j && j <= i && i != j)的值为 true:
Integer i = new Integer(0); Integer j = new Integer(0);
前两个子表达式(i <= j 和 j <= i)在 i 和 j 上执行解包转换,并且在数字上比较所产生的 int 数值。i 和 j 都表示 0,所以这两个子表达式都被计算为 true。第三个子表达式(i != j)在对象引 用 i 和 j 上执行标识比较,因为它们都初始化为一个新的 Integer 实例,因此,第三个子表达式同样也被计算为 true。
总结:当两个操作数都是被包装的数字类型时,数值比较操作符和判等操作符的行为存在着根本的差异:数值比较操作符执行的是值比较,而判等操作符执行的是引用标识的比较。
注:本【java解惑】系列均是博主阅读《java解惑》原书后将原书上的讲解和例子部分改编然后写成博文进行发布的。所有例子均亲自测试通过并共享在github上。通过这些例子激励自己惠及他人。同时本系列所有博文会同步发布在博主个人微信公众号搜索“爱题猿”或者“ape_it”方便大家阅读。如果文中有任何侵犯原作者权利的内容请及时告知博主以便及时删除如果读者对文中的内容有异议或者问题欢迎通过博客留言或者微信公众号留言等方式共同探讨。
源代码地址https://github.com/rocwinger/java-disabuse
本文出自 “winger” 博客,谢绝转载!
【java解惑】java中那些反常识的小知识