《Java解惑》阅读笔记
系统学习 Java 0 184

简介:

本书写了95个有关Java或其类库的陷阱和缺陷的谜题,其中大多数谜题都采用了短程序的方式,这些程序的行为与其看似的大相径庭。在每个谜题之后都给出了详细的解惑方案,这些解惑方案超越了对程序行为的简单解释,向读者展示了如何一劳永逸地避免底层的陷阱与缺陷。

虽然其中许多可以通过IDE的静态分析识别并解决,但学会解决这些谜题有助于提升编程能力【并且一般笔试面试都喜欢考】。

相关资料:

表达式之谜

(No.1) 奇数性

典型案例:

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

原因分析: 整数取模操作就是求余数,满足(a / b) * b + (a % b) == a,因此输入为负整数时返回值一定为false

解决方案:

  1. 使用相反的含义:
    public class Oddity {
        public static boolean isOdd(int i) {
            return i % 2 != 0;
        }
    }
  2. 位运算【速度更快】
    public class Oddity {
        public static boolean isOdd(int i) {
            return (i & 1) != 0;
        }
    }

(No.2) 浮点数计算

典型案例:

public class Change {
    public static void main(String args[]) {
        System.out.println(2.00 - 1.10);
    }
}

原因分析: 不是所有的小数都可以用二进制浮点数精确表示

解决方案: 在需要精确答案的地方,要避免使用floatdouble,尽量多使用intlongBigDecimal

  1. 使用int:
    public class Change {
        public static void main(String args[]) {
            System.out.println((200 - 110) + "cents");
        }
    }
  2. 使用BigDecimal
    import java.math.BigDecimal;
    public class Change {
        public static void main(String args[]) {
            System.out.println(new BigDecimal("2.00").subtract(new BigDecimal("1.10"))); # NOTE: java出于清晰性考虑,不支持运算符重载
        }
    }

字面量

(No.3) 赋值范围

典型案例:

public class LongDivision {
    public static void main(String[] args) {
        final long MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;
        final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
        System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
    }
}

原因分析: MICROS_PER_DAY初始化赋值时字面量已溢出

解决方案:

public class LongDivision {
    public static void main(String[] args) {
        long MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;
        long MILLIS_PER_DAY = 24L * 60 * 60 * 1000;
        System.out.println(MICROS_PER_DAY / MILLIS_PER_DAY);
    }
}

(No.4) 类型标识

典型案例:

public class Elementary {
    public static void main(String[] args) {
        System.out.println(12345 + 5432l);
    }
}

原因分析: 将l错看成1

解决方案: 字面量后的标记L一定要用大写

public class Elementary {
    public static void main(String[] args) {
        System.out.println(12345 + 5432L);
    }
}

多进制

(No.5) 十六进制字面量表达

典型案例:

public class JoyOfHex {
    public static void main(String[] args) {
        System.out.println(Long.toHexString(0x100000000L + 0xcafebabe));
    }
}

原因分析: 0xcafebabe作为int类型,表示的是一个负数【16进制字面量首bit代表正负】

解决方案: 尽量避免混合类型的计算

public class JoyOfHex {
    public static void main(String[] args) {
        System.out.println(Long.toHexString(0x100000000L + 0xcafebabeL));
    }
}

(No.59) 八进制字面量表达

典型案例: 以下代码并不会打印"8",而是会打印"14"

import java.util.*;

public class Differences {
    public static void main(String[] args) {
        int vals[] = { 789, 678, 567, 456, 345, 234, 123, 012 };
        Set<Integer> diffs = new HashSet<Integer>();

        for (int i = 0; i < vals.length; i++)
            for (int j = i; j < vals.length; j++)
                diffs.add(vals[i] - vals[j]);
        System.out.println(diffs.size());
    }
}

原因分析: vals[]中的最后一个量是012,是一个八进制字面量,使得vals不再是个等差数列

(No.6) 多重转型

典型案例:

public class Multicast {
    public static void main(String[] args) {
        System.out.println((int) (char) (byte) -1);
    }
}

原因分析:

  1. -1字面量本身是int类型,所以存储的是0xFFFFFF
  2. int转为byte,前三字节截断,变为0xFF
  3. 由于byte是有符号的,所以转为char时首部填充符号位,变为0xFFFF【符号扩展】
  4. 由于char是没有符号的,所以转为int时首部填充0,变为0x0000FFFF,表示65535【零扩展】

解决方案: 不要随意转换有符号和无符号数据类型【符号扩展与零扩展是为了避免数值在扩展过程中发生改变】

如果不能通过观察确定程序要做什么,那么它做的就很可能不是你想要的

(No.7) 交换内容

典型案例:

public class CleverSwap {
    public static void main(String[] args) {
        int x = 1984;
        int y = 2001;
        x ^= y ^= x ^= y;
        System.out.println("x = " + x + "; y = " + y);
    }
}

原因分析: java中的操作符自左向右求值,因此无法实现理想的依次异或三次。【x ^= y ^= x ^= y应该解释为x = x ^ (y ^= x ^= y),等式右边的x是初始的x

解决方案: 在单个表达式中不要对相同的变量赋值两次

public class CleverSwap {
    public static void main(String[] args) {
        int x = 1984;
        int y = 2001;
        x ^= y;
        y ^= x;
        x ^= y;
        System.out.println("x = " + x + "; y = " + y);
    }
}

(No.8) 条件表达式的返回值类型

典型案例: 输出的结果为"X"和"88"

public class DosEquis {
    public static void main(String[] args) {
        char x = 'X';
        int i = 0;
        System.out.println(true ? x : 0);
        System.out.println(false ? i : x);
    }
}

原因分析: 当条件表达式的一个操作数为T类型(byte/short/char中的一个),而另一个操作符为int类型的常量表达式时,返回值就是T类型;否则返回操作数提升后的类型。【根本原因是常量优化机制(只有int,不包括long类型)】

解决方案: 条件表达式中需使用相同的操作数类型

混合类型

(No.9) 赋值

典型案例:

short x = 0;
int i = 123456;
x += i; # IDE不会报错,但实际上也会溢出
x = x + i; # IDE会报错,提示无法转换

原因分析: E1 op= E2实际上等于E1 = (T)(E1 op E2),其中T代表E1的类型

解决方案: 在混用类型时不使用复合操作符,避免窄化转型

(No.24) 比较

典型案例: 无法打印"Joy"

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

原因分析: 实质上是进行了混合类型比较,导致操作数提升过程中出现了永远无法相等的情况。

解决方案: 要避免混合类型比较,因为它们内在地容易引起混乱;最好通过常量进行比较,因为编译器在常量声明并赋值的过程中会给出数值合法性提示。

if (b == (byte) 0x90)
    System.out.print("Joy!");

移位操作符

