从字节码看Java中for-each循环(增强for循环)实现原理

转发:http://blog.csdn.net/u011392897/article/details/54562596

for-each循环是jdk1.5引入的新的语法功能。并不是所有东西都可以使用这个循环的。可以看下Iterable接口的注释,它说明了除了数组外,其他类想要使用for-each循环必须实现这个接口。这一点表明除了数组外的for-each可能底层是由迭代器实现的。

Iterable接口在1.8之前只有一个方法,Iterator<T> iterator(),此方法返回一个迭代器。由于更早出现的Collection接口中早就有了这个同样的方法,所以只需要让Collection接口继承Iterable接口,基于Collection的集合类就可以不做任何更改就使用for-each循环。

对于数组,因为数组不实现Iterable接口,它的for-each实现原理应该和Collection不一样。

下面就通过分析下不同方式编译后的字节码,简单研究下for-each的的底层原理。

一、数组的for-each

下面是的两个很简单的类,可以看出它们的功能是一样的。Java环境使用的是jdk1.8_111。

[java] view plain copy

  1. package iter;
  2. public class TestArray {
  3. public static void main(String[] args) {
  4. //String[] a = {"a", "b", "c"};
  5. long[] a = {2L, 3L, 5L};
  6. for (long i : a) {
  7. System.err.println(i);
  8. }
  9. }
  10. }

[java] view plain copy

  1. package iter;
  2. public class TestArrayFor {
  3. public static void main(String[] args) {
  4. //String[] a = {"a", "b", "c"};
  5. long[] a = {2L, 3L, 5L};
  6. for (int i = 0, len = a.length; i < len; i++) {
  7. System.err.println(a[i]);
  8. }
  9. }
  10. }

TestArray使用for-each,TestArrayFor使用传统for循环,使用long数组是为了字节码中好区分int/long。

用javap -c看下两个类的字节码操作,保存成了文本,具体情况如下。

[java] view plain copy

  1. Compiled from "TestArray.java"
  2. public class iter.TestArray {
  3. public iter.TestArray();
  4. Code:
  5. 0: aload_0
  6. 1: invokespecial #8                  // Method java/lang/Object."<init>":()V
  7. 4: return
  8. public static void main(java.lang.String[]);
  9. Code:
  10. 0: iconst_3
  11. 1: newarray       long
  12. 3: dup
  13. 4: iconst_0
  14. 5: ldc2_w        #16                 // long 2l
  15. 8: lastore
  16. 9: dup
  17. 10: iconst_1
  18. 11: ldc2_w        #18                 // long 3l
  19. 14: lastore
  20. 15: dup
  21. 16: iconst_2
  22. 17: ldc2_w        #20                 // long 5l
  23. 20: lastore
  24. 21: astore_1           /* 0-21行,创建long数组a,并保存在线程的当前栈帧的局部变量表的第1格*/
  25. 22: aload_1            /* 读取保存在线程的当前栈帧的局部变量表的第1格的对象的引用,就是读取数组a */
  26. 23: dup                /* 把a的引用复制一遍并且再放到栈顶 */
  27. 24: astore        6    /* 把栈顶的数存在线程的当前栈帧的局部变量表的第6格,就是生成a的一个值复制品b并存储起来,暂时不知道为什么这里要复制一次,后面的数组都还是用a表示 */
  28. 26: arraylength        /* 获取数组长度a.length */
  29. 27: istore        5    /* 把数组长度存储在线程的当前栈帧的局部变量表的第5格,22-27隐式执行了int len = a.length */
  30. 29: iconst_0           /* 读取数字0(这个就是普通的0)到栈中 */
  31. 30: istore        4    /* 把数字0放在线程的当前栈帧的局部变量表的第4格,29-30隐式执行了int i = 0 */
  32. 32: goto          51   /* 无条件跳转到51那个地方,开始循环的代码 */
  33. 35: aload         6    /* 读取数组a */
  34. 37: iload         4    /* 读取i */
  35. 39: laload             /* 读取a[i] */
  36. 40: lstore_2           /* 把a[i]存在线程的当前栈帧的局部变量表的第2格 */
  37. 41: getstatic     #22                 // Field java/lang/System.err:Ljava/io/PrintStream; /* 获取类的static属性,就是System.err */
  38. 44: lload_2            /* 读取存在线程的当前栈帧的局部变量表的第2格的数据,就是读取a[i] */
  39. 45: invokevirtual #28                 // Method java/io/PrintStream.println:(J)V          /* 执行虚拟机方法,30-36就是执行System.err.println(a[i]) */
  40. 48: iinc          4, 1 /* 将第4格的数字加1,就是执行i++ */
  41. 51: iload         4    /* 读取i */
  42. 53: iload         5    /* 读取a.length */
  43. 55: if_icmplt     35   /* 如果i < len,跳到标记35的那个地方,不满足就往下 */
  44. 58: return
  45. }

