今天继续分享一道Java经典面试题:

   直接上题:

   我们能自己写一个容器类,然后使用 for-each 循环吗?
   可以,你可以写一个自己的容器类。如果你想使用 Java 中增强的循环来遍历,你只需要实现 Iterable      接口。如果你实现 Collection 接口,默认就   具有该属性。


   对于for-each的理解答案过于浅 所以上网查询了相关资料进行了拓展,现在分享给大家:


  

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。

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

  1. package iter;  
  2.   
  3. public class TestArrayFor {  
  4.     public static void main(String[] args) {  
  5.         //String[] a = {"a", "b", "c"};  
  6.         long[] a = {2L, 3L, 5L};  
  7.         for (int i = 0, len = a.length; i < len; i++) {  
  8.             System.err.println(a[i]);  
  9.         }  
  10.     }  
  11. }  
TestArray使用for-each,TestArrayFor使用传统for循环,使用long数组是为了字节码中好区分int/long。
用javap -c看下两个类的字节码操作,保存成了文本,具体情况如下。

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

  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.        4return  
  8.   
  9.   public static void main(java.lang.String[]);  
  10.     Code:  
  11.        0: iconst_3  
  12.        1: newarray       long  
  13.        3: dup  
  14.        4: iconst_0  
  15.        5: ldc2_w        #16                 // long 2l  
  16.        8: lastore  
  17.        9: dup  
  18.       10: iconst_1  
  19.       11: ldc2_w        #18                 // long 3l  
  20.       14: lastore  
  21.       15: dup  
  22.       16: iconst_2  
  23.       17: ldc2_w        #20                 // long 5l  
  24.       20: lastore  
  25.       21: astore_1           /* 0-21行,创建long数组a,并保存在线程的当前栈帧的局部变量表的第1格*/  
  26.       22: iconst_0           /* 读取数字0(这个就是普通的0)到栈中 */  
  27.       23: istore_2           /* 将栈顶的数字0保存在第二个,22-23就是执行int i = 0; */  
  28.       24: aload_1            /* 读取保存在线程的当前栈帧的局部变量表的第1格的对象的引用,就是读取数组a */  
  29.       25: arraylength        /* 获取数组长度a.length */  
  30.       26: istore_3           /* 把数组长度保存在线程的当前栈帧的局部变量表的第3格,24-26就是执行int len = a.length */  
  31.       27goto          42   /* 无条件跳到标记42的那个地方,开始循环的代码 */  
  32.       30: getstatic     #22                 // Field java/lang/System.err:Ljava/io/PrintStream; /* 获取类的static属性,就是System.err */  
  33.       33: aload_1            /* 读取数组a */  
  34.       34: iload_2            /* 读取i */  
  35.       35: laload             /* 读取a[i] */  
  36.       36: invokevirtual #28                 // Method java/io/PrintStream.println:(J)V          /* 执行虚拟机方法,30-36就是执行System.err.println(a[i]) */  
  37.       39: iinc          21 /* 将第2格的数字加1,就是执行i++ */  
  38.       42: iload_2            /* 读取i */  
  39.       43: iload_3            /* 读取len */  
  40.       44: if_icmplt     30   /* 如果i < len,跳到标记30的那个地方,不满足就往下 */  
  41.       47return  
  42. }  
本人对照下字节码指令表,简单翻译了以下,都写在上面,还算是比较清楚。/**/中的就是本人的注释,//开头的是字节码自带的信息,这些信息不能完全算是注释吧,可以算是对字节码中出现的常量的一种直白翻译,让你看得懂这些常量代表什么。
通过编译后的字节码可以看出,数组的for-each和普通的for循环底层原理是一样的,都是用的普通for循环的那一套。数组的for-each比普通for循环多一点点操作,理论上是要慢一点点,这个暂时也不知道是为什么。这也是语法糖的一些代价,语法越简单,反而越不好进行底层优化。不过这个慢一点那真是一点,在循环体比较复杂时,这个差距就更小了,所以基本上可以认为这两种方式效率一样。实际中根据自己的情况选择,如果需要显式使用下标,就用传统for循环,其他的都可以使用for-each循环。


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

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

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

TestFor是for-each循环,TestIter是使用迭代器循环。
还是跟数组的一样分析,贴下编译后的字节码。


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

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

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

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

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