(No.27) 移动位数

典型案例: -1<<i永远不等于0,循环无法终止

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

原因分析: 在移位过程中,当左操作数为int类型时,移位操作符只使用其右操作数的低5位;当左操作数位long类型时,移位操作符使用其右操作数的低6位。因此,-1 << i最多能移动31位,永远无法等于0。

解决方法: 如果可能的话,移位长度应该是常量。

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

(No.31) 移位类型

典型案例: 以下代码会产生一个无限循环

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

原因分析: 移位操作符只支持intlong类型,如果左操作数是charshort或者byte,就会自动转型位int,从而引发了无限循环

字符之谜

(No.11) 字符串拼接

典型案例: 无法打印"HaHa"

public class LastLaugh {
    public static void main(String args[]) {
        System.out.print("H" + "a");
        System.out.print('H' + 'a');
    }
}

原因分析: 两个char类型数据相加会发生提升,得到int类型结果

解决方案: 当且仅当+操作符的操作数至少有一个是String时,才会执行字符串拼接操作

public class LastLaugh {
    public static void main(String args[]) {
        System.out.print("H" + "a");
        System.out.print("" + 'H' + 'a');
    }
}

(No.12) 数组打印

典型案例: int[]会打印出地址; char[]会打印出内容,但经过字符串拼接后打印也只能打印地址

public class Abc {
    public static void main(String[] args) {
        char[] letters = { 'a', 'b', 'c' };
        int[] numbers = { 1, 2, 3 };
        System.out.println(numbers);
        System.out.println(letters);
        System.out.println(letters + " is easy.");
    }
}

原因分析:

  1. 打印char[]类型是直接调用writeln(x); 打印其他数组类型调用的都是writeln(String.valueOf(String.valueOf(x)))
  2. String.valueOf()处理char[]返回的是new String(x); 处理其他数组类型都是调用(obj == null) ? "null" : obj.toString()
  3. 所有的数组类型的toString方法都是返回类名简写+地址的字符串
  4. 字符串拼接操作符的作用是先将前后操作数转换为字符串类型,再通过StringBuilder拼接

因此,numbers的打印会调用toString从而打印出类名简写+地址;letters通过writeln(x)会打印出内容;letters + " is easy."由于拼接过程中将letters转换为类名简写+地址,所以最后的打印结果仍然是类名简写+地址。

解决方案:

  1. 直接打印
    System.out.print(letters);
    System.out.println(" is easy.");
  2. 调用String.valueOf(char[])
    System.out.println(String.valueOf(letters) + " is easy.");

(No.13) 字符串比较

典型案例: 打印的是"Animals are equal: false"而非"Animals are equal: true"

public class AnimalFarm {
    public static void main(String[] args) {
        final String pig = "length: 10";
        final String dog = "length: " + pig.length();

        System.out.println("Animals are equal: "
                + (pig == dog));
    }
}

原因分析:

  1. java中的==操作符在对Object重载时统一选择的是比较引用地址
  2. String类型的编译期常量是内存限定的(自动把所有相同的字符串当作一个对象放入常量池),但需要使用常量表达式定义

因此,pigdog不指向相同的内存空间,因此返回false

解决方案: 不要依赖字符串常量的内存限定(这是java虚拟机的内存优化策略),而是应该使用equals直接比较值

System.out.println("Animals are equal: " + pig.equals(dog);

UniCode转义字符

(No.14) 预处理

典型案例: 并不会打印16,而是会打印2

public class EscapeRout {
  public static void main(String[] args) {
    // \u0022 is the Unicode escape for double-quote (")
    System.out.println("a\u0022.length() + \u0022b".length());
  }
}

原因分析: Java对在字符串字面常量中的Unicode转义字符没有提供任何特殊处理。编译器会直接将Unicode转义字符转化为它表达的字符。所以,"a\u0022.length() + \u0022b".length()就会被直接翻译为"a".length() + "b".length(),得到2

解决方法: 使用通用的转义字符\"【也可以使用八进制转义字符,如\012(\n)等】

System.out.println("a\".length() + \"b".length());

Unicode字符只是用在将非ASCII字符置于标识符、字符(串)字面常量以及注释中。

(No.15) 合法性要求

典型案例: 以下代码编译器会提示不合法

/**
 * Generated by the IBM IDL-to-Java compiler, version 1.0
 * from F:\TestRoot\apps\a1\units\include\PolicyHome.idl
 * Wednesday, June 17, 1998 6:44:40 o'clock AM GMT+00:00
 */
public class Test {
    public static void main(String[] args) {
        System.out.print("Hell");
        System.out.println("o world");
    }
}

原因分析: Unicode转义字符必须是良构的,即便在注释中也是如此。

解决方法:

  1. 在javadoc注释中,应该使用HTML转义字符而不是Unicode转义字符。
  2. 确保源码中的\u开头的字符串是Unicode转义字符并且合法。【尤其要注意windows路径】

(No.16) 换行符跨平台

典型案例: 以下代码在不同平台效果不一致,无法保证打印两行空行

public class LinePrinter {
  public static void main(String[] args) {
    char c = '\n';
    System.out.println(c);
  }
}

原因分析: Windows平台上的换行符使用CR字符(回车)和LF字符(换行)组成,而在Unix平台上只需要LF。

解决方法:

  1. 两个println
    System.out.println();
    System.out.println();
  2. 使用格式化字符串%n
    System.out.printf("%n%n");

(No.18) 字符集

典型案例: 会打印0, 1, 2, ... 127外加128个65533

public class StringCheese {
    public static void main(String args[]) {
        byte bytes[] = new byte[256];
        for (int i = 0; i < 256; i++)
            bytes[i] = (byte) i;
        String str = new String(bytes);
        for (int i = 0, n = str.length(); i < n; i++)
            System.out.print((int) str.charAt(i) + " ");
    }
}

原因分析:

  1. String(byte[])构造器内部调用的是this(bytes, offset, length, Charset.defaultCharset());,用途是使用默认字符集将字符的数字编码转化为字符。【Unix是"UTF-8",Windows则不是】
  2. 字符集的字符和字节序列之间并非一对一关系。
  3. 0~255转为byte时,128及以上都变成负值。
  4. String()构造器会对"UTF_8","ISO_8859_1","US_ASCII"进行分类讨论:
    • "UTF-8"不支持byte负值
    • 只有ISO-8859-1(Latin-1)才在0~255之间有完整的一对一映射。
  5. 对于不可映射到字符的字节,Java通常会使用特殊的替换字符(�, U+FFFD, 66533)来表示。

补充案例:

System.out.println((char) 128); // (0x80)
System.out.println((char) (byte) 128); // タ
System.out.println(new String(new byte[] { (byte) 128 })); // �

解决方法: 字节序列到字符的转换需要显式指定charset

String str = new String(bytes, "ISO-8859-1");

循环之谜

(No.25) 增量操作

典型案例: j无法按照预期的增加到100

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

原因分析: 后缀增量操作符的值等于自增前的值。因此,j = j++相当于int tmp = j; j = j + 1; j = tmp;

解决方法: 在单个表达式中不要对相同的变量赋值两次。

for (int i = 0; i < 100; i++)
    j++;

整型边界

(No.26) 溢出行为

典型案例: 循环无法终止

public class InTheLoop {
    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);
    }
}

