Java 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循環的效率沒有絕對高低,具體數據結構選擇具體的遍歷方式,才能使程序更加的高效;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章