java foreach实现原理
java foreach 语法糖实现原理
一 、 示例代码
1 import java.util.ArrayList; 2 import java.util.List; 3 4 /** 5 * 6 * @author lulei 7 * @date 2014-9-23 8 * 9 */ 10 public class TestForeach { 11 12 private List<String> list = new ArrayList<String>(); 13 private String[] array = new String[10]; 14 15 void testCollect() { 16 for (String str : list) { 17 System.out.println(str); 18 } 19 } 20 21 void testArray() { 22 for (String str : array) 23 System.out.println(str); 24 } 25 26 public void testVarargs(String... strings) { 27 for (String string : strings) { 28 System.out.println(string); 29 } 30 } 31 }
二 、 字节码
void testCollect(); Code: 0: aload_0 1: getfield #4 // Field list:Ljava/util/List; 4: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator; 9: astore_1 10: aload_1 11: invokeinterface #8, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z 16: ifeq 39 19: aload_1 20: invokeinterface #9, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object; 25: checkcast #5 // class java/lang/String 28: astore_2 29: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 32: aload_2 33: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 36: goto 10 39: return void testArray(); Code: 0: aload_0 1: getfield #6 // Field array:[Ljava/lang/String; 4: astore_1 5: aload_1 6: arraylength 7: istore_2 8: iconst_0 9: istore_3 10: iload_3 11: iload_2 12: if_icmpge 34 15: aload_1 16: iload_3 17: aaload 18: astore 4 20: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 23: aload 4 25: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: iinc 3, 1 31: goto 10 34: return public void testVarargs(java.lang.String...); Code: 0: aload_1 1: astore_2 2: aload_2 3: arraylength 4: istore_3 5: iconst_0 6: istore 4 8: iload 4 10: iload_3 11: if_icmpge 34 14: aload_2 15: iload 4 17: aaload 18: astore 5 20: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 23: aload 5 25: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: iinc 4, 1 31: goto 8
三、 注释
void testCollect();
可以看到在java虚拟机编译时,会将集合容器类型的foreach转化成了使用迭代器进行迭代遍历, 源码中我们只是显式的声明一个局部变量(str),但是在经过编译器编译后已经使用到下标为2的slot(分别为this,iterator,str所用)。这是foreach语法糖在经过虚拟机编译时,生成的临时变量。
0: aload_0 // 将 this 从存储栈帧load到操作栈帧上 1: getfield #4 // 将 list 成员变量load到操作栈帧上 4: invokeinterface #7, 1 // 调用 list.iterator() 方法返回 iterator 临时迭代变量到操作栈帧上 9: astore_1 // 将栈帧栈帧上的新返回的迭代变量的引用存储到存储栈帧的1号slot中 10: aload_1 // 将 1 号slot中的迭代变量的引用load到操作栈帧上 11: invokeinterface #8, 1 // 调用Iterator接口声明的 hasNext() 方法,返回一个boolean(实际是 0或者1)到操作栈上 16: ifeq 39 // 如果返回的是0,则调到程序结束 19: aload_1 // 否则,将1号slot中的临时迭代变量load到操作栈帧上 20: invokeinterface #9, 1 // 调用临时迭代变量的 next() 方法,返回当前的 string 实例 25: checkcast #5 // 检查上一步返回时的引用类型是否是String类型 28: astore_2 // 将返回的 string 对象从操作栈保存到存储栈 29: getstatic #10 // 将 out 对象load到操作栈帧上 32: aload_2 // 将2号slot中的那个 string 对象load到操作栈帧上 33: invokevirtual #11 // 调用 out 对象的 println 方法 36: goto 10 // 循环继续 39: return
void testArray();
在下面的分析中你将看到,数组的foreach语法糖在编译时,是被分解成类似for的循环(准确的是while循环),与一般的循环语句相比并没有什么新的虚拟机指令使用。
0: aload_0 // 将 this 从存储栈帧load到操作栈帧上 1: getfield #6 // 将 array 成员变量load到操作栈帧上 4: astore_1 // 将操作栈帧中的 array存储到存储栈帧中 1 号slot里 5: aload_1 // 将存储栈中的 array load到操作栈帧中 6: arraylength // 计算 array 引用对应数组的长度 7: istore_2 // 将 array 的长度保存到存储栈帧的 2 号slot里 8: iconst_0 // 将常数 0 压入操作栈帧中 9: istore_3 // 将操作栈帧里的常数 0 保存到存储栈帧3 号 slot里 10: iload_3 // 将存储栈帧里 3 号slot中的常数 0 load到操作栈帧上 (数组下标计数变量) 11: iload_2 // 将存储栈帧 2 号slot中的 array 的长度值load到操作栈帧里 12: if_icmpge 34 // 比较操作栈帧里的数组的访问下标是否大于等于数组的长度,相等则跳转程序结束 15: aload_1 // 将存储栈帧中的 1 号slot中存储的 array 引用load到操作栈里 16: iload_3 // 将存储栈帧中 3 号slot中存储的数组访问下标变量load到操作栈帧中 17: aaload // 将 array 的当前操作栈帧中对应的下标位置的String引用load到操作栈帧中 18: astore 4 // 将操作栈帧上的String引用存储到存储栈帧的 4 号slot中 20: getstatic #10 // 将 out 对象load到操作栈帧上 23: aload 4 // 将存储栈帧中 4 号slot中的String变量load到操作栈中 25: invokevirtual #11 // 调用 out 对象的 println 方法 28: iinc 3, 1 // 将存储栈帧中 3 号 slot对应的值加1 并保存到原位置(数组下标加 1) 31: goto 10 // 继续循环 34: return
void testVarargs(String... strings);
再来看看可变参数是怎么处理的,一样,在编译时会同样把传入的实参变量列表封装成一个数组,这时数组的长度是知道的,而将可变参数的形式变成了一个数组声明形式。
public void testVarargs(java.lang.String...); Code: 0: aload_1 // 将形参从第 1 个slot中load到运算栈上 1: astore_2 // 将运算栈上刚刚load上来的参数保存到存储栈的第 2 个slot中 2: aload_2 // 将刚刚保存的那个参数load到运算栈上 3: arraylength // 计算当前运算栈上的参数的数组长度 4: istore_3 // 将计算的数组长度结果保存到存储栈中的第3个slot中 5: iconst_0 // 将常数 0 压入运算栈顶(数组的循环控制变量) 6: istore 4 // 为循环控制变量分配一个 slot,指定到存储栈上的第 4 个slot 8: iload 4 // 将数组的长度load到运算栈顶 10: iload_3 // 将当前循环控制变量的值load到栈顶 11: if_icmpge 34 // 比较计算循环是否应该结束 14: aload_2 // 将数组类型的参数再一次从存储栈中load到运算栈顶 15: iload 4 // 将数组下边load到运算栈中 17: aaload // 将数组中相应下边位置的引用loa到运算栈顶(这里的string 临时变量) 18: astore 5 // 将临时变量保存到存储栈的第 5 号slot中 20: getstatic #10 // 将 out 对象的load到引用栈帧上 23: aload 5 // 将 临时变量 string load到运算栈顶 25: invokevirtual #11 // 调用out对象的 println方法将临时变量输出 28: iinc 4, 1 // 循环变量加 1 31: goto 8 // 循环继续 34: return
四、总结
1,既然没有”新的实质性质的东西“,那么为什么还要使用呢?
没有新的东西,也没有对效率向差的方向影响,但是可以发现这里的循环变量(编译器编译时生成的迭代变量和数组访问下边变量)都被隐藏在循环语句的内部,这样就缩小的局部变量的作用域,如 Effective Java那本书上所提,这样可以防止变量作用域污染,如果误用出作用域的变量则在编译时就可以查出。更重要的是这样做某些情况下可以缩小函数调用栈帧中的存储栈帧的长度。当然现代虚拟机都有活性分析优化,可以优化掉这部分的问题。
2 ,注释那部分mark的那条 checkCast字节码是干什么的?
其实这个和foreach语法糖没有任何关系,这是泛型的问题,因为java泛型是使用checkCast检查类型转换。起因在java.util.Iterator这个接口的 E next()方法在声明时使用的unbound泛型类型,所以在编译过程中类型擦出后就用Object类型来代替了,这时next返回的是一个Object对象,而我们要它转换成本例中的String类型,所有就会checkCast。
更多关于java泛型的知识请参考 http://docs.oracle.com/javase/tutorial/java/generics/index.html