原因分析: i作为int型变量,无法超过END【出现了溢出】

解决方法: 使用long类型的循环索引

for (long i = START; i <= END; i++)

补充案例: 利用溢出实现不终止的循环

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

(No.33) 最小的数

典型案例: 以下代码可以实现无限循环

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

原因分析: Integer.MIN_VALUE的补码是自身,因此i == -i

浮点数

(No.28) 浮点数间的空隙

典型案例: 以下代码实现了一个无限循环

double i = Double.POSITIVE_INFINITY;
while (i == i + 1) {
}

原因分析: 一旦相邻的浮点数值之间的距离(unit in the last place, ulp)大于2,那么对其中一个浮点数值加1将不会产生任何效果。换句话说,将一个很小的浮点数加到一个很大的浮点数上时,将不会改变大浮点数的值【二进制浮点算数只是对实际算数的一种近似】。

补充案例: 一下代码会输出0f++无效且初始时(float)f == START + 50

public class Count {
    public static void main(String[] args) {
        final int START = 2000000000;
        int count = 0;
        for (float f = START; f < START + 50; f++)
            count++;
        System.out.println(count);
    }
}

(No.29) 浮点数中的NaN

典型案例: 以下代码实现了一个无限循环

double i = 0.0 / 0;
while (i != i) {
}

原因分析: float和double中都保留了一个特殊的NaN值,用来表示不是数字的数量。NaN不等于任何浮点数,包括它自身。任何浮点操作,只要它的一个或多个操作数为NaN,那么其结果为NaN。例如i - i = 0当且仅当i等于NaN时不成立。

(No.30) 字符串的连接

典型案例: 以下代码可以实现无限循环

String i = "Hello";
while (i != i + 0) {
}

原因分析: 字符串连接

(No.32) 包装类型比较

典型案例: 以下代码可以实现无限循环

Integer i = Integer.valueOf(199);
Integer j = Integer.valueOf(199);
while (i <= j && j <= i && i != j) {
}

原因分析:

  1. 包装类型的引用间的比较不会自动解包,但包装类型和非包装类型之间的比较会自动解包变成数值比较【历史遗留问题】
  2. 注意包装类常量池的范围

异常之谜

(No.36) finally的意外结束

典型案例: 以下代码输出的是false

public class Indecisive {
    public static void main(String[] args) {
        System.out.println(decision());
    }

    static boolean decision() {
        try {
            return true;
        } finally {
            return false;
        }
    }
}

原因分析: finally在控制权离开try时执行,无论try是正常结束还是意外结束。 - 意外结束: 出现异常/throwbreakcontinuereturn【程序无法顺序执行】 - 当tryfinally都意外结束时,try中引发意外结束的原因将被丢弃。

解决方法: 确保finally可以正常结束,且如果finally语句中如果有受检查异常要捕捉而不能任其传播。

(No.37) 异常捕捉编译错误

典型案例:

  1. 以下代码会编译报错: "Unreachable catch block for IOException. This exception is never thrown from the try statement body."
    import java.io.IOException;
    
    public class Arcane1 {
        public static void main(String[] args) {
            try {
                System.out.println("Hello world");
            } catch(IOException e) {
                System.out.println("I've never seen println fail!");
            }
        }
    }
  2. 以下代码可以正常执行
    public class Arcane2 {
        public static void main(String[] args) {
            try {
                // If you have nothing nice to say, say nothing
            } catch(Exception e) {
                System.out.println("This can't happen");
            }
        }
    }
  3. 以下代码可以正常执行
    interface Type1 {
        void f() throws CloneNotSupportedException;
    }
    
    interface Type2 {
        void f() throws InterruptedException;
    }
    
    interface Type3 extends Type1, Type2 {
    }
    
    public class Arcane3 implements Type3 {
        public void f() {
            System.out.println("Hello world");
        }
    
        public static void main(String[] args) {
            Type3 t3 = new Arcane3();
            t3.f();
        }
    }

原因分析:

  1. 案例一: 如果catch要捕捉一个受检查异常,而对应的try子句不能抛出这一类型的异常,就会产生一个编译错误。
  2. 案例二: Exception不是受检查异常,不需要在子句中捕捉。
  3. 案例三: 一个方法可以抛出的受检查异常集合是它所适用的所有声明要抛出的受检查异常集合的交集。

(No.38) final静态域的初始化

典型案例: 静态域不可以抛出受检查异常,因此需要用catch直接捕捉错误,但以下代码会提示USER_ID可能已经被赋值,导致编译不通过。

public class UnwelcomeGuest {
    public static final long GUEST_USER_ID = -1;

    private static final long USER_ID;
    static {
        try {
            USER_ID = getUserIdFromEnvironment();
        } catch (IdUnavailableException e) {
            USER_ID = GUEST_USER_ID;
            System.out.println("Logging in as guest");
        }
    }

    private static long getUserIdFromEnvironment()
            throws IdUnavailableException {
        throw new IdUnavailableException(); // Simulate an error
    }

    public static void main(String[] args) {
        System.out.println("User ID: " + USER_ID);
    }
}

class IdUnavailableException extends Exception {
}

原因分析: 一个空final域只有在他的确未赋值的时候才能被赋值,但要确定一个final域是否已被赋值是很困难的,因此编译器只能采取保守策略,拒绝某些可以被证明是安全的程序。

解决方案: 直接用一个静态域操作/方法替换初始化语句块【final类型就是要立刻赋值】

public class UnwelcomeGuest {
    public static final long GUEST_USER_ID = -1;

    private static final long USER_ID = getUserIdOrGuest();

    private static long getUserIdOrGuest() {
        try {
            return getUserIdFromEnvironment();
        } catch (IdUnavailableException e) {
            System.out.println("Logging in as guest");
            return GUEST_USER_ID;
        }
    }

    private static long getUserIdFromEnvironment()
            throws IdUnavailableException {
        throw new IdUnavailableException(); // Simulate an error
    }

    public static void main(String[] args) {
        System.out.println("User ID: " + USER_ID);
    }
}

class IdUnavailableException extends Exception {
}

(No.39) System.exit

典型案例: 以下代码不会输出"Goodbye world"

