大多數人不清楚的:for循環與foreach循環的性能差距


一、ArrayList中,for循環 VS 增強for

import java.util.ArrayList;
import java.util.List;

public class ArrayListForEach {
	public static void main(String[] args) {
		List<Integer> arrayList = new ArrayList<Integer>();
		// 每個集合插入10萬條數據
		for (int i = 0; i < 100000; i++) {
			arrayList.add(i);
		}
		long StartTime;//開始時間
		long EndTime;//結束時間

		System.out.println("============遍歷ArrayList============");

		// 用for循環arrayList
		StartTime = System.currentTimeMillis();
		for (int i = 0; i < arrayList.size(); i++) {
			arrayList.get(i);
		}
		EndTime = System.currentTimeMillis();
		System.out.println("for遍歷需要:" + (EndTime - StartTime) + "毫秒");

		// 用增強for循環arrayList
		StartTime = System.currentTimeMillis();
		for (Integer in : arrayList) {

		}
		EndTime = System.currentTimeMillis();
		System.out.println("增強for遍歷需要:" + (EndTime - StartTime) + "毫秒");
	}
}

運行結果:
在這裏插入圖片描述
結論:遍歷ArrayList時,普通for循環比foreach循環花費的時間要少一點.

二、LinkedList中,for循環 VS 增強for

import java.util.LinkedList;
import java.util.List;

public class LinkedListForEach {
	public static void main(String[] args) {
		List<Integer> linkList = new LinkedList<Integer>();
		// 每個集合插入10萬條數據
		for (int i = 0; i < 100000; i++) {
			linkList.add(i);
		}
		long StartTime;
		long EndTime;

		System.out.println("============遍歷LinkedList============");

		// 用for循環linkList
		StartTime = System.currentTimeMillis();
		for (int i = 0; i < linkList.size(); i++) {
			linkList.get(i);
		}
		EndTime = System.currentTimeMillis();
		System.out.println("for遍歷:" + (EndTime - StartTime) + "毫秒");

		// 用增強for循環linkList
		StartTime = System.currentTimeMillis();
		for (Integer in : linkList) {

		}
		EndTime = System.currentTimeMillis();
		System.out.println("用增強for遍歷需要:" + (EndTime - StartTime) + "毫秒");
	}
}

運行結果:
在這裏插入圖片描述

結論:遍歷LinkList時,普通for循環比foreach循環花費的時間要多很多


三、剖析ArrayList中兩者性能的原理

我們對上面的代碼進行反編譯,一探究竟:
在這裏插入圖片描述
通過反編譯代碼,我們可以看到,for循環倒是沒變化,增強for循環卻是變化劇烈,原來增強for循環只是Java提供給我們的語法糖,其底層實現還是for循環。如果只是到這裏當然不能叫原理剖析,下面我們繼續深入。

1.ArrayList的底層數據結構

首先要明確的是ArrayList只是JDK爲我們封裝好的類,通過翻看源碼,可以發現其底層數據結構就是數組。詳細可以參看這篇博文:ArrayList源碼詳細分析JDK1.8(一)

2.ArrayList中get方法的實現

用for循環遍歷ArrayLis集合的過程就是不斷調用get()方法的過程,所以我們要知道get()方法是怎麼工作的。
在這裏插入圖片描述
上圖,我們可以知道get()方法內部調用了elementDate()方法,而elementDate()方法就是直接通過下標從數組中拿到一個元素並返回。學過數據結構的都知道通過下標在數組中找到指定元素的時間複雜度是O(1)。

3.ArrayList中iterator方法的實現

通過反編譯後的代碼,我們知道使用增強for循環遍歷ArrayList集合的過程有兩個關鍵點。

(一)就是調用了iterator()方法,通過翻看源碼,我們發現該方法返回了一個Itr類。源碼如下:
在這裏插入圖片描述
(二)主要依賴next()方法拿元素。
在這裏插入圖片描述
上面是next()方法的源碼,有很多if語句,這些不用管,即使不理解也根本不妨礙,我們聚焦與紅色方框標出的部分。這是不是很熟悉呢?????原來next()也是通過下標出數組中拿數據的,那麼它的時間複雜度也是O(1)。

4.小結

小結一:對於ArrayList來說,使用for循環和增強for循環性能相差無幾的根本原因是:其底層實現都是通過下標從數組中拿數據,每拿一個數據的時間複雜度都是O(1),所以兩者性能上沒有很大的差距。

小結二:爲什麼for循環還是要比增強for循環更加快一點呢?get()方法多簡潔就一行,而next()方法卻有那麼多安全性判斷,慢一點很正常嘛!

四、剖析LinkedList中兩者性能的原理

我們繼續對LinkedList情況下代碼反編譯
在這裏插入圖片描述

1.LinkedList的底層數據結構

同樣LinkedList也是JDK封裝好給我們使用的類,其底層數據結構其實是鏈表(更具體一點是雙向鏈表),詳細可以看看這篇文章分析的很透徹:JDK8:LinkedList源碼分析

2.LinkedList中get方法的實現

同理,用for循環遍歷LinkedLis集合的過程也是不斷調用get()方法的過程,所以我們要知道get()方法是怎麼工作的。
在這裏插入圖片描述
上圖,我們可以知道get()方法內部調用了node()方法。對於node()方法那就沒什麼好說的啦,因爲是鏈表,沒有下標可用,查找到一個指定元素必須從第一個元素到指定元素挨個遍歷一遍。

不過JDK在這個有一個小的優化。首先,“size >> 1”相當於除以2,所以當指定元素在整個鏈表的前半段的時候從首元素開始遍歷,當指定元素在後半段的時候從尾元素開始遍歷,可以節約一半時間,所以找到某個指定元素的平均時間複雜度是O(N/4),不過在數據結構中我們認爲還是O(N)。

3.LikedList中iterator方法的實現

LinkedList中iterator()方法的實現非常複雜,不是說代碼有多難,而是子、父類方法相互調用,還涉及到子、父類的內部類和抽象方法,繞的了一大圈,有興趣的可以自己親自理一遍。最終還是找到了其具體代碼,如下圖所示:
在這裏插入圖片描述
這個next()方法的實現可比上面那種節約時間多了,就是從第一個元素開始遍歷,每調用一次next()方法,就返回當前元素的值並移動到下一個元素,不是每一次都是從頭開始查找的,所以每遍歷一個元素的時間複雜度是O(1)。

4.小結

小結:爲什麼在LinkedList中for循環比增強for循環慢那麼多?因爲,for循環每次都要從頭(或尾)查找,每遍歷一個元素的時間複雜度是O(N),而增強for循環是返回當前元素並移動到下一個元素,每遍歷一個元素的時間複雜度是O(1)。

結束語

在學習過程中搜索了很多for循環和增強for循環性能比較的文章,一些文章開門見山直接給出結論,還是一些更加嚴謹,會給出測試代碼以及測試結果,這樣更有說服力。但僅僅但這一步還是無法滿足我的好奇心,我就想知道到底是爲什麼會產生這樣的性能差距?結果大部分文章卻是沒有給出,而且看一些文章的評論,也有其他讀者很想知道其中的內情,所以就促成了這篇文章。最後當然跪求三連啊!!!!!!!!!!

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