[java] view plain copy

  1. Compiled from "TestArrayFor.java"
  2. public class iter.TestArrayFor {
  3. public iter.TestArrayFor();
  4. Code:
  5. 0: aload_0
  6. 1: invokespecial #8                  // Method java/lang/Object."<init>":()V
  7. 4: return
  8. public static void main(java.lang.String[]);
  9. Code:
  10. 0: iconst_3
  11. 1: newarray       long
  12. 3: dup
  13. 4: iconst_0
  14. 5: ldc2_w        #16                 // long 2l
  15. 8: lastore
  16. 9: dup
  17. 10: iconst_1
  18. 11: ldc2_w        #18                 // long 3l
  19. 14: lastore
  20. 15: dup
  21. 16: iconst_2
  22. 17: ldc2_w        #20                 // long 5l
  23. 20: lastore
  24. 21: astore_1           /* 0-21行,创建long数组a,并保存在线程的当前栈帧的局部变量表的第1格*/
  25. 22: iconst_0           /* 读取数字0(这个就是普通的0)到栈中 */
  26. 23: istore_2           /* 将栈顶的数字0保存在第二个,22-23就是执行int i = 0; */
  27. 24: aload_1            /* 读取保存在线程的当前栈帧的局部变量表的第1格的对象的引用,就是读取数组a */
  28. 25: arraylength        /* 获取数组长度a.length */
  29. 26: istore_3           /* 把数组长度保存在线程的当前栈帧的局部变量表的第3格,24-26就是执行int len = a.length */
  30. 27: goto          42   /* 无条件跳到标记42的那个地方,开始循环的代码 */
  31. 30: getstatic     #22                 // Field java/lang/System.err:Ljava/io/PrintStream; /* 获取类的static属性,就是System.err */
  32. 33: aload_1            /* 读取数组a */
  33. 34: iload_2            /* 读取i */
  34. 35: laload             /* 读取a[i] */
  35. 36: invokevirtual #28                 // Method java/io/PrintStream.println:(J)V          /* 执行虚拟机方法,30-36就是执行System.err.println(a[i]) */
  36. 39: iinc          2, 1 /* 将第2格的数字加1,就是执行i++ */
  37. 42: iload_2            /* 读取i */
  38. 43: iload_3            /* 读取len */
  39. 44: if_icmplt     30   /* 如果i < len,跳到标记30的那个地方,不满足就往下 */
  40. 47: return
  41. }

本人对照下字节码指令表,简单翻译了以下,都写在上面,还算是比较清楚。/**/中的就是本人的注释,//开头的是字节码自带的信息,这些信息不能完全算是注释吧,可以算是对字节码中出现的常量的一种直白翻译,让你看得懂这些常量代表什么。

通过编译后的字节码可以看出,数组的for-each和普通的for循环底层原理是一样的,都是用的普通for循环的那一套。数组的for-each比普通for循环多一点点操作,理论上是要慢一点点,这个暂时也不知道是为什么。这也是语法糖的一些代价,语法越简单,反而越不好进行底层优化。不过这个慢一点那真是一点,在循环体比较复杂时,这个差距就更小了,所以基本上可以认为这两种方式效率一样。实际中根据自己的情况选择,如果需要显式使用下标,就用传统for循环,其他的都可以使用for-each循环。

二、Collection的for-each

还是先贴两段简单的对比的代码,代码逻辑一样。Java环境使用的是jdk1.8_111。

[java] view plain copy

  1. package iter;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public class TestFor {
  5. public static void main(String[] args) {
  6. List<String> listA = new ArrayList<String>();
  7. for(String str : listA) {
  8. System.err.println(str);
  9. }
  10. }
  11. }