public class HelloGoodbye {
    public static void main(String[] args) {
        try {
            System.out.println("Hello world");
            System.exit(0);
        } finally {
            System.out.println("Goodbye world");
        }
    } 
}

原因分析: System.exit方法将停止当前线程和所有其他当场死亡的线程【try语句根本没机会正常结束或意外结束】

解决方法: 可以将需要执行的操作注册到shutdownHook上,这样使用System.exit时会自动调用shutdownHook【如果不想调用shutdownHook,就可以使用System.halt

public class HelloGoodbye {
    public static void main(String[] args) {
        System.out.println("Hello world");
        Runtime.getRuntime().addShutdownHook(new Thread() {
            public void run() {
                System.out.println("Goodbye world");
            }
        });
        System.exit(0);
    }
}

(No.40) 构造器的异常抛出

典型案例: 以下代码会栈溢出

public class Reluctant {
    private Reluctant internalInstance = new Reluctant();

    public Reluctant() throws Exception {
        throw new Exception("I'm not coming out");
    }

    public static void main(String[] args) {
        try {
            Reluctant b = new Reluctant();
            System.out.println("Surprise!");
        } catch (Exception ex) {
            System.out.println("I told you so");
        }
    }
}

原因分析: 实例变量的初始化操作将优先于构造器的程序体而运行,导致无限递归,而StackOverflowErrorError的子类型而不是Exception的子类型,因此不会被捕捉。

解决方法: 如果构造对象时使用了类本身,要避免无限递归的情况发生。

(No.41) 流的关闭

典型案例: 以下无法做到总是关闭创建的每个流

import java.io.*;

public class Copy {
    static void copy(String src, String dest) throws IOException {
        InputStream in = null;
        OutputStream out = null;
        try {
            in = new FileInputStream(src);
            out = new FileOutputStream(dest);
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) > 0)
                out.write(buf, 0, n);
        } finally {
            if (in != null) in.close();
            if (out != null) out.close();
        } 
    }

    public static void main(String[] args) throws IOException {
        if (args.length != 2)
            System.out.println("Usage: java Copy <source> <dest>");
        else
            copy(args[0], args[1]);
    }
}

原因分析: in.close()如果关闭出现异常,这个异常就会组织out.close()被调用,从而导致输出流一直保持开放状态

解决方法: 使用try-with-resource语句

(No.42) &引发的异常

典型案例: 以下代码并不能按预期的打印"2",而是直接报错ArrayIndexOutOfBoundsException错误

public class Loop {
    public static void main(String[] args) {
        int[][] tests = { { 6, 5, 4, 3, 2, 1 }, { 1, 2 }, { 1, 2, 3 }, { 1, 2, 3, 4 }, { 1 } };
        int n = 0;
        for (int i = 0; i < tests.length; i++) {
            if (thirdElementIsThree(tests[i])) {
                n++;
            }
        }
        System.out.println(n);
    }
    private static boolean thirdElementIsThree(int[] a) {
        return a.length >= 3 & a[2] == 3;
    }
}

原因分析: &并不是短路与,左右两个表达式都会执行【一般很少使用&,如果真的要用,请一定要加注释】

(No.45) 持续爆栈

典型案例: 以下代码会无限运行

public class Workout {
    public static void main(String[] args) {
        workHard();
        System.out.println("It's nap time.");
    }

    private static void workHard() {
        try {
            workHard();
        } finally {
            workHard();
        }
    }
}

原因分析:

  1. workHard引发爆栈错误时,会被try捕捉,执行finally
  2. tryfinally都意外结束时,try中引发意外结束的原因将被丢弃
  3. finally中的错误上抛时,会降低栈的深度,从而使得上一层的finally继续运行,使得workHard的调用图类似一个二叉树
  4. JVM的默认栈深度为1024,这使得形成调用数量近乎无限,看起来就像是无限运行

类之谜

构造器

(No.46) 令人疑惑的构造器

典型案例: 以下代码会依次打印double arrayObject

public class Confusing {
    private Confusing(Object o) {
        System.out.println("Object");
    }

    private Confusing(double[] dArray) {
        System.out.println("double array");
    }

    public static void main(String[] args) {
        new Confusing(null);
        new Confusing((Object) null);
    }
}

原因分析: Java的构造器重载分为两个阶段,第一阶段选取可获得并且可应用的构造器,第二阶段在第一阶段的构造器中选取最精准的一个。

解决方法:

  1. 确保所有的重载版本所接受的参数类型都互不相容,这样不会存在同时可应用的构造方法
  2. 如果存在了兼容的重载方法,传参时就要强制转型
  3. 如果重载需求比较大,推荐将构造器设置为私有的并提供公有的静态工厂

(No.51) 在父类构造器中调用方法

典型案例: 以下代码会打印"[4,2]:null"

class Point {
    protected final int x, y;
    private final String name; // Cached at construction time

    Point(int x, int y) {
        this.x = x;
        this.y = y;
        name = makeName();
    }

    protected String makeName() {
        return "[" + x + "," + y + "]";
    }

    public final String toString() {
        return name;
    }
}

public class ColorPoint extends Point {
    private final String color;

    ColorPoint(int x, int y, String color) {
        super(x, y);
        this.color = color;
    }

    protected String makeName() {
        return super.makeName() + ":" + color;
    }

    public static void main(String[] args) {
        System.out.println(new ColorPoint(4, 2, "purple"));
    }
}

原因分析: 子类在被读取到方法区后,就已经覆盖了makeName()方法,因此,在父类构造器被子类构造器调用时,使用的makeName方法就已经是被覆盖的了。而此时color变量还未被赋值,因此是null,所以最终输出的name是"[4,2]:null"。

解决方法: 不要在构造器中调用可覆盖的方法

(No. 53) 子类向父类构造方法传递实例属性

典型案例: 以下代码无法通过编译,并会报错"Cannot refer to an instance field arg while explicitly invoking a constructor"

public class MyThing extends Thing {
    private final int arg;

    public MyThing() {
        super(arg = (int) System.currentTimeMillis());
    }
}

class Thing {
    public Thing(int i) {
    }
}

解决方法:

  • 直觉是使用静态变量代替,但这样做在多线程场景下需要对源码做很多的修改【根本原因是静态变量内存中只有一份】
    public class MyThing extends Thing {
        private static final int arg = (int) System.currentTimeMillis();
    
        public MyThing() {
            super(arg);
        }
    }
    
    class Thing {
        public Thing(int i) {
        }
    }
  • 推荐的方法是使用交替构造器调用机制
    public class MyThing extends Thing {
        private int arg;
    
        public MyThing() {
            super((int) System.currentTimeMillis());
        }
    
        public MyThing(int i) {
            super(i);
            arg = i;
        }
    }
    
    class Thing {
        public Thing(int i) {
        }
    }

静态方法

(No.48) 静态方法的调用

