《java解惑》读书笔记4——循环谜题

1.byte数值比较:

问题:

下面的程序循环遍历byte数值,以查找某个特定值,代码如下:

public class Test {
    public static void main(String[] args) {
        for(byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++){
            if(b == 0x90){
                System.out.println("Joy!");
            }
        }
    }
}

该程序期望打印出”Joy!”来,但是程序真实运行结果是什么也没有打印出来。

原因:

上述程序循环在除了Byte.MAX_VALUE之外所有的byte数值中进行迭代,以查找0x90(十进制为144)。这个数值适合用byte表示,并且不等于Byte.MAX_VALUE,因此按理说在循环中应该找到一次,但是真实运行确实没有找到。

原因很简单,java中二进制表示的数值最高位表示符号位,byte表示的十进制数值范围是-128~127,十六进制0x90(十进制数值144),超过了byte的表示范围,因此byte被类型扩展提高为int类型,用一个byte类型与一个int类型进行比较是一个混合类型比较运算时,byte是一个有符号的类型,所以在进行数据类型扩展时符号位要保留,因此(byte)0x90被提升为int数值-112,它确实不等于int数值144。

结论:

修正这个问题有以下三种方法:

方法1:

比较时进行强制数据类型转换,代码如下:

public class Test {
    public static void main(String[] args) {
        for(byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++){
            if(b == (byte)0x90){
                System.out.println("Joy!");
            }
        }
    }
}

方法2:

使用屏蔽码消除符合扩展,代码如下:

public class Test {
    public static void main(String[] args) {
        for(byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++){
            if((b & 0xff) == 0x90){
                System.out.println("Joy!");
            }
        }
    }
}

2.变量增量操作:

问题:

下面的程序在循环中,对变量进行增量操作,代码如下:

public class Test {
    public static void main(String[] args) {
        int j = 0;
        for(int i = 0; i < 100; i++){
            j = j++;
        }
        System.out.println(j);
    }
}

该程序期望打印输出100,但是程序真实运行打印输出结果为0.

原因:

上述程序问题出现在j= j++;增量语句上。在java中,该增量语句等价于如下的语句序列:

int tmp = j;
j = j + 1;
j = tmp;

从上述分解我们可以看到,虽然经过了100次增量操作,但是j始终被复位到循环之前的值。

结论:

修改改程序非常简单,代码如下:

public class Test {
    public static void main(String[] args) {
        int j = 0;
        for(int i = 0; i < 100; i++){
            j++;
        }
        System.out.println(j);
    }
}

只需要移除无关的赋值操作,就可以打印输出我们期望的100次增量操作的结果。

3.循环计数

问题:

下面的程序计算循环迭代的次数,代码如下:

public class Test {
    public static final int END = Integer.MAX_VALUE;
    public static final int START = END - 100;
    public static void main(String[] args) {
        int count = 0;
        for(int i = START; i <= END; i++){
            count++;
        }
        System.out.println(count);
    }
}

程序期望打印输出100,也有人认为应该打印输出101,因此程序结束循环的条件是小于等于END,该程序真实运行时会产生死循环,根本无法执行结束,更不用说什么也不会打印输出。

原因:

上述程序的for循环是一个无限循环,因为int最大值就是Integer.MAX_VALUE当int i循环达到Integer.MAX_VALUE时再次执行增量操作时,它就又绕回到了Integer.MIN_VALUE

 

结论:

解决该问题有如下几种方法:

方法1:

当需要的循环会迭代到int数值的边境附件时,最好使用一个long类型的变量作为循环索引,因此只需要将循环所以从int类型修改为long类型就可以解决该问题,从而使程序打印出我们期望的101,代码如下:

public class Test {
    public static final int END = Integer.MAX_VALUE;
    public static final int START = END - 100;
    public static void main(String[] args) {
        int count = 0;
        for(long i = START; i <= END; i++){
            count++;
        }
        System.out.println(count);
    }
}

方法2:

不使用long类型也可以解决该问题,使用do-while循环也可以打印输出我们期望的101,代码如下:

public class Test {
    public static final int END = Integer.MAX_VALUE;
    public static final int START = END - 100;
    public static void main(String[] args) {
        int count = 0;
        int i = START;
        do{
            count++;
        }while(i++ != END);
        System.out.println(count);
    }
}

在进行循环或者运算时,特别注意数据类型的溢出。

4.循环移位计数:

问题:

下面程序通过移位操作,在循环中记录迭代次数,代码如下:

public class Test {
    public static void main(String[] args) {
        int i = 0;
        while(-1 << i != 0){
            i++;
        }
        System.out.println(i);
    }
}

