Java for、foreach 循環底層實現原理,以及如何判斷集合支持 foreach 循環

專欄原創出處:github-源筆記文件 github-源碼 ,歡迎 Star,轉載請附上原文出處鏈接和本聲明。

Java 核心知識專欄系列筆記,系統性學習可訪問個人覆盤筆記-技術博客 Java 核心知識

一、前言

本節內容主要研究 for、foreach 循環的底層實現原理,再比較兩種實現方式的性能。最後通過 RandomAccess 接口說明 JDK 讓我們怎麼去識別集合是否支持隨機訪問。

隨機訪問表示,像數組那樣,隨便給定一個下標我們就可以訪問內存的數據。而鏈式結構的存儲只能順序遍歷各個鏈表節點訪問。

二、for 循環底層實現

for 循環是對數值型數據出棧、改變值、比較的過程。

public void foriMethod() {
    for (int i = 0; i < 10; i++) {
        // ...
    }
}

———— 轉爲字節碼,部分關鍵字節碼如下:

 0 iconst_0         // 將 int 型 0 推送至棧頂
 1 istore_1         // 將棧頂 int 型數值存入第二個本地變量
 2 iload_1          // 將第二個 int 型本地變量推送至棧頂
 3 bipush 10        // 將單字節的常量值 10 推送至棧頂
 5 if_icmpge 14 (+9)// 比較棧頂兩 int 型數值大小, 當結果大於等於 0 時跳轉
 8 iinc 1 by 1      // 將指定 int 型變量增加指定值,此處 +1
11 goto 2 (-9)      // 無條件跳轉至 計數器爲 2 code,繼續循環
14 return

三、foreach 循序底層實現

foreach 循環底層會把循環主體對象(實現了 Iterable 接口)轉換爲 Iterator 對象進行迭代器遍歷。

public void foreachMethod(List<String> list) {
    for (String s : list) {
     }
}
// 等價於 for (Iterator i=list.iterator(); i.hasNext(); ) i.next();

———— 轉爲字節碼,部分關鍵字節碼如下:
    
 0 aload_1            // 將第二個引用類型本地變量 (list) 推送至棧頂
 1 invokeinterface #2 // 調用 iterator 方法 <java/util/List.iterator> count 1
 6 astore_2           // 將棧頂引用型數值存入第三個本地變量
 7 aload_2            // 將第三個引用類型本地變量推送至棧頂
 8 invokeinterface #3 // 調用 Iterator.hasNext <java/util/Iterator.hasNext> count 1
13 ifeq 29 (+16)      // 當棧頂 int 型數值等於 0 時跳轉到 29 code
16 aload_2            // 將第三個引用類型本地變量推送至棧頂
17 invokeinterface #4 // 調用 Iterator.hasNext 方法返回當前取值對象 <java/util/Iterator.next> count 1
22 checkcast       #5 // 強轉爲 <java/lang/String>
25 astore_3           // 將棧頂引用型數值存入第四個本地變量
26 goto 7 (-19)       // 繼續循環
29 return

四、foreach 與 for 性能比較

先下結論

儘量使用 for 循環,開銷比 foreach 低。

對於 (int i = 0; i < list.size(); i++) ,長度儘量定義爲變量,減少每次計算消耗。

—————————————————————————— foreach 生成字節碼 ———————————————————————
for (String s : list) {}

 0 aload_1
 1 invokeinterface #2 <java/util/List.iterator> count 1
 6 astore_2
 7 aload_2
 8 invokeinterface #3 <java/util/Iterator.hasNext> count 1
13 ifeq 29 (+16)
16 aload_2
17 invokeinterface #4 <java/util/Iterator.next> count 1
22 checkcast #5 <java/lang/String>
25 astore_3
26 goto 7 (-19)

—————————————————————————— for 生成字節碼 —————————————————————————
for (int i = 0; i < list.size(); i++) {
    String s = list.get(i);
}

29 iconst_0
30 istore_2
31 iload_2
32 aload_1
33 invokeinterface #6 // 此處可優化爲一個變量 list.size(),減少每次方法調用(如果編譯器足夠智能可能進行標量替換)
38 if_icmpge 58 (+20)
41 aload_1
42 iload_2
43 invokeinterface #7 <java/util/List.get> count 2
48 checkcast #5 <java/lang/String>
51 astore_3
52 iinc 2 by 1
55 goto 31 (-24)
  1. 從字節碼層面分析,發現 foreach 會生成 Iterator 對象,而且在調用時實時創建的。並且在整個迭代過程中,hasNext 方法處理邏輯比較複雜。

以 ArrayList 源碼爲例:

public Iterator<E> iterator() {
        return new Itr(); // 每次調用實時創建返回
}

// ArrayList.Itr 類中的 next 方法如下:
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}
  1. 如果我們用 for 時,只需要根據下標值,取對應的數據即可
public E get(int index) {
    Objects.checkIndex(index, size);
    return elementData(index);
}

五、RandomAccess 接口讓我們儘量使用 for 循環

  1. RandomAccess 接口是幹什麼的?
public interface RandomAccess { }

我們可以看到這個接口沒有任何實現,它僅僅起一個標識的作用,標識這個集合是否支持隨機訪問。類似於 Serializable, Cloneable

  1. RandomAccess 接口有什麼用?

舉個例子:

List 接口有 ArrayList、LinkedList 兩個實現。但是 LinkedList 底層是鏈表實現不支持隨機訪問,所以無法使用 for 循環,只能使用 foreach 循環遍歷了。

public <T extends Object> void randomAccess(List<T> list) {
    if (list instanceof RandomAccess) {
        for (int i = 0; i < list.size(); i++) {
            T t = list.get(i);
        }
    } else {
        for (T t : list) {
            System.out.println(t);
        }
    }
}

現在我們知道爲什麼 Set 接口沒有實現 RandomAccess 接口了。這也是爲什麼 Set 不能通過下標取值的原因之一。

Set 底層一般通過 Map 集合的 Key 實現的。我們知道 Map 作爲一個哈希表底層使用鏈表節點存儲。因此不支持隨機訪問。這也是爲什麼 Set 沒有 get(int index) 方法的根本原因。

專欄更多文章筆記

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章