典型案例: 以下代码会打印"woof woof"

class Dog {
    public static void bark() {
        System.out.print("woof ");
    }
}

class Basenji extends Dog {
    public static void bark() {
    }
}

public class Bark {
    public static void main(String args[]) {
        Dog woofer = new Dog();
        Dog nipper = new Basenji();
        woofer.bark();
        nipper.bark();
    }
}

原因分析: 静态方法的调用不存在任何动态的分派机制,换句话说,调用什么方法完全是在编译期决定的,只会考虑编译时的类型。

解决方法: 所有的静态方法调用都要使用类名,而非表达式/变量名,这样可以避免很多误解。

(No.54) 用null调用静态方法

典型案例: 以下代码居然可以正确输出"Hello world!"

public class Null {
    public static void greet() {
        System.out.println("Hello world!");
    }

    public static void main(String[] args) {
        ((Null) null).greet();
    } 
}

原因分析: 静态方法调用的限定表达式是可以计算的,但它的值会被忽略。

静态变量(静态域)

(No.47) 共享的静态域

典型案例: 以下代码会打印"5 woofs and 5 meows"

class Counter {
    private static int count = 0;
    public static final synchronized void increment() {
        count++;
    }
    public static final synchronized int getCount() {
        return count; 
    } 
}
class Dog extends Counter {
    public Dog() { }
    public void woof() { increment(); }
}
class Cat extends Counter {
    public Cat() { } 
    public void meow() { increment(); }
}
public class Ruckus {
    public static void main(String[] args) { 
        Dog dogs[] = { new Dog(), new Dog() };
        for (int i = 0; i < dogs.length; i++)
            dogs[i].woof();
        Cat cats[] = { new Cat(), new Cat(), new Cat() };
        for (int i = 0; i < cats.length; i++)
            cats[i].meow();
        System.out.print(Dog.getCount() + " woofs and ");
        System.out.println(Cat.getCount() + " meows");
    }
}

原因分析: 每一个静态域在声明它的类及其所有子类中共享一份单一的拷贝

解决方法: 根据一个类的每个实例都是另一个类的实例,还是都有另一个类的一个实例,决定该用继承还是组合/聚合,一般优选组合/聚合而不是继承。

(No.49) 静态域的初始化

典型案例: 以下代码会打印"Elvis wears a size -1930 belt."

import java.util.Calendar;
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private final int beltSize;
    private static final int CURRENT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
    private Elvis() {
        beltSize = CURRENT_YEAR - 1930;
    }
    public int beltSize() {
        return beltSize;
    }
    public static void main(String[] args) {
        System.out.println("Elvis wears a size " + INSTANCE.beltSize() + " belt.");
    }
}

原因分析: 静态域字段的初始化是先开辟空间,放入缺省值,然后再依次初始化【包括final字段也是如此】

解决方法: 需要对静态域的字段重新排序,使得被依赖的字段出现在前面。

(No. 52) 积极初始化与惰性初始化的混乱

典型案例: 以下代码不会打印"4950",而是打印"9900"

class Cache {
    static {
        initializeIfNecessary();
    }

    private static int sum;

    public static int getSum() {
        initializeIfNecessary();
        return sum;
    }

    private static boolean initialized = false;

    private static synchronized void initializeIfNecessary() {
        if (!initialized) {
            for (int i = 0; i < 100; i++)
                sum += i;
            initialized = true;
        }
    }
}

public class Client {
    public static void main(String[] args) {
        System.out.println(Cache.getSum());
    }
}

原因分析: 在调用静态方法前,随着类加载到内存,会执行一次静态域初始化,而以上代码中的静态代码块会对静态变量惰性初始化,随后积极初始化会覆盖惰性初始化的值,导致initializeIfNecessary()被执行了两遍,引发错误。

解决方法: 要么使用积极初始化,要么使用惰性初始化;一般都推荐积极初始化,因为更容易把控顺序。

类型转换

(No.50) instanceof和转型的冷僻案例

典型案例:

  • 以下代码会打印"false"
    public class Type1 {
        public static void main(String[] args) {
            String s = null;
            System.out.println(s instanceof String);
        }
    }
  • 以下代码编译期就会报错
    public class Type2 {
        public static void main(String[] args) {
            System.out.println((new Type2()) instanceof String);
        }
    }
  • 以下代码运行时报错
    public class Type3 {
        public static void main(String args[]) {
            Type3 t2 = (Type3) new Object();
        }
    }

原因分析:

  • 案例1: instanceof除了在多态时使用来将父类转型为子类外,还被定义为当左操作数为null时,返回false【非常符合直觉】
  • 案例2: instanceof有明确的使用场景,并不是用来随意判断某个对象对应类是否存在某个父类的
  • 案例3: 冷僻案例,原因是编译期不够智能

(No.55) 实例创建

典型案例: 以下代码甚至不能通过编译

public class Creator {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++)
            Creature creature = new Creature();
        System.out.println(Creature.numCreated());
    }
}

class Creature {
    private static long numCreated = 0;

    public Creature() {
        numCreated++;
    }

    public static long numCreated() {
        return numCreated;
    }
}

原因分析: Java不允许一个本地变量声明语句作为一条语句在for、while和do中重复执行。一个本地变量声明作为一条语句只能出现在一个语句块中。

解决方法:

  1. 加上大括号或者去掉声明
  2. 最好顺带考虑下线程安全(例如加上synchronized)

(No.67) 隐藏

典型案例: 以下代码报错"className has private access in Derived"

class Base {
    public String className = "Base";
}
class Derived extends Base {
    private String className = "Derived";

}
public class PrivateMatter {
    public static void main(String[] args) {
        System.out.println(new Derived().className);
    }
}

原因分析: 如果className是实例方法,那么这段代码会因为访问权限问题而不能编译;但className作为域,发生的其实是隐藏,但由于访问权限为private,这才引发了错误

解决方法: 在Java中,尽量避免隐藏【很容易违反置换原则】

(No.68, No.69) 遮掩

典型案例: 以下代码会打印"White"

public class ShadesOfGray {
    public static void main(String[] args){
        System.out.println(X.Y.Z);
    }
}
class X {
    static class Y {
        static String Z = "Black";
    }
    static C Y = new C();
}
class C {
    String Z = "White";
}

原因分析: 当一个变量名和一个类型具有相同的名字,并且他们位于相同的作用域,变量名遮掩(obsure)类型【变量名 > 类型名 > 包名】

解决方法:

  • 规范化命名【单个的大写字母只能用于范型】
  • 如果一定要在不修改类定义的情况下引用被遮掩的类名,可以选择在某个特殊的语法上下文中使用该名字
    public static void main(String[] args){
        System.out.println(((X.Y) null).Z);
    }
    public class ShadesOfGray {
        static class Xy extends X.Y {}
        public static void main(String[] args){
            System.out.println(X.Y.Z);
        }
    }

