實際開發過程中,大家肯定都使用過for()循環與foreach()循環,但是有沒有思考過什麼時候選擇for(),什麼時候選擇foreach(),兩者的使用場景以及遍歷效率的區別?下面就來一起揭祕兩者的使用與區別。
for循環和foreach循環的選擇使用
一、思考案例
統計一個省的各科高考平均值,比如數學平均分是多少,語文平均分是多少等,這是每年招生辦都會公佈的數據,我們來想想看該算法應如何實現。當然使用數據庫中的一個SQL語句就可能求出平均值,不過這不再我們的考慮之列,這裏還是使用純Java的算法來解決之,看代碼:
@Test
public void test1() {
// 學生數量 300萬
int stuNum = 300 * 10000;
// List集合,記錄所有學生的分數
List<Integer> scores = new ArrayList<Integer>(stuNum);
// 寫入分數
for (int i = 0; i < stuNum; i++) {
scores.add(new Random().nextInt(150));
}
// 記錄開始計算 時間
long start = System.currentTimeMillis();
System.out.println("平均分是:" + average(scores));
long end = System.currentTimeMillis();
System.out.println("執行時間:" + (end - start) + "ms");
}
/**
* 求平均數
* @param scores 分數集合
* @return
*/
public static int average(List<Integer> scores) {
int sum = 0;
// 遍歷求和
for (int i : scores) {
sum += i;
}
return sum / scores.size();
}
- 300萬名考生的成績放到一個ArrayList數組中,然後通過foreach方法遍歷求和,再計算平均值,程序很簡單,運行結果:
- 上面方法在遍歷list時使用了foreach進行遍歷操作,只是計算一個算術平均值就花了20ms,不要說什麼其它諸如加權平均值,補充平均值等算法,那花的時間肯定更長。仔細分析一下average方法,加號操作是最基本的,沒有什麼可以優化的,剩下的就是一個遍歷了,問題是List的遍歷可以優化嗎?
二、優化List的遍歷方式
這裏嘗試將list的遍歷方式更改爲普通for循環(下標遍歷):
/**
* 求平均數
* @param scores 分數集合
* @return
*/
public static int average(List<Integer> scores) {
int sum = 0;
// 遍歷求和
for (int i = 0; i < scores.size(); i++) {
sum += scores.get(i);
}
return sum / scores.size();
}
- 當把遍歷方法更改爲普通for以後,來看看運行效率:
- 很明顯,執行時間少了4ms,如果是更大數據量效果會更加明顯。那爲什麼使用下標方式遍歷數組可以提高的性能呢?
- 原因: 因爲ArrayList數組實現了RandomAccess接口(隨機存取接口),這樣標誌着ArrayList是一個可以隨機存取的列表。在Java中,RandomAccess和Cloneable、Serializable一樣,都是標誌性接口,不需要任何實現,只是用來表明其實現類具有某種特質的,實現了Cloneable表明可以被拷貝,實現了Serializable接口表明被序列化了,實現了RandomAccess接口則表明這個類可以隨機存取,對我們的ArrayList來說也就標誌着其數據元素之間沒有關聯,即兩個位置相鄰的元素之間沒有相互依賴和索引關係,可以隨機訪問和存取。我們知道,Java的foreach語法時iterator(迭代器)的變形用法,也就是說上面的foreach與下面的代碼等價:
/**
* 求平均數
* @param scores 分數集合
* @return
*/
public static int average(List<Integer> scores) {
int sum = 0;
// 遍歷求和
for (Iterator<Integer> i = scores.iterator(); i.hasNext();) {
sum += i.next();
}
return sum / scores.size();
}
- 也就是說使用foreach遍歷ArrayList需要先創建一個迭代器容器,這個容器屏蔽了內部細節,對外只提供hasNext、next等方法。這就是問題的所在,ArrayList本身實現RandomAccess接口,元素之間本來就沒有關係,但是使用foreach後,通過迭代器強行的去建立元素之間的關係,上一個元素遍歷完成後,需要判斷下一個元素是否存在,這樣就降低了遍歷的效率,所以foreach對於ArrayList的遍歷會比for遍歷低效;
- 那是不是就表明foreach循環的效率真的就比for循環的效率低呢?再來看一個示例;
三、for循環遍歷LinkedList
示例代碼如下:
@Test
public void test1() {
// 學生數量 10萬
int stuNum = 10 * 10000;
// List集合,記錄所有學生的分數
List<Integer> scores = new LinkedList<>();
// 寫入分數
for (int i = 0; i < stuNum; i++) {
scores.add(new Random().nextInt(150));
}
// 記錄開始計算 時間
long start = System.currentTimeMillis();
System.out.println("平均分是:" + average(scores));
long end = System.currentTimeMillis();
System.out.println("執行時間:" + (end - start) + "ms");
}
/**
* 求平均數
* @param scores 分數集合
* @return
*/
public static int average(List<Integer> scores) {
int sum = 0;
// 遍歷求和
for (int i = 0; i < scores.size(); i++) {
sum += scores.get(i);
}
return sum / scores.size();
}
- 示例代碼的測試數據更換成了10萬條,因爲使用for循環遍歷300萬條數據,程序直接卡死了,代碼運行結果:
- for循環遍歷LinkedList的運行結果(10萬條數據):
- foreach循環遍歷LinkedList的運行結果(10萬條數據):
- 很明顯在對LinkedList進行遍歷操作時,for循環的效率比foreach循環的效率低了100倍不止,這是什麼原因呢?來看看LinkedList.get()方法的源碼:
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//...............中間源碼已省略...............
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
- LinkedList.get()方法通過node()方法查找指定下標的節點,然後返回其包含的元素,而node()方法會先判斷輸入的下標與中間值(size右移一位,也就是除以2了)的關係,小於中間值則從頭開始正向搜索,大於中間值則從尾節點反向搜索,也就是說每一次get()操作都是一次新的遍歷,性能理所當然會低;
- 那爲什麼相比之下foreach循環 效率就很高?這是因爲LinkedList類實現了雙向鏈表,每個數據節點都有三個數據項:前節點的引用(Previous Node)、本節點元素(Node Element)、後繼結點的引用(Next Node),也就是說在LinkedList中的兩個元素本來就是有關聯關係的。既然元素之間已經有關聯關係,使用foreach循環也就是迭代器方式肯定就比for循環要高效很多;
四、方法改進
明白了for()循環和foreach()循環對於數組結構和鏈表結構遍歷時的效率差別後,可以對示例中average方法進行一下小改進,以便實現不同的列表採用不同的遍歷方式,代碼如下:
/**
* 求平均數
* @param scores 分數集合
* @return
*/
public static int average(List<Integer> scores) {
int sum = 0;
if (scores instanceof RandomAccess) {
// 可以隨機存取,則使用下標遍歷
for (int i = 0; i < scores.size(); i++) {
sum += scores.get(i);
}
} else {
// 有序存取,使用foreach方式
for (int i : scores) {
sum += i;
}
}
return sum / scores.size();
}
五、結論
- 遍歷隨機存取列表(數組結構)時用for或者foreach都行:
1. 在固定長度或者長度不需要計算的時候for循環效率高於foreach循環;
2. 在不確定長度或者計算長度有損性能的時候用foreach比較方便; - 遍歷有序存取列表(鏈表結構)時,一定不要用for循環;
- 所以for循環與foreach循環的效率沒有絕對高低,具體數據結構選擇具體的遍歷方式,才能使程序更加的高效;