【笔记】Java解惑 表达式之谜

Puzzle1:

第一个迷惑性的错误,属于想当然的类型,不过,我好像不太会遇到,因为自己肯定会判断余数为0而不是1的(虽然并没有考虑到负数),另外,位操作才是王道!


/**
 * 判断一个数的奇偶性,使用? % 2 == 1正确吗?
 */

public class P1 {
    public static void main(String[] args) {
        System.out.println("i   isOddByOne   isOddByZero   isOddByBit");
        for(int i = -5; i < 6; i++){
            System.out.print(i + "      " + isOddByOne(i));
            System.out.print("        " + isOddByZero(i));
            System.out.println("      " + isOddByBit(i));
        }
    }

    public static boolean isOddByOne(int i){
        return i % 2 == 1; // i < 0,i % 2 == -1, return false
    }

    public static boolean isOddByZero(int i){
        return i % 2 != 0; // 优先使用第三个判断方法~
    }

    public static boolean isOddByBit(int i){
        return (i & 1) != 0; // 对于1而言,其余位都为0,只有末位为1。与操作,仅当i的末位也为1时判断结果才能为true,也就是i必须为奇数。数学渣表示这里还不是一眼就看穿的,心好累。
    }
}

【笔记】Java解惑 表达式之谜_第1张图片


对于负数,判断尾数为1是无效的,因为尾数为-1。



Puzzle2:

关于这个容易踩到的陷阱,从我第一天学习编程语言(没有错,就是C)的时候,就被老师重点指出过,也被各种书籍无数次提醒过,但日常其实还是一直用着float和double这种,一来是对精度要求有限,二来是很少意识到要避免这个问题,还没有被坑过,所以不深刻啊。。。


import java.math.BigDecimal;

/**
 * 日常操作,2.00元扣除1.10元,余额是?
 */

public class P2 {
    public static void main(String[] args) {
        System.out.println(2.00 - 1.10); // 由于1.10并不能被精确地表示为一个double,导致结果不可能是0.90这个准确值,而是0.8999999999999999
        System.out.println(new BigDecimal(1.10).toString()); // 尽量避免使用new BigDecimal(double),该构造器,会用参数的精确值来创建一个实例,而事实是,类似1.10这种,是没有所谓的正确精确值的,只能表示为1.100000000000000088817841970012523233890533447265625,并不是想要的1.10。
        System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10"))); // : 0.90,使用BidDecimal,会比使用原生类型来的更慢,如果确实在意,可以考虑改变数值的单位,如“元”改为“分”,这样的话,2.00和1.10就能当做200和110两个整数来处理,即精确又迅速。
    }
}



Puzzle3:

/**
 * 长整除,不是最终类型为long就一定不溢出
 */

public class P3 {
    public static void main(String[] args) {
        final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000; // 微秒 int类型的范围是-2147483648 ——2147483647,尽管该变量的类型是long,但直到结束所有乘法运算之前,都还是int类型,于是溢出,导致结果不对。在平时的编程中,一定要注意这种静默出现的错误,没摔过跟头之前,总觉得难以记牢。。。
        final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000; // 毫秒
        System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY); // : 5
        final long MICROS_PER_DAYL = 24L * 60 * 60 * 1000 * 1000;
        final long MILLIS_PER_DAYL = 24L * 60 * 60 * 1000;
        System.out.println(MICROS_PER_DAYL / MILLIS_PER_DAYL); // : 1000
    }
}


Puzzle4:

/**
 * 论长整数时L而非l的重要性
 */

public class P4 {
    public static void main(String[] args) {
        // Source Code Pro还是可以清楚看出1和l的区别的,压力不大O(∩_∩)O哈哈~
        System.out.println(12345 + 5432l); // : 17777
    }
}


Puzzle5:

/**
 * 十六进制,避免混合类型的计算
 */

public class P5 {
    public static void main(String[] args) {
        System.out.println(Long.toHexString(0x100000000L + 0xcafebabe)); // : cafebage
        // 对于八进制或者十六进制,最高位表示正负号,0xcafebabe是int型,故最高位是1,表示负数,等于-889275714,也等于0xffffffffcafebageL,也因此运行结果丢失了开头的1。
        System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL)); // : 1cafebage # 尽量避免混合类型的计算,都设置为长整型,就不会有这种问题了!
    }
}



Puzzle6:

不得不说,原码、反码、补码这块知识点,头疼了好久,底层的东西,感觉不容易理解,又容易忘却。


/**
 * 多重转型
 * int 32位  char 16位(无符号)  byte 8位
 * Java使用基于2的补码的二进制运算!!!
 * 正数的补码等于原码,负数的补码为原码取反(符号位不变,即反码)再加一
 */