(No.71) 遮蔽

典型案例: 以下代码会报错"method toString in class Object cannot be applied to given types"

import static java.util.Arrays.toString;

class ImportDuty {
    public static void main(String[] args) {
        printArgs(1, 2, 3, 4, 5);
    }

    static void printArgs(Object... args) {
        System.out.println(toString(args));
    }

}

原因分析: 导入的toStringImportDutyObject继承来的toString遮蔽(shadow)了【本身就属于某个范围的成员在该范围内与静态导入相比具有优先权】

库之谜

BigInteger

(No.56) BigInteger的操作返回值

典型案例: 以下代码只会打印"0"

import java.math.BigInteger;
public class BigProblem {
    public static void main(String[] args) {
        BigInteger fiveThousand = new BigInteger("5000");
        BigInteger fiftyThousand = new BigInteger("50000");
        BigInteger fiveHundredThousand = new BigInteger("500000");

        BigInteger total = BigInteger.ZERO;
        total.add(fiveThousand);
        total.add(fiftyThousand);
        total.add(fiveHundredThousand);
        System.out.println(total);
    }
}

原因分析: BigInteger实例是不可变的,每次操作的都将返回新的实例【或许plusminus才是更好的名字】

解决方法:

BigInteger fiveThousand = new BigInteger("5000");
BigInteger fiftyThousand = new BigInteger("50000");
BigInteger fiveHundredThousand = new BigInteger("500000");

BigInteger total = BigInteger.ZERO;
total = total.add(fiveThousand);
total = total.add(fiftyThousand);
total = total.add(fiveHundredThousand);
System.out.println(total);

HashSet

(No.57) hashCode

典型案例: 以下代码不能打印true

import java.util.*;

public class Name {
    private String first, last;

    public Name(String first, String last) {
        this.first = first;
        this.last = last;
    }

    public boolean equals(Object that) {
        if (!(that instanceof Name))
            return false;
        Name n = (Name) that;
        return n.first.equals(first) && n.last.equals(last);
    }

    public static void main(String[] args) {
        Set<Name> s = new HashSet<Name>();
        s.add(new Name("Mickey", "Mouse"));
        System.out.println(
                s.contains(new Name("Mickey", "Mouse")));
    }
}

原因分析: 没有实现hashCode()方法

解决方法: 使用Objects自带的hashCode

@Override
public int hashCode() {
    return Objects.hash(first, last);
}

(No.58) equals

典型案例: 以下代码不能打印true

import java.util.*;

public class Name {
    private String first, last;

    public Name(String first, String last) {
        this.first = first; this.last = last;
    }

    public boolean equals(Name n) {
        return n.first.equals(first) && n.last.equals(last);
    }

    public int hashCode() {
        return 31 * first.hashCode() + last.hashCode(); 
    }

    public static void main(String[] args) {
        Set<Name> s = new HashSet<Name>();
        s.add(new Name("Donald", "Duck"));
        System.out.println(
            s.contains(new Name("Donald", "Duck")));
    }
}

原因分析: 实现的equals(Name n)是重载的而非覆盖的

解决方法: 为了避免无意识的重载,每次覆盖的时候,都要拷贝其超类上的方法声明,并添加上@Override注解来让编译器检查

@Override
public boolean equals(Object o) {
    return o instanceof Name && equals((Name) o);
}

Math

(No.64) Math.abs

典型案例: 以下代码会直接报索引负数越界

public class Mod {
    public static void main(String[] args) {
        final int MODULUS = 3;
        int[] histogram = new int[MODULUS];

        // Iterate over all ints (Idiom from Puzzle 25)
        int i = Integer.MIN_VALUE;
        do {
            histogram[Math.abs(i) % MODULUS]++;
        } while (i++ != Integer.MAX_VALUE);

        for (int j = 0; j < MODULUS; j++)
            System.out.println(histogram[j] + " ");
    } 
}

原因分析: Integer.MIN_VALUE的绝对值无法用Integer表达

Array

(No.65) Array.sort中的Comparator

典型案例: 以下代码会打印"UNORDERED"

import java.util.*;

public class SuspiciousSort {
    public static void main(String[] args) {
        Random rnd = new Random();
        Integer[] arr = new Integer[100];

        for (int i = 0; i < arr.length; i++)
            arr[i] = rnd.nextInt();

        Comparator<Integer> cmp = new Comparator<Integer>() {
            public int compare(Integer i1, Integer i2) {
                return i2 - i1;
            }
        };
        Arrays.sort(arr, cmp);
        System.out.println(order(arr));
    }

    enum Order {
        ASCENDING, DESCENDING, CONSTANT, UNORDERED
    };

    static Order order(Integer[] a) {
        boolean ascending = false;
        boolean descending = false;

        for (int i = 1; i < a.length; i++) {
            ascending |= a[i] > a[i - 1];
            descending |= a[i] < a[i - 1];
        }

        if (ascending && !descending)
            return Order.ASCENDING;
        if (descending && !ascending)
            return Order.DESCENDING;
        if (!ascending)
            return Order.CONSTANT; // All elements equal
        return Order.UNORDERED; // Array is not sorted
    }
}

原因分析: 用相减的方法比较可能会溢出

多线程

(No.76) 用synchronized实现"单线程"

典型案例: 一下代码永远会输出"PingPong"

public class PingPong {
    public static synchronized void main(String[] a) {
        Thread t = new Thread() {
            public void run() {
                pong();
            }
        };
        t.start();
        System.out.print("Ping");
    }

    static synchronized void pong() {
        System.out.print("Pong");
    }
}

原因分析: 由于main方法在创建第二个线程前就拥有了PingPong.class的管程锁,因此只能等main执行完再执行pong

(No.78)join与锁

典型案例: 以下代码不会终止

import java.util.*;

public class Worker extends Thread {
    private volatile boolean quittingTime = false;

    public void run() {
        while (!quittingTime)
            pretendToWork();
        System.out.println("Beer is good");
    }

    private void pretendToWork() {
        try {
            Thread.sleep(300); // Sleeping on the job?
        } catch (InterruptedException ex) {
        }
    }

    // It's quitting time, wait for worker - Called by good boss
    synchronized void quit() throws InterruptedException {
        quittingTime = true;
        join();
    }

    // Rescind quitting time - Called by evil boss
    synchronized void keepWorking() {
        quittingTime = false;
    }

    public static void main(String[] args) throws InterruptedException {
        final Worker worker = new Worker();
        worker.start();

        Timer t = new Timer(true); // Daemon thread
        t.schedule(new TimerTask() {
            public void run() {
                worker.keepWorking();
            }
        }, 500);

        Thread.sleep(400);
        worker.quit();
    }
}

