專欄原創出處: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)
- 從字節碼層面分析,發現 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];
}
- 如果我們用 for 時,只需要根據下標值,取對應的數據即可
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
五、RandomAccess 接口讓我們儘量使用 for 循環
- RandomAccess 接口是幹什麼的?
public interface RandomAccess { }
我們可以看到這個接口沒有任何實現,它僅僅起一個標識的作用,標識這個集合是否支持隨機訪問。類似於 Serializable, Cloneable
。
- 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)
方法的根本原因。