For-each 和 Index-for 迴圈最佳實踐

lymxit發表於2017-11-04

For-each 和 Index-for 迴圈最佳實踐

for-each 迴圈優先於傳統的for迴圈 -- Joshua Bloch

a hand-written counted loop is better than for the enhanced loop. -- jackiemliu

本文僅限於 ArrayList,LinkedList 不在討論話題內。

首先,相信大家對於這兩種迴圈都很熟悉:

// foreach
for (Box box : boxList)
    box.id++;

// index for(記住儲存len,否則每次都需要呼叫boxList.size())
for (int index = 0,len = boxList.size(); index < len; index++)
    boxList.get(index).id++;複製程式碼

但兩者何時使用,效能和效率怎麼權衡,又成為了新的問題!

因此這篇文章就主要是針對這兩種迴圈方式和 Android 平臺上的取捨做一些簡單的分析。

最佳實踐

先給出最佳實踐,原因後面進行分析:

1、優先使用 index-for 模式(Android Framework 推薦);

2、如果想在遍歷過程中暴露出其它執行緒正在修改(ConcurrentModificationException)的問題,請使用 for-each 模式.

深入分析

Iterate 模式

for each這種寫法其實是一個語法糖,其實

for(Box box:boxList) 等同於 for(Iterator var1 = boxList.iterator(); var1.hasNext();var1.next())

可以看到這種模式會去額外生成一個 Iterator 物件,所以相較於 Index 模式而言,它會額外使用一些記憶體。

在 Android 平臺,記憶體資源是極為有限的,如果只是單層的迴圈還算 OK,但是如果是多層迴圈,

或者是隱式的多層迴圈中使用 Iterate 模式,可想而知記憶體會臨時分配很多個變數。

例如:

// 顯式多層迴圈
for (Box box : boxList)
  for (InnerBox innerBox : box)
    ; // do something

// 隱式的多層迴圈(View 的 onDraw 方法)
void onDraw(Canvas c){
  for (Box box : boxList)
    ; // do something
}複製程式碼

以上這兩種情況可能來說就會分配過多的臨時物件,導致記憶體不足進行 GC ,從而影響 App 流暢度。

除此之外,在迭代的過程中,會去呼叫 Iterator 的 next(),這裡我以 ArrayList 為例:

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];  // 返回物件
}複製程式碼

相比與Index 模式,它增加了很多檢查,所以會帶來一定的開銷,但也是一種特性(檢查遍歷中是否有人修改)。

Index 模式

大家最常見的可能是:

for(int index = 0; index < boxList.size();index++)
    ; // do something複製程式碼

但這種方式其實並不夠理想,因為每次迴圈時,都會去呼叫 List.size()。

所以我們可以將其儲存下來:

for(int index = 0,len = boxList.size(); index < len;index++)
    ; // do something複製程式碼

相對而言,儲存 len 的方法更快,尤其在 Effective Java 中的範例:

for(int index = 0,n = expensiveComputation(); index < n;index++)
    ; // do something複製程式碼

如果一個方法耗時較多且結果不會改變,那麼可以用一個臨時變數充當快取。

效能比較

速度

一開始我還自己在電腦上做了個小 demo,然而發現 Android Team 已經做了一個更具代表性的(多次取平均值),這裡直接借用吧:

mark
mark

這裡的案例是 400,000 隨機的 Integer,每種模式都跑 10 次,去掉最大最小值後取平均結果。

可以很明顯看出 Index 模式的耗時完美勝出,Yeah |ω・)

記憶體佔用

由於之前介紹了 Iterate 模式會初始化一個 Iterator 物件,所以它的記憶體佔用肯定多於 Index 模式。

總結

0、語法糖可能很甜,但也可能有隱藏的效能損耗(e.g lambda 表示式會增加執行時開銷)

1、從語法上而言,foreach 這種更簡潔,也更加地隱藏了細節(語法糖),但也缺失了某些特性。

簡單來說,儘可能將 foreach 看成是一種 只讀向後遍歷

在 Effective Java 中,介紹了無法使用的三種場景:

  • 過濾——如果需要在遍歷過程中刪除元素。如果在 foreach 中刪除,會引發併發修改的異常。
  • 轉換——如果需要在遍歷過程中對元素進行替換。如果在 foreach 中替換,例如 o = new Object(); 其實並沒有改變列表中的值。
  • 平行迭代——如果需要靈活更改遍歷的順序時。

2、開發 Android 的時候,儘可能放棄使用 foreach ,減少記憶體壓力。

3、如果嫌棄 Index 模式的模板程式碼太麻煩,可以試試 Live Templates 中自帶的 itli ,一鍵生成迴圈程式碼哦~

4、在寫 Index 模式的時候,儘可能去儲存 list.size(),雖然 JIT 有可能會進行優化,但這種方式可以更加保險。

5、效能優化不只是整體架構或者類庫的優化,也要從平時點滴做起。

最後,推薦大家看看: Performance Tips

參考:

To Index or Iterate?

Use Enhanced For Loop Syntax

相關文章