[java] view plain copy

  1. package iter;
  2. import java.util.ArrayList;
  3. import java.util.Iterator;
  4. import java.util.List;
  5. public class TestIter {
  6. public static void main(String[] args) {
  7. List<String> listA = new ArrayList<String>();
  8. for (Iterator<String> iter = listA.iterator(); iter.hasNext();) {
  9. String s = iter.next();
  10. System.err.println(s);
  11. }
  12. }
  13. }

TestFor是for-each循环,TestIter是使用迭代器循环。

还是跟数组的一样分析,贴下编译后的字节码。

[java] view plain copy

  1. Compiled from "TestFor.java"
  2. public class iter.TestFor {
  3. public iter.TestFor();
  4. Code:
  5. 0: aload_0
  6. 1: invokespecial #8                  // Method java/lang/Object."<init>":()V
  7. 4: return
  8. public static void main(java.lang.String[]);
  9. Code:
  10. 0: new           #16                 // class java/util/ArrayList
  11. 3: dup
  12. 4: invokespecial #18                 // Method java/util/ArrayList."<init>":()V
  13. 7: astore_1
  14. 8: aload_1
  15. 9: invokeinterface #19,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
  16. 14: astore_3
  17. 15: goto          35
  18. 18: aload_3
  19. 19: invokeinterface #25,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
  20. 24: checkcast     #31                 // class java/lang/String
  21. 27: astore_2
  22. 28: getstatic     #33                 // Field java/lang/System.err:Ljava/io/PrintStream;
  23. 31: aload_2
  24. 32: invokevirtual #39                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  25. 35: aload_3
  26. 36: invokeinterface #45,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
  27. 41: ifne          18
  28. 44: return
  29. }

[java] view plain copy

  1. Compiled from "TestIter.java"
  2. public class iter.TestIter {
  3. public iter.TestIter();
  4. Code:
  5. 0: aload_0
  6. 1: invokespecial #8                  // Method java/lang/Object."<init>":()V
  7. 4: return
  8. public static void main(java.lang.String[]);
  9. Code:
  10. 0: new           #16                 // class java/util/ArrayList
  11. 3: dup
  12. 4: invokespecial #18                 // Method java/util/ArrayList."<init>":()V
  13. 7: astore_1
  14. 8: aload_1
  15. 9: invokeinterface #19,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
  16. 14: astore_2
  17. 15: goto          35
  18. 18: aload_2
  19. 19: invokeinterface #25,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
  20. 24: checkcast     #31                 // class java/lang/String
  21. 27: astore_3
  22. 28: getstatic     #33                 // Field java/lang/System.err:Ljava/io/PrintStream;
  23. 31: aload_3
  24. 32: invokevirtual #39                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  25. 35: aload_2
  26. 36: invokeinterface #45,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
  27. 41: ifne          18
  28. 44: return
  29. }

这两段字节码中自带的注释很多,基本上看得懂,就不添加注释了。

两段字节码除了几个变量保存在线程的当前栈帧的局部变量表的索引(astore_n,这个n就是索引)不一样外,其余的都是一模一样的。不排除某次编译后连那几个索引值也一样,那就真一模一样了。字节码自带的注释都说了,Collection的for-each底层也是使用迭代器来实现的,两种方式可以说是完全等价的。

对于实现了RandomAccess接口的实现类,因为它们的随机访问操作的时间复杂度为O(1),大多数情况使用传统for循环会比用迭代器循环(这里的迭代器也可以用for-each替换,上面说了它们底层整体是一样的)要快。至于这一点是为什么,可以看下ArrayList的源码。它的迭代器虽然也是通过下标直接访问elementData数组,但是迭代器多了很多方法调用以及其他的额外操作,现在很多编译器cpu也都会对传统for循环进行特别的优化,在这个层面十几个指令的差别就很大了,这些因素加在一起导致RandomAccess的迭代器比传统for循环要慢一些。对于ArrayList这种,在cpu密集型的应用中应该只使用传统for循环,在循环体执行时间比较长的应用中,传统for循环和迭代器循环的差别就很小了,这时候使用迭代器(for-each循环)也不会明显降低执行效率。

参考:

1、https://docs.oracle.com/javase/8/docs/technotes/guides/language/foreach.html

2、Java虚拟机规范(Java SE 8)

时间: 2024-10-29 19:09:46

从字节码看Java中for-each循环(增强for循环)实现原理的相关文章

通过字节码分析java中的switch语句