public class P6 {
    public static void main(String[] args) {
//        System.out.println(Integer.toBinaryString(-1)); // : 11111111111111111111111111111111 # 32个1
        System.out.println((int)(char)(byte)-1); // : 65535 #
        // (byte)-1 == (原码)10000001 == (补码)11111111
        // (char)(byte)-1 == (补码)1111111111111111 // 由byte转char,因为byte是有符号的,所以执行符号扩展
        // (int)(char)(byte)-1 == (补码)00000000000000001111111111111111 == (原码)00000000000000001111111111111111 == 65535 // 由char转int,由于char是无符号位的,因此执行零扩展,直接导致-1变成了65535
        // 不得不说,当初上学那会,对原码、反码、补码纠结着,总记着那个时钟的例子。。。
    }
}



Puzzle7:

我就不说这种错误我是不会犯的,因为我就没想过用异或这种东西,我好菜。。。


/**
 * 通过异或运算符互换内容,在Java里可不要想着一行完成这种事,得不偿失
 */

public class P7 {
    public static void main(String[] args) {
        int x = 1984; // 0x7c0
        int y = 2001; // 0x7d1
        x ^= y ^= x ^= y; // 据说在C或者C++中,能实现互换内容,我已经不写C好多年,谁知道呢→_→
        System.out.println("x = " + x + "; y = " + y); // : x = 0; y = 1984 #
        /* 假设按照C的处理方式,来计算下:
            x == 1984 == 0x7c0 == (原码)0000 0111 1100 0000 == (反码)0000 0111 1100 0000 == (补码)0000 0111 1100 0000
            y == 2001 == 0x7d1 == (原码)0000 0111 1101 0001 == (反码)0000 0111 1101 0001 == (补码)0000 0111 1101 0001
            x == x^y == 0000 0000 0001 0001
            y == y^x == 0000 0111 1100 0000
            x == x^y == 0000 0111 1101 0001
            由此可见,确实是互换了内容,但是!!!!!!这是假设按照C的处理方式,事实上,Java语言规范描述了“操作符的操作数是从左向右求值的,为了求x^=expr的值,在计算expr之前提取x的值” 
            The Java language specification says that operands of operators are evaluated from left to right [JLS 15.7]. To evaluate the expression x ^= expr, the value of x is sampled before expr is evaluated, and the exclusive OR of these two values is assigned to the variable x [JLS 15.26.2]. 
            显然,按照Java的做法,这段代码是不可能实现互换内容的,当然,非要实现,可以改为y = (x^= (y^= x)^ y,但是,这种代码,和以前在C语言书上看过的i = ++i++++之流有何区别,可读性差,容易出错,好处就仅仅是节省了一行空间?!完全是bad smell
        */
    }
}


Puzzle8:

又是混合类型造成的,你让我这个从没踩过类似坑的是该笑呢还是该哭,我太菜了是吧╮(╯▽╰)╭

/**
 * 三目运算符的陷阱
 */

public class P8 {
    public static void main(String[] args) {
        char x = 'X';
        int i = 0;
        final int LX = 89;
        System.out.println(true ? x : 0); // : X # 0为常数,返回值类型为char
        System.out.println(false ? i : x); // : 88 # i为变量,二进制数字提升
        System.out.println(true ? x : i); // : 88 # i为变量,二进制数字提升
        System.out.println(true ? x : LX); // : X # LX为常量,返回值类型为char
        System.out.println(false ? x : LX); // : Y # LX为常量,返回值类型为char
        /*
            对于三目运算符,如果第二个和第三个参数类型相同,则返回值类型也一致,否则,大致按照以下规则:
            如果一个类型为T(byte short char),另一个为int类型的常量表达式(注意,这里第二个参数和第三个参数,是平等地位,别理解成了以第二个参数为主),则返回值类型为T,否则,对两个参数类型进行二进制数字提升,提升之后的类型即为返回值类型。
            总而言之,不要使用混合类型!!!
         */
    }
}



Puzzle9:

原来复合赋值操作符这么危险,哎,再感慨一句,我又没遇到过,菜!


/**
 * 复合赋值操作符的陷阱 k op v 等价于 k = (K)(k op v) K为k的类型
 */

public class P9 {
    public static void main(String[] args) {
        short x = 1;
        int i = 123456;
        x += i; // 这里会将 x + i的结果转换为short类型,如果值过大,就会出现不正确的结果了
        System.out.println(x); // : -7615 # 123457对于short来说太大了,自动转换时得到不正确的结果
        x = (short) (x + i); // 将int隐式转为short是非法的,可以显式转换,但是,你还要确保结果不出错呦
    }
}



Puzzle10:

复合赋值操作符除了上一篇讲的默认转类型外,还有另一个要求!


/**
 * 复合赋值操作符( += -= *= /= %= <<= >>= >>>= &= ^= != )的陷阱
 */

public class P10 {
    public static void main(String[] args) {
        Object x = "L";
        String i = "X";
        x = x + i; // String可以向上转型为Object类型
        i += x; // 左侧为String类型,允许右侧为任何类型
        x += i; // 左侧为Object类型,不可以,此行报错,无法运行!
        /*
        复合赋值操作符要求两个操作数都是原生类型,例如int,或者包装了的原生类型,例如Integer,唯一的例外是当左侧的操作数为String类型时,右侧可以为任何类型,在这种情况下,执行的是字符串连接操作。
         */
    }
}






你可能感兴趣的:(Java,【笔记】Java解惑)