基本类型和其包装类型之间的自动转换,也就是自动装箱、自动拆箱,是通过加入[Wrapper].valueOf(如 Integer.valueOf)以及[Wrapper].[primitive]Value(如 Integer.intValue)方法调用来实现的。
Java 程序中的泛型信息会被擦除。具体来说,Java 编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的具体类。
由于 Java 语义与 Java 字节码中关于重写的定义并不一致,因此 Java 编译器会生成桥接方法作为适配器。
此外,我还介绍了 foreach 循环以及字符串 switch 的编译。
- 自动装箱、自动拆箱
- 泛型擦除
- foreach循环的编译
- switch的编译
1. 自动装箱、自动拆箱
1 public int foo() { 2 ArrayList<Integer> list = new ArrayList<>(); 3 list.add(0); // Integer.valueOf()自动装箱 (基本型--> 包装类型) 4 int result = list.get(0); // Integer.intValue()的自动拆箱 5 return result; 6 }
public int foo(); Code: 0: new java/util/ArrayList 3: dup 4: invokespecial java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: iconst_0 10: invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 13: invokevirtual java/util/ArrayList.add:(Ljava/lang/Object;)Z 16: pop 17: aload_1 18: iconst_0 19: invokevirtual java/util/ArrayList.get:(I)Ljava/lang/Object; 22: checkcast java/lang/Integer 25: invokevirtual java/lang/Integer.intValue:()I 28: istore_2 29: iload_2 30: ireturn
1 public static Integer valueOf(int i) { 2 if (i >= IntegerCache.low && i <= IntegerCache.high) 3 return IntegerCache.cache[i + (-IntegerCache.low)]; // 直接从缓存返回,否则new一个对象 4 return new Integer(i); 5 }
2. 泛型擦除
1 package java8; 2 3 import java.util.ArrayList; 4 5 /** 6 * 既然泛型会被类型擦除,那么我们还有必要用它吗?我认为是有必要的。Java 编译器可以根据泛型参数判断程序中的语法是否正确。 7 * 举例来说,尽管经过类型擦除后,ArrayList.add 方法所接收的参数是 Object 类型,但是往泛型参数为 Integer 类型的 8 * ArrayList 中添加字符串对象,Java 编译器是会报错的。 9 */ 10 public class Foo { 11 12 public int foo() { 13 14 /** 15 * 字节码如下 16 13: invokevirtual java/util/ArrayList.add:(Ljava/lang/Object;)Z 17 ... 18 19: invokevirtual java/util/ArrayList.get:(I)Ljava/lang/Object; 19 22: checkcast java/lang/Integer 20 21 生成的字节码中,往 ArrayList 中添加元素的 add 方法,所接受的参数类型是 Object; 22 而从 ArrayList 中获取元素的 get 方法,其返回类型同样也是 Object。 23 */ 24 ArrayList<Integer> list = new ArrayList<>(); 25 list.add(0); 26 int result = list.get(0); 27 return result; 28 } 29 } 30 31 32 class GenericTest<T extends Number> { 33 34 /** 35 * 当然,并不是每一个泛型参数被擦除类型后都会变成 Object 类。对于限定了继承类的泛型参数,经过类型擦除后, 36 * 所有的泛型参数都将变成所限定的继承类。 37 * 可以看到,foo 方法的方法描述符所接收参数的类型以及返回类型都为 Number。 38 * 39 * 40 T foo(T); 41 descriptor: (Ljava/lang/Number;)Ljava/lang/Number; 42 flags: (0x0000) 43 Code: 44 stack=1, locals=2, args_size=2 45 0: aload_1 46 1: areturn 47 Signature: (TT;)TT; 48 * 49 */ 50 T foo(T t) { 51 return t; 52 } 53 }
3. foreach循环的编译
foreach 循环允许 Java 程序在 for 循环里遍历数组或者 Iterable 对象。对于数组来说,foreach 循环将从 0 开始逐一访问数组中的元素,直至数组的末尾。其等价的代码如下面所示:
1 public void foo(int[] array) { 2 for (int item : array) { 3 } 4 } 5 // 等同于 6 public void bar(int[] array) { 7 int[] myArray = array; 8 int length = myArray.length; 9 for (int i = 0; i < length; i++) { 10 int item = myArray[i]; 11 } 12 }
对于 Iterable 对象来说,foreach 循环将调用其 iterator 方法,并且用它的 hasNext 以及 next 方法来遍历该 Iterable 对象中的元素。其等价的代码如下面所示:
1 public void foo(ArrayList<Integer> list) { 2 for (Integer item : list) { 3 } 4 } 5 // 等同于 6 public void bar(ArrayList<Integer> list) { 7 Iterator<Integer> iterator = list.iterator(); 8 while (iterator.hasNext()) { 9 Integer item = iterator.next(); 10 } 11 }
4. switch的编译
1 package java8; 2 3 /** 4 * 字符串 switch 编译而成的字节码看起来非常复杂,但实际上就是一个哈希桶。由于每个 case 所截获的字符串都是常量值, 5 * 因此,Java 编译器会将原来的字符串 switch 转换为 int 值 switch,比较所输入的字符串的哈希值。 6 * 7 * 由于字符串哈希值很容易发生碰撞,因此,我们还需要用 String.equals 逐个比较相同哈希值的字符串。 8 * 9 * tableswitch用于case比较紧凑的代码,而lookup用于case比较分散的代码。 10 * 如果不考虑空间的话,tableswitch指令比lookup指令有更高的执行效率。 11 */ 12 public class SwitchTest { 13 14 /** 15 * 1: lookupswitch { // 2 16 * 1: 28 17 * 2: 39 18 * default: 50 19 * } 20 * 21 */ 22 void switchGo(int inPut) { 23 switch (inPut) { 24 case 1: 25 System.out.println("***********"); 26 break; 27 case 2: 28 System.out.println("#########"); 29 break; 30 default: 31 System.out.println("DEFAULT_&&"); 32 break; 33 } 34 } 35 36 37 /** 38 * 1: tableswitch { // 0 to 2 39 * 0: 28 40 * 1: 30 41 * 2: 32 42 * default: 34 43 * } 44 * 45 * 46 */ 47 int chooseNear(int i) { 48 switch (i) { 49 case 0: return 0; 50 case 1: return 1; 51 case 2: return 2; 52 default: return -1; 53 } 54 } 55 }