Java使用的是基于2的补码的二进制算术运算,因此-1在任何有符号的整数类型中(byte、short、int或long)的表示都是所有的为被置位,因此常量-1是所有32为都被置位的int数值(oxffffffff),左移位操作符将0移入到最低位,因此表达式(-1 << i)将i最右边的位设置为0,并保持左边的32-i位为1,因此我们期望整个循环在32次循环移位后将-1变为0从而结束循环,因此程序应该打印输出32,但是真实程序运行时同样成了一个无限死循环,没有任何打印输出。

原因:

Java中三个移位操作符:<<,>>和>>>的移位长度总数介于0到数据类型长度(不包括)之间,即int类型移位长度总数介于0~31之间,long类型移位长度总数介于0~63之间,也就是移位长度对数据类型长度取余。如果视图对一个int类型移位32位或者对一个long类型移位64为,都只能返回这个数自身的值。

结论:

解决这个问题很简单,我们不要让-1重复地移位不同的位移长度,而是将前一次的移位结果保存,并且让其在每一次循环迭代时都向做移位恒定的1位,下面的代码可以打印出我们期望的32:

public class Test {
    public static void main(String[] args) {
        int count = 0;
        for(int i = -1; i != 0; i <<= 1){
            count++;
        }
        System.out.println(count);
    }
}

如果可能的话,尽可能在移位时移动恒定位数。

5.无限循环:

下面的循环都是无限死循环:

(1).数据类型溢出:

int start = Integer.MAX_VALUE – 1;
for(int i = start; i <= start + 1; i++){}

(2).浮点数之间的最小距离:

double i = 1.0 / 0.0;//或者i = Double.POSITIVE_INFINITY;
while(i == i + 1){}

也可以使用double i =1.0e40;

一个浮点数数值越大,它和其后继数值之间的间隔就越大,浮点数的这种分布是用固定数量的有效位来表示它们的必然结果,对于一个足够大的浮点数加1不会改变他的值,因为1不足以填补它与其后继者之间的空隙。

Java中浮点数操作返回的是最接近其精确的数学结果的浮点数值,一旦毗邻的浮点数值之间的距离大于2,则对于其中的一个浮点数值加1将不会产生任何效果,因为其结果没有达到两个数值之间的一半,对于float类型,加1不会产生任何效果的最小级数是2的35次方;而对于double类型,最小级数是2的54次方。

毗邻的浮点数值之间的距离被称为一个ulp(unitin the last place),JDK1.5中引入了Math。ulp方法来计算float或double数值的ulp。

(3).NaN:

Double i = 0.0 / 0.0;//或者i = Double.NaN;
While(i != i){}

Java浮点数中的NaN表示不是一个数字,对于所有没有数学定义的浮点数都由其来表示,NaN不等于任何浮点数值,包括其自身在内。

(4).类型扩展和类型窄化:

short i = -1;
while(i != 0){
	i >>>= 1;
}

short类型的-1是oxffff,i >>>= 1操作分解为一下:

首先,将数据类型扩展为int,即0xffffffff。

然后,进行无符号右移操作变成0x7fffffff。

最后,把int类型窄化为short,截掉高位之后变为0xffff。

因此注意不要在short,byte和char类型的变量上使用复合赋值操作符,因为复合赋值表达式执行的是混合类型算术运算,运算结果可能会自动只想数据类型窄化转换而丢失精度。

(5).自动装箱比较运算符:

Integer i = new Integer(0);
Integer j = new Integer(0);
while(i <= j && j <= i && i != j){}

原始数值类型被自动装箱为对象类型后,相等运算符比较的不再是数值而是对象的引用。

(6).数值的非对称性:

int i = Integer.MIN_VALUE;//或long i = Long.MIN_VALUE;
while(i != 0 && i == -i){}

计算机中有符号数使用的二进制补码算术运算,为了对一个数值取其负值,需要反转其每一位然后加1从而得到结果,二进制补码算术运算的一个优势是0具有唯一的表示形式,但是有个不利之处在于总共存在偶数个int数值(2的32次方),其中一个用来表示0,剩下奇数个数用来表示正负数,因此整数和负数的个数不想等,而Integer.MIN_VALUE是2的负32次方,即0x8000000,其符号位为1,如果对其去负值,得到是0x7fffffff+1,结果还是0x80000000,因此Integer.MIN_VALUE和其相反数是同一个数,Long.MIN_VALUE也一样,更一般的讲java中所有的数值类型都是非对称,他们的MIN_VALUE的相反数都是自身。

猜你喜欢

转载自blog.csdn.net/chjttony/article/details/18412207