原因分析: 大致在400毫秒时,在主线程中work.quit()方法被调用,在设置完quittingTime = true后,执行[this.]join()方法,背后的含义是让main使用this.wait()交出控制权,等待Thread-0结束后notify回来;到了500毫秒时,Timer启动,又把quittingTime置为false,从而导致代码无法终止。根本原因在于这段代码里的锁会被三者访问(mainthread-0timer-0)

Thread中的quit->quit(0)方法效果如下图:

解决方法:

  • maintimer-0之间创建一个锁
    import java.util.*;
    
    public class Worker extends Thread {
        private volatile boolean quittingTime = false;
        private final Object lock = new Object();
    
        public void run() {
            while (!quittingTime)
                pretendToWork();
            System.out.println("Beer is good");
        }
    
        private void pretendToWork() {
            try {
                Thread.sleep(300); // Sleeping on the job?
            } catch (InterruptedException ex) {
            }
        }
    
        // It's quitting time, wait for worker - Called by good boss
        void quit() throws InterruptedException {
            synchronized (lock) {
                quittingTime = true;
                join();
            }
        }
    
        // Rescind quitting time - Called by evil boss
        synchronized void keepWorking() {
            quittingTime = false;
        }
    
        public static void main(String[] args)
                throws InterruptedException {
            final Worker worker = new Worker();
            worker.start();
    
            Timer t = new Timer(true); // Daemon thread
            t.schedule(new TimerTask() {
                public void run() {
                    synchronized (worker.lock) {
                        worker.keepWorking();
                    }
                }
            }, 500);
    
            Thread.sleep(400);
            worker.quit();
        }
    }
  • 改造ThreadRunnable,也分离为两把锁
    import java.util.*;
    
    public class Worker implements Runnable {
        private volatile boolean quittingTime = false;
    
        public void run() {
            while (!quittingTime) {
                System.out.println("开始一轮摸鱼");
                pretendToWork();
            }
    
            System.out.println("Beer is good");
        }
    
        private void pretendToWork() {
            try {
                Thread.sleep(300);
            } catch (InterruptedException ex) {
            }
        }
    
        synchronized void quit(Thread t) throws InterruptedException {
            quittingTime = true;
            t.join();
        }
    
        synchronized void keepWorking() {
            quittingTime = false;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Worker worker = new Worker();
            Thread t1 = new Thread(worker);
            t1.start();
    
            Timer t = new Timer(true);
            t.schedule(new TimerTask() {
                public void run() {
                    worker.keepWorking();
                }
            }, 500);
    
            Thread.sleep(400);
            worker.quit(t1);
        }
    }

(No.79)Thread与遮蔽

典型案例: 以下代码会导致编译错误

public class Pet {
    public final String name;
    public final String food;
    public final String sound;

    public Pet(String name, String food, String sound) {
        this.name = name;
        this.food = food;
        this.sound = sound;
    }

    public void eat() {
        System.out.println(name + ": Mmmmm, " + food);
    }
    public void play() {
        System.out.println(name + ": " + sound + " " + sound);
    }
    public void sleep() {
        System.out.println(name + ": Zzzzzzz...");
    }

    public void live() {
        new Thread() {
            public void run() {
                while (true) {
                    eat();
                    play();
                    sleep();
                }
            }
        }.start();
    }

    public static void main(String[] args) {
        new Pet("Fido", "beef", "Woof").live();
    }
}

原因分析: Pet中的sleep方法被Thread内的sleep方法遮蔽(shadow),但Thread没有无参重载,因此会报错。

解决方法: 重命名Pet中的sleep或者使用Thread(Runnable)构造器创建线程。

(No.85) 类的初始化

典型案例: 以下代码会被挂起

public class Lazy {
    private static boolean initialized = false;

    static {
        Thread t = new Thread(new Runnable() {
            public void run() {
                initialized = true;
            }
        });
        t.start();
        try {
            t.join();
        } catch(InterruptedException e) {
            throw new AssertionError(e);
        }
    }

    public static void main(String[] args) {
        System.out.println(initialized);
    }
}

原因分析:

  • 主线程调用main方法时,会等待Lazy初始化,其静态代码块中存在一个子进程,并调用了join,需要等子进程结束才能完成初始化
  • 由于这个子进程也调用了Lazy的属性,所以也要等待Lazy初始化,这就形成了死锁

(No.81) PrintStream的刷新机制

典型案例: 以下代码不会打印任何东西

public class Greeter {
    public static void main (String[] args) {
        String greeting = "Hello world";
        for (int i = 0; i < greeting.length(); i++)
            System.out.write(greeting.charAt(i));
    }
}

原因分析:

  1. PrintStream可以被创建为自动刷新的,System.outSystem.err所引用的就是这样的
  2. 自动刷新机制在以下情况启动
    • 写入字节数组、换行字符或者'\n'
    • 调用println方法
    • 调用print(String), print(char)【事实上如此,但Java-API不是这么说的】

(No.91) 反序列化中的顺序

典型案例: 以下代码会报"AssertionError: invariant violated"

import java.util.*;
import java.io.*;

public class SerialKiller {
    public static void main(String[] args) {
        Sub sub = new Sub(666); 
        sub.checkInvariant();

        Sub copy = (Sub) deepCopy(sub);
        copy.checkInvariant();
    }

    // Copies its argument via serialization (See Puzzle 80)
    static public Object deepCopy(Object obj) {
        try {
            ByteArrayOutputStream bos = 
                new ByteArrayOutputStream();
            new ObjectOutputStream(bos).writeObject(obj);
            ByteArrayInputStream bin =
                new ByteArrayInputStream(bos.toByteArray());
            return new ObjectInputStream(bin).readObject(); 
        } catch(Exception e) {
            throw new IllegalArgumentException(e); 
        }
    }
}

class Super implements Serializable {
    final Set<Super> set = new HashSet<Super>();
} 

final class Sub extends Super {
    private int id;
    public Sub(int id) {
        this.id = id;
        set.add(this); // Establish invariant
    }

    public void checkInvariant() {
        if (!set.contains(this))
            throw new AssertionError("invariant violated");
    }

    public int hashCode() {
        return id;
    }

    public boolean equals(Object o) {
        return (o instanceof Sub) && (id == ((Sub)o).id);
    }
}

原因分析: 类似No.51,也是父类使用了子类属性的默认值。HashSet在反序列化时其唯一的元素(Sub实例)的hashCode0,而检验时的hashCode666【需要更加小心的使用java的反序列化】

高级谜题

反射

(No.78) 访问权限

典型案例: 以下代码会报错"IllegalAccessException: class Reflector cannot access a member of class java.util.HashMap$HashIterator..."

import java.util.*;
import java.lang.reflect.*;