在一次做题中遇到了switch的问题,由于对switch执行顺序的不了解,在这里简单的通过字节码的方式理解一下switch执行顺序(题目如下): public class Ag{ static public int i=10; public static void main(String []args){ switch(i){ default: System.out.println("this is default"); case 1: System.out.println("

从源码看java中Integer的缓存问题

在开始详细的说明问题之前,我们先看一段代码 1 public static void compare1(){ 2 Integer i1 = 127, i2 = 127, i3 = 128, i4 = 128; 3 System.out.println(i1 == i2); 4 System.out.println(i1.equals(i2)); 5 System.out.println(i3 == i4); 6 System.out.println(i3.equals(i4)); 7 } 这段代

java中的Iterator与增强for循环的效率比较

最近在优化代码时遇到了这个问题:Iterator与增强for循环到底哪个效率高?之前在学习的时候,好像记着老师说过遍历集合(如list)时,使用iterator好像正规一些,因为是专用的,但是运行效率问题确实不曾考虑,今天做了一个实验:对两者进行了简单的比较,得出的结论是:增强for循环运行效率更高一些.但是我不确定这是否会代表全部情况,这里仅仅记录一下,做个参考,后期有新的认识再来补充,欢迎大家批评指正. 1 public static void main(String[] args) { 2

Java 中的foreach(增强for循环)

foreach概述 增强for循环:底层使用的是送代器,使用for循环的格式,简化了送代器的书写,foreach是JDK1.5之后出现的新特性 使用增强for循环 遍历集合 /** * 遍历集合 * @param arrayList 集合 */ public static void demoCollection(ArrayList<String> arrayList) { for (String string: arrayList) { System.out.println(string);

通过字节码分析JDK8中Lambda表达式编译及执行机制

关于Lambda字节码相关的文章,很早之前就想写了,[蜂潮运动]APP 产品的后端技术,能快速迭代,除了得益于整体微服架构之外,语言层面上,也是通过Java8的lambda表达式的运用以及rxJava响应式编程框架,使代码更加简洁易维护,调用方式更加便捷.本文将介绍JVM中的方法调用相关的字节码指令,重点解析JDK7(JSR-292)之后新增的invokedynamic指令给lambda表达式的动态调用特性提供的实现机制,最后再探讨一下lambda性能方面的话题. 方法调用的字节码指令 在介绍i

看Java中==、equals、hashCode的来龙去脉

我有一个哥们去参加了面试,面试官这样问一个Java问题: 你说一下java对象的equals方法调用什么方法呢?我这个哥们想了想,回答说"应该是比较的引用".听了这个答案之后,那个面试官摇头晃脑的说:"不对,你回答的不对,equals方法调用的是hashCode方法".于是乎,我那个技术还不错的哥们就悲壮地栽在这道题目上了. 今天晚上,西安历史上少有的热,那就好好总结一下这个题目的来龙去脉好了,也方便给后面的朋友们提个醒,不要栽在这么简单的问题上.你说这个面试官回答

java中break和continue跳出指定循环(转载)

java中break和continue跳出指定循环 java中break和continue可以跳出指定循环,break和continue之后不加任何循环名则默认跳出其所在的循环,在其后加指定循环名,则可以跳出该指定循环(指定循环一般为循环嵌套的外循环). break跳出指定循环示例代码: loop1:for(int x = 0; x < 4; x++) { loop2:for (int y = 0; y < 5 ; y++ ) { System.out.println("x=&quo

【Java字节码】Idea中查看Java字节码的插件jclasslib Bytecode viewer

Idea插件搜索:jclasslib Bytecode viewer 安装完后,maven install你的项目(因为该插件会读取target下的class文件),然后选中某个java文件,按下图操作即可查看对应的字节码 详情:jclasslib Bytecode viewer 原文地址:https://www.cnblogs.com/756623607-zhang/p/11062087.html

透过字节码分析java基本类型数组的内存分配方式。

我们知道java中new方式创建的对象都是在堆中创建的,而局部变量对应的值存放在栈上.那么java中的int [] arr={1,2,3}是存放在什么地方的呢,int []arr = new int[3]又是存放在什么地方的呢, 下面我们通过编写两个小例子,通过查看生成的字节码文件,来了解jvm会如何来处理这两种情况的. 1.int[] arr = new int[3]示例 public class ArrayTest { public static void main(String[] arg