1. 程式人生 > >我們能自己寫一個容器類,然後使用 for-each 迴圈碼?

我們能自己寫一個容器類,然後使用 for-each 迴圈碼?

   今天繼續分享一道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. 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. }  
  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看下兩個類的位元組碼操作,儲存成了文字,具體情況如下。
  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.   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.       32goto          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          41 /* 將第4格的數字加1,就是執行i++ */  
  41.       51: iload         4    /* 讀取i */  
  42.       53: iload         5    /* 讀取a.length */  
  43.       55: if_icmplt     35   /* 如果i < len,跳到標記35的那個地方,不滿足就往下 */  
  44.       58return  
  45. }  
  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.   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.       27goto          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          21 /* 將第2格的數字加1,就是執行i++ */  
  37.       42: iload_2            /* 讀取i */  
  38.       43: iload_3            /* 讀取len */  
  39.       44: if_icmplt     30   /* 如果i < len,跳到標記30的那個地方,不滿足就往下 */  
  40.       47return  
  41. }  
本人對照下位元組碼指令表,簡單翻譯了以下,都寫在上面,還算是比較清楚。/**/中的就是本人的註釋,//開頭的是位元組碼自帶的資訊,這些資訊不能完全算是註釋吧,可以算是對位元組碼中出現的常量的一種直白翻譯,讓你看得懂這些常量代表什麼。 通過編譯後的位元組碼可以看出,陣列的for-each和普通的for迴圈底層原理是一樣的,都是用的普通for迴圈的那一套。陣列的for-each比普通for迴圈多一點點操作,理論上是要慢一點點,這個暫時也不知道是為什麼。這也是語法糖的一些代價,語法越簡單,反而越不好進行底層優化。不過這個慢一點那真是一點,在迴圈體比較複雜時,這個差距就更小了,所以基本上可以認為這兩種方式效率一樣。實際中根據自己的情況選擇,如果需要顯式使用下標,就用傳統for迴圈,其他的都可以使用for-each迴圈。


二、Collection的for-each 還是先貼兩段簡單的對比的程式碼,程式碼邏輯一樣。Java環境使用的是jdk1.8_111。
  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. }  
  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是使用迭代器迴圈。 還是跟陣列的一樣分析,貼下編譯後的位元組碼。
  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.   public static void main(java.lang.String[]);  
  9.     Code:  
  10.        0new           #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.       15goto          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.       44return  
  29. }  
  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.   public static void main(java.lang.String[]);  
  9.     Code:  
  10.        0new           #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.       15goto          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.       44return  
  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迴圈)也不會明顯降低執行效率。