public class Reflector {
    public static void main(String[] args) throws Exception {
        Set<String> s = new HashSet<String>();
        s.add("foo");
        Iterator<String> it = s.iterator();
        Method m = it.getClass().getMethod("hasNext");
        System.out.println(m.invoke(it));
    }
}

原因分析: it.getClass()java.util.HashMap$KeyIterator,这是个包私有的方法,而访问位于其他包中的非公共类型的成员是不合法的。这个错误并不直观,但可以看以下案例:

Iterator<String> it1 = s.iterator(); // 没有问题
HashMap.KeyIterator<String> it2 = s.iterator(); // 会报不可见编译错误

解决方法: 直接使用public类的.class获得Method类【推荐只有实例化时才使用反射获得构造方法,而方法调用则都通过接口.class进行】

Method m = Iterator.class.getMethod("hasNext");

(No.80) 成员内部类的构造器

典型案例: 以下代码编译报错"java.lang.InstantiationException: Outer$Inner"

public class Outer {
    public static void main(String[] args) throws Exception {
        new Outer().greetWorld();
    }

    private void greetWorld() throws Exception {
        System.out.println(Inner.class.newInstance());
    }

    public class Inner {
        public String toString() {
            return "Hello world";
        }
    }
}

原因分析:

  • Class.newInstance只在以下情况下报错:
    • Class对应的是抽象类、接口、数组类、基本类型或为空
    • 这个类没有任何无参构造器
    • 实例化由于某些其他原因而失败
  • 成员内部类并不像正常的类那样在没有显式无参构造时会自带缺省的无参构造,换句话说,以下代码会报"java.lang.NoSuchMethodException: Outer$Inner.<init>()"错误
    System.out.println(Inner.class.getConstructor());

解决方法: 获取带有Outer类对象的构造器,并传递Outer.this获取Inner实例

Constructor<Inner> c = Inner.class.getConstructor(Outer.class);
System.out.println(c.newInstance(Outer.this));

注意事项:

  1. 优先使用静态内部类而不是成员内部类
  2. 避免用反射来实例化内部类(反射位于虚拟机层次,存在很多不容易理解的细节)

范型

(No.88) 原生类型

典型案例: 以下代码会直接编译报错"incompatible types: Object cannot be converted to String"

import java.util.*;

public class Pair<T> {
    private final T first;
    private final T second;

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T first() {
        return first;
    }

    public T second() {
        return second;
    }

    public List<String> stringList() {
        return Arrays.asList(String.valueOf(first),
                String.valueOf(second));
    }

    public static void main(String[] args) {
        Pair p = new Pair<Object>(23, "skidoo");
        System.out.println(p.first() + " " + p.second());
        System.out.println(p.stringList() instanceof List);
        for (String s : p.stringList())
            System.out.print(s + " ");
    }
}

原因分析: 出于兼容性考量,Java保留了原生类型(Raw Types),但带来的问题是,new Pair<Object>(23, "skidoo")被声明为原生类型Pair后,其中的所有参数化的类型都被擦除,这就使得stringList()返回的类型为List,因此遍历它需要的循环变量应该是Object【需要注意的是,List不等于List<Object>,它不是类型安全的】

其他案例: T getAnnotation(Class<T> annotationClass)需要通过范型实现,如果调用这个方法的对象(例如Class)是原生类型,就会擦除所有的参数化类型,引发类型不匹配错误

(No.89) 范型类中的内部类

典型案例: 以下代码会编译报错"incompatible types: LinkedList<E#1>.Node<E#1> cannot be converted to LinkedList<E#1>.Node<E#2>"

public class LinkedList<E> {
    private Node<E> head = null;

    private class Node<E> {
        E value;
        Node<E> next;

        // Node constructor links the node as a new head
        Node(E value) {
            this.value = value;
            this.next = head;
            head = this;
        }
    }

    public void add(E e) {
        new Node<E>(e);
        // Link node as new head
    }

    public void dump() {
        for (Node<E> n = head; n != null; n = n.next)
            System.out.println(n.value + " ");
    }

    public static void main(String[] args) {
        LinkedList<String> list = new LinkedList<String>();
        list.add("world");
        list.add("Hello");
        list.dump();
    }
}

原因分析: 定义了两个同名的范型,但本质上并不是同一个

解决方法:

  • 直接去掉Node类上的范型
  • 修改成员内部类为静态内部类
    public class LinkedList<E> {
        private Node<E> head = null;
        private static class Node<T> {
            T value;
            Node<T> next;
    
            Node(T value, Node<T> next) {
                this.value = value;
                this.next = next;
            }
        }
        public void add(E e) {
            head = new Node<E>(e, head);
        }
        public void dump() {
            for (Node<E> n = head; n != null; n = n.next)
                System.out.println(n.value + " ");
        }
        public static void main(String[] args) {
            LinkedList<String> list = new LinkedList<String>();
            list.add("world");
            list.add("Hello");
            list.dump();
        }
    }

内部类

(No.92) 匿名内部类的继承

典型案例: 以下代码会打印main

public class Twisted {
    private final String name;

    Twisted(String name) {
        this.name = name;
    }

    private String name() {
        return name;
    }

    private void reproduce() {
        new Twisted("reproduce") {
            void printName() {
                System.out.println(name());
            }
        }.printName();
    }

    public static void main(String[] args) {
        new Twisted("main").reproduce();
    }
}

原因分析: 私有成员name()不会被继承,因此匿名内部类中调用的name()其实就是外层的name()

二进制兼容性

(No.93) 常量变量的使用

典型案例: 先编译好WordsPrintWords,再对Words修改后重新编译,之后执行PrintWord中的main会打印"the chemistry set"

public class Words {
    private Words() {
    }; // Uninstantiable

    public static final String FIRST = "the";
    public static final String SECOND = null;
    public static final String THIRD = "set";
}

// The second version of the Words class appears below (commented out)
//
// public class Words {
// private Words() {
// }; // Uninstantiable

// public static final String FIRST = "physics";
// public static final String SECOND = "chemistry";
// public static final String THIRD = "biology";
// }
public class PrintWords {
    public static void main(String[] args) {
        System.out.println(Words.FIRST + " " +
                Words.SECOND + " " +
                Words.THIRD);
    }
}

原因分析: 可以从字节码的角度来看

// Source code is decompiled from a .class file using FernFlower decompiler.
public class PrintWords {
   public PrintWords() {
   }
   public static void main(String[] var0) {
      System.out.println("the " + Words.SECOND + " set");
   }
}
  • 常量变量:
    • 定义: 在编译期被常量表达式初始化的final的基本类型或String类型的变量【null不算常量表达式】
    • 特点: 对于常量域的引用会在编译期被转换为它们所表示的常量的值
      • 注意: 这里说的机制是编译期的,不是运行时的常量池
编写
预览