`
arcticfox9902
  • 浏览: 107190 次
  • 性别: Icon_minigender_2
  • 来自: 杭州
社区版块
存档分类
最新评论

Java解惑之表达式谜题(1)

阅读更多

1、奇数性

public static boolean isOdd (int i) {
       return i % 2 == 1;
}

      上面这个函数在四分之一的时间里返回的都是错误的答案,因为负奇数模2的结果是-1。

      Java中,%操作符被定义为对于所有的int数值a和所有的非零int数值b,都满足下面的恒等式:

(a / b) * b + (a % b) == a。

      上面的问题很容易改正,只需将i%2与0比较,而不是与1比较,如下:

public static boolean isOdd (int i) {
       return i % 2 != 0;
}

      如果考虑到性能,用位操作符&来取代%会更好一些:

public static boolean isOdd (int i) {
       return (i & 1) != 0;
}

 

2、浮点数

      不是所有的小数都可以用二进制浮点数来精确表示。浮点数运算在一个范围很广的值域上提供了很好的近似,但是它通常不能产生精确的结果。在商业计算中往往需要精确的结果,这种情况下,就不能用float和double类型了。

      如果要得到精确的结果,一种方式是使用执行精确小数运算的BigDecimal,但是,一定要用BigDecimal(String)构造函数,而不要用BigDecimal(double),后一个构造函数将用它的参数的“精确”值来创建一个实例:

System.out.println(new BigDecimal(1.1));
System.out.println(new BigDecimal("1.1"));

输出结果:
1.100000000000000088817841970012523233890533447265625
1.1

     使用BigDecimal的计算很有可能比使用原始类型的计算要慢一些,因为Java并没有为BigDecimal提供任何语言支持。

 

3、长整数

      首先来看下面这段代码:

long MICROS_PER_DAY = 24 * 60 * 60 * 1000;
long MILLIS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
System.out.println(MICROS_PER_DAY);
System.out.println(MILLIS_PER_DAY);

      输出结果跟想象中有点不一样:

86400000
500654080

      问题出在哪里?24这个字面常量代表的是一个int型的数字,同样60、1000也是int型的,int型的数字相乘,得到的结果也是int型的,24*60*60*1000得到的结果是86400000,在int的范围之内,因此第一个输出结果是正确的,但是24*60*60*1000*1000的结果超过了int的范围,也就是说结果溢出了!虽然MILLIS_PER_DAY是long型的,但不影响右边的计算结果,于是就得到了上面的500654080。

      为了解决这类问题,一般使用long型的字面常量来代替int型的作为每个乘积的第一个因子,这样就强制表达式中的后续计算都使用long运算。修改后的代码如下:

long MICROS_PER_DAY = 24L * 60 * 60 * 1000;
long MILLIS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
System.out.println(MICROS_PER_DAY);
System.out.println(MILLIS_PER_DAY);

输出结果:
86400000
86400000000

      注意:当你在操作很大的数字时,千万要提防溢出!溢出不会报错,但是会导致结果不正确,这才是真正可怕的地方,你可能完全没有意识结果错误,或者你发现结果错误但是找不到哪里出错了(因为它不会有任何提示!)。当你不确定是否会溢时,就使用long运算来执行整个运算。

 

4、初级问题:1和l

      在这里1和l比较容易区分,l就是一个竖线,但其他字体中就不是这样了,1和l有着非常细小的差异,不注意还真区分不出来。数字1的水平笔划(称为“臂(arm)”)和垂直笔划(称为“茎(stem)”)之间是个锐角,而小写字母l的臂和茎之间是个直角。(这里就不上图了,不知道为什么只能插入网络图片...)

      因此,在long型字面常量中,一定要用大写的L,千万不要用小写l。类似的,要避免使用单独的一个l字母作为变量名。总之,小写字母l和数字1在大多数字体中都是几乎一样的,为避免混淆,一定要注意上面的警告。

 

5、十六进制的趣事

System.out.println(Long.toHexString(0x100000000L + 0xcafebabe));

输出结果:
cafebabe 

      这个输出是不是挺奇怪的?难道不应该是1cafebabe吗?前面的1哪里去了?

      我们先来看一些规定,所有的十进制字面常量都是正的,如果需要表示负数,则在前面加上“-”,而八进制和十六进制则不同,如果他们的最高位被置1,那么表示负数。

      现在我们来看看上面的代码,0x100000000L是long型的,0xcafebabe是int型的,运算之前首先要把0xcafebabe转换为long型的,看仔细了:0xcafebabe一共32位,最高位是1,它实际上是一个负数!等于十进制的-889275714,转换为long型需要带符号扩展,扩展后为0xffffffffcafebabeL,与0x100000000L相加,得到0xcafebabeL。

      为了避免出现上述问题,只需将上面的右操作数改为long型的0xcafebabeL,long型是64位,因此它的最高位未被置位,表示正数。

      十六进制和八进制的混合类型的计算可能会产生混淆,因此应该避免混合类型的计算。

 

6、多重类型转换

System.out.println((int)(char)(byte)-1);

      上面这个转换没有我们想象中那么简单,而且它的结果也不是-1。让我们来仔细分析一下:-1这个字面常量是int型的,32位,用二进制表示是11111111 11111111 11111111 11111111。

      第一步,从int类型到byte类型的转换比较简单,它执行了一个窄化原始类型转换,只留下低8位,其他的位数全丢掉。这样就得到了11111111。

      第二步,从byte类型到char类型的转换稍微复杂一点,因为byte类型是有符号的,而char类型是一个无符号类型。将整数类型转换成另一个更宽的整数类型时,通常是可以保持其数值不变的,但是却不可能将一个负的byte数值表示成一个char。从byte类型到char类型的转换被认为不是一个拓宽原始类型的转换,而是一个拓宽并窄化原始类型的转换(这句话听着好复杂...),简单点说:byte类型不能直接转换为char类型,byte类型先被转换为int类型,再将int类型转换为char类型,是通过这种曲线救国的方式实现的byte类型到char类型的强制类型转换。再看上面的例子,11111111这个byte类型先被转换为int类型,带符号扩展之后为11111111 11111111 11111111 11111111,再把这个int类型转换为char类型,只留下低16位11111111 11111111。

      第三步,从char类型到int类型的转换为拓宽原始类型转换,因为char类型是无符号类型,所以将执行零扩展而不是符号扩展。扩展之后结果为00000000 00000000 11111111 11111111,这个int数值是65535,这才是上面的代码打印出的结果。

 

      上面那段话是从表面上理解byte类型到char类型的转换,现在我们从更深层来研究下这个类型转换:在java虚拟机中,数值类型、char类型都是以32位为单位来存的,也就是说存储的基本单位是int型,不管是byte类型,还是short类型,或者char类型,在java虚拟机中其实都是以int类型存储的!我们回头重新再看上面的例子:

      第一步,字面常量-1是int类型,首先要进行强制类型转换,转换为byte类型,在java虚拟机里的指令是i2b,这个指令先将int类型截短为byte类型11111111,再对其进行带符号扩展恢复为int类型11111111 11111111 11111111 11111111,这个数字才是真正存储的数字。

      第二步,byte类型转换为char类型,java虚拟机中没有相应的指令,byte类型的运算需先将byte类型转换为int类型再进行运算,转换也是一样,要先转换为int类型再进行其他的转换。java虚拟机中也没有byte类型到int类型的转换指令,原因很明显,byte类型在java虚拟机中本来就是以int类型保存的,那么就可以直接进行int类型到char类型的转换,相应的指令为i2c,这个指令先将int类型截短为char类型11111111 11111111,再对其进行零扩展恢复为int类型00000000 00000000 11111111 11111111。

      第三步,char类型转换为int类型,与byte类型到int类型的转换相似,java虚拟机中没有相应的指令,真正保存的类型正是int类型,因此直接返回该int类型的数字就是了,那么最终结果是65535。

 

7、互换内容

      一个例子:x^=y^=x^=y,额,好丑!这种代码我实在不想分析!简单点说好了。

      这行代码的目的是交换x和y的值,在早期,人们利用异或的属性(x^y^x == y)来避免使用临时变量,这种方法曾经在C编程语言中被使用过,进一步被用在C++中,它不能保证在两者中都可以正确运行。但是有一点可以保证,在java中这样的代码一定不能正确运行。java中运算是从左到右进行的,上面的表达式中,会先把最左边的x入栈,再计算右边那一串y^=x^=y,计算结果是y=x,然后计算x^=y,也就是x^=x,结果就得到了x=0!可以看到,计算最左边的^=时,x已经不是原始值了,但是最左边的x一开始就会作为操作数入栈,而不是等到真正计算的时候才取。

      结论是:在单个表达式中不要对相同的变量赋值两次。这样会引起混乱,并且很少能够执行你希望的操作,这种代码很容易产生bug,不仅难以维护,运行速度往往也比它们替换掉的直观的代码要慢。

 

8、条件操作符(? 分支1 : 分支2)

char x = 'X';
int i = 0;
System.out.println(true ? x : 0);
System.out.println(false ? i : x);

      这段代码看起来好像没什么问题,应该打印XX,但实际上打印的却是X88。

      上面那两个表达式中,第二个和第三个操作数的类型不同,一个是char类型,一个是int类型,这就涉及到了混合类型计算。条件表达式结果类型的规则的核心有以下三点:

  •       如果第二个和第三个操作数具有相同的类型,那么这个类型就是条件表达式的结果类型。
  •       如果一个操作数的类型是T,T表示byte、short、char,而另一个操作数是一个int类型的常量表达式,它的值是可以用类型T表示的,那么条件表达式的结果类型是T。
  •       否则,将对操作数类型运用二进制数字提升,条件表达式的结果类型就是第二个和第三个操作数被提升之后的类型。

      在本例的第一个条件表达中,x是char类型的,0是int类型的字面常量值,0可以用char类型表示,因此这个条件表达式的结果类型是char类型。第二个表达式中,i是int类型的变量,x是char类型,需要对x提升到int类型,因此这个条件表达式的结果是int类型。

      通常应该尽量避免使用混合类型计算,尤其是在条件表达式中。

 

9、复合赋值与简单赋值(一)

      复合赋值x+=i并不等同于x=x+i,它们有一些细微的区别。我们来看下面的例子:

 

short x = 0;
int i = 123456;

      复合赋值会自动将计算结果转换为左边的操作数的类型,因此x += i是不会报错的,但是得到的结果是-7616。而简单赋值x = x + i则会编译不通过,因为等号右侧的计算结果是int类型,将int类型赋值给short类型需要强制类型转换。

      需要注意:复合赋值会悄悄的产生一个类型转换,这个转换可能会让你的程序运行结果出乎意料。

 

10、复合赋值与简单赋值(二)

      复合赋值与简单赋值的另一个区别:复合赋值要求两个操作数都是原始类型或被包装了的原始类型,例如int或Integer等,有一个例外:如果左操作数是String类型,那么它允许右侧的操作数使任意类型,这种情况下,该操作符执行字符串连接操作;简单赋值允许其左操作数是对象引用类型,只要简单赋值操作符右侧的变量类型与左侧的变量是赋值兼容的类型。

 

分享到:
评论
1 楼 fancyleeo 2012-04-17  
呵呵,这是第一部分?

相关推荐

Global site tag (gtag.js) - Google Analytics