Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1938070
  • 博文数量: 219
  • 博客积分: 8963
  • 博客等级: 中将
  • 技术积分: 2125
  • 用 户 组: 普通用户
  • 注册时间: 2005-10-19 12:48
个人简介

文章分类

全部博文(219)

文章存档

2021年(1)

2020年(3)

2015年(4)

2014年(5)

2012年(7)

2011年(37)

2010年(40)

2009年(22)

2008年(17)

2007年(48)

2006年(31)

2005年(4)

分类: Java

2007-03-17 10:10:54

请提供一个对i的声明,将下面的循环转变为一个无限循环:

while (i != i) {

}

这个循环可能比前一个还要使人感到困惑。不管在它前面作何种声明,它看起来确实应该立即终止。一个数字总是等于它自己,对吗?

对,但是IEEE 754浮点算术保留了一个特殊的值用来表示一个不是数字的数量[IEEE 754]。这个值就是NaN不是一个数字(Not a Number的缩写),对于所有没有良好的数字定义的浮点计算,例如0.0/0.0,其值都是它。规范中描述道,NaN不等于任何浮点数值,包括它自身在内[JLS 15.21.1]。因此,如果i在循环开始之前被初始化为NaN,那么终止条件测试(i != i)的计算结果就是true,循环就永远不会终止。很奇怪但却是事实。

你可以用任何计算结果为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值意图使受损的计算继续执行下去,直到方便处理这种情况的地方为止。

总之,floatdouble类型都有一个特殊的NaN值,用来表示不是数字的数量。对于涉及NaN值的计算,其规则很简单也很明智,但是这些规则的结果可能是违背直觉的。

请提供一个对i的声明,将下面的循环转变为一个无限循环:

while (i != i + 0) {

}

与前一个谜题不同,你必须在你的答案中不使用浮点数。换句话说,你不能把i声明为doublefloat类型的。

与前一个谜题一样,这个谜题初看起来是不可能实现的。毕竟,一个数字总是等于它自身加上0,你被禁止使用浮点数,因此不能使用NaN,而在整数类型中没有NaN的等价物。那么,你能给出什么呢?

我们必然可以得出这样的结论,即i的类型必须是非数值类型的,并且这其中存在着解谜方案。唯一的 + 操作符有定义的非数值类型就是String+ 操作符被重载了:对于String类型,它执行的不是加法而是字符串连接。如果在连接中的某个操作数具有非String的类型,那么这个操作书就会在连接之前转换成字符串[JLS 15.18.1]

事实上,i可以被初始化为任何值,只要它是String类型的即可,例如:

String i = "Buy seventeen copies of Effective Java";

int类型的数值0被转换成String类型的数值0”,并且被追加到了感叹号之后,所产生的字符串在用equals方法计算时就不等于最初的字符串了,这样它们在使用==操作符进行计算时,当然就不是相等的。因此,计算布尔表达式(i != i + 0)得到的值就是true,循环也就永远不会被终止了。

总之,操作符重载是很容易令人误解的。在本谜题中的加号看起来是表示一个加法,但是通过为变量i选择合适的类型,即String,我们让它执行了字符串连接操作。甚至是因为变量被命名为i,都使得本谜题更加容易令人误解,因为i通常被当作整型变量名而被保留的。对于程序的可读性来说,好的变量名、方法名和类名至少与好的注释同等重要。

对语言设计者的教训与谜题1113中的教训相同。操作符重载是很容易引起混乱的,也许 + 操作符就不应该被重载用来进行字符串连接操作。有充分的理由证明提供一个字符串连接操作符是多么必要,但是它不应该是 +

请提供一个对i的声明,将下面的循环转变为一个无限循环:

while (i != 0) {

    i >>>= 1;

}

回想一下,>>>=是对应于无符号右移操作符的赋值操作符。0被从左移入到由移位操作而空出来的位上,即使被移位的负数也是如此。

这个循环比前面三个循环要稍微复杂一点,因为其循环体非空。在其循环题中,i的值由它右移一位之后的值所替代。为了使移位合法,i必须是一个整数类型(bytecharshortintlong)。无符号右移操作符把0从左边移入,因此看起来这个循环执行迭代的次数与最大的整数类型所占据的位数相同,即64次。如果你在循环的前面放置如下的声明,那么这确实就是将要发生的事情:

long i = -1; // -1L has all 64 bits set

你怎样才能将它转变为一个无限循环呢?解决本谜题的关键在于>>>=是一个复合赋值操作符。(复合赋值操作符包括*=/=%=+=-=<<=>>=>>>=&=^=|=。)有关混合操作符的一个不幸的事实是,它们可能会自动地执行窄化原始类型转换[JLS 15.26.2],这种转换把一种数字类型转换成了另一种更缺乏表示能力的类型。窄化原始类型转换可能会丢失级数的信息,或者是数值的精度[JLS 5.1.3]

让我们更具体一些,假设你在循环的前面放置了下面的声明:

short i = -1;

因为i的初始值((short)0xffff)是非0的,所以循环体会被执行。在执行移位操作时,第一步是将i提升为int类型。所有算数操作都会对shortbytechar类型的操作数执行这样的提升。这种提升是一个拓宽原始类型转换,因此没有任何信息会丢失。这种提升执行的是符号扩展,因此所产生的int数值是0xffffffff。然后,这个数值右移1位,但不使用符号扩展,因此产生了int数值0x7fffffff。最后,这个数值被存回到i中。为了将int数值存入short变量,Java执行的是可怕的窄化原始类型转换,它直接将高16位截掉。这样就只剩下(short)oxffff了,我们又回到了开始处。循环的第二次以及后续的迭代行为都是一样的,因此循环将永远不会终止。

如果你将i声明为一个shortbyte变量,并且初始化为任何负数,那么这种行为也会发生。如果你声明i为一个char,那么你将无法得到无限循环,因为char是无符号的,所以发生在移位之前的拓宽原始类型转换不会执行符号扩展。

总之,不要在shortbytechar类型的变量之上使用复合赋值操作符。因为这样的表达式执行的是混合类型算术运算,它容易造成混乱。更糟的是,它们执行将隐式地执行会丢失信息的窄化转型,其结果是灾难性的。

对语言设计者的教训是语言不应该自动地执行窄化转换。还有一点值得好好争论的是,Java是否应该禁止在shortbytechar变量上使用复合赋值操作符。

阅读(1889) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~