天天看點

for、for-each以及for-each(lambda)循環的執行效率研究,以及額外的收獲

之前對循環操作基本上是能用lambda就用lambda,不行再用for-each,最後再考慮for,畢竟是優先選擇書寫簡便的循環方式。然而不經意間看到一篇關于三種類型的循環執行效率的部落格,才發現自己之前隻顧着書寫簡便,而沒有考慮到效率問題,于是敲了段代碼研究研究。

List<Person> arrayList = new ArrayList<>();
		Person person;
		for (int i = 0; i < 10000000; i++) {
			person = new Person();
			person.setI(i);
			person.setStr(String.valueOf(i));
			arrayList.add(person);;
		}
		/*for*/
		long start = System.currentTimeMillis();
		for (int i = 0, len = arrayList.size(); i < len; i++) {
			int test = arrayList.get(i).getI();
		}
		System.out.println("arrayList-for:" + (System.currentTimeMillis() - start));
		/*foreach*/
		start = System.currentTimeMillis();
		for (Person p : arrayList) {
			int test = p.getI();
		}
		System.out.println("arrayList-foreach:" + (System.currentTimeMillis() - start));
		/*lambda*/
		start = System.currentTimeMillis();
		arrayList.forEach(p -> {
			int test = p.getI();
		});
		System.out.println("arrayList-lambda:" + (System.currentTimeMillis() - start));

           

執行多次後結果分析:

在對擁有一千萬元素的集合進行周遊時,for和foreach耗時差不多,lambda也就多個一百毫秒左右,是以在實際應用中,還是放心的使用foreach或lambda吧!

but,這隻是對ArrayList的分析,當我将ArrayList換成LinkedList之後,結果卻截然不同,在for循環下LinkedList的執行效率低到了令人發指的地步!這是因為ArrayList和LinkedList雖然都可以根據下标擷取元素,但ArrayList本質上是一個數組,可以直接根據下标擷取元素,複雜度是O(1),而LinkedList是一個連結清單,每次必須先循環周遊到對應的下标位置後才能擷取元素,複雜度是O(index),自然消耗的時間遠遠大于ArrayList。

是以如果是LinkedList,最好不要用for循環進行周遊

到了這裡,我又産生了新的疑問,foreach又叫增強for循環,也就是将for循環的書寫簡化,本質上不還是for循環嗎?那為什麼LinkedList用foreach周遊速度卻這麼快呢?

抱着這個疑問,我又在網上查詢foreach相關資料,發現原來我對foreach的了解錯了! foreach 在周遊集合時,會有選擇的轉換為for循環或者Iterator疊代器,而LinkedList是使用疊代器周遊而不是for循環,相當于按照順序從首到尾逐條擷取集合中的元素,這樣不需要每次循環周遊到對應下标位置了,複雜度也是O(1)是以能達到快速周遊的效果。

說到這,又出現一個問題,foreach是根據什麼來選擇轉換為for循環還是Iterator疊代器呢?

通過ArrayList和LinkedList的源代碼我們可以發現,ArrayList實作了一個叫做RandomAccess的接口,而這個叫做随機通路的接口裡空空如也,那麼顯而易見,這接口的唯一作用就是作為一個标記,表示實作了這個接口的類擁有随機通路的屬性,而ArrayList正好符合這個條件。

通過查詢JAVA API文檔中關于RandomAccess的介紹:

List 實作所使用的标記接口,用來表明其支援快速(通常是固定時間)随機通路。此接口的主要目的是允許一般的算法更改其行為,進而在将其應用到随機或連續通路清單時能提供良好的性能。

将操作随機通路清單的最佳算法(如 ArrayList)應用到連續通路清單(如 LinkedList)時,可産生二次項的行為。如果将某個算法應用到連續通路清單,那麼在應用可能提供較差性能的算法前,鼓勵使用一般的清單算法檢查給定清單是否為此接口的一個 instanceof,如果需要保證可接受的性能,還可以更改其行為。

現在已經認識到,随機和連續通路之間的差別通常是模糊的。例如,如果清單很大時,某些 List 實作提供漸進的線性通路時間,但實際上是固定的通路時間。這樣的 List 實作通常應該實作此接口。實際經驗證明,如果是下列情況,則 List 實作應該實作此接口,即對于典型的類執行個體而言,此循環:

for (int i=0, n=list.size(); i < n; i++)
         list.get(i);
           

的運作速度要快于以下循環:

for (Iterator i=list.iterator(); i.hasNext(); )
         i.next();
           

同時閱讀網上一些相關部落格也證明了這一點,foreach會根據集合類是否實作了RandomAccess接口來選擇用for循環還是Iterator疊代器來周遊該集合,這樣能夠保證集合周遊的高效運作。

總結:一開始隻是出于好奇驗證下for與foreach以及lambda執行效率的問題,沒想到卻誤打誤撞學習了很多額外的知識,有了很多的收獲,也算是學習JAVA的樂趣所在了。