Java遍歷集合的幾種方法分析(實現原理、演算法性能、適用場合)

来源:http://www.cnblogs.com/xyhuangjinfu/archive/2016/04/25/5429644.html
-Advertisement-
Play Games

概述 Java語言中,提供了一套數據集合框架,其中定義了一些諸如List、Set等抽象數據類型,每個抽象數據類型的各個具體實現,底層又採用了不同的實現方式,比如ArrayList和LinkedList。 除此之外,Java對於數據集合的遍歷,也提供了幾種不同的方式。開發人員必須要清楚的明白每一種遍歷 ...


概述

        Java語言中,提供了一套數據集合框架,其中定義了一些諸如List、Set等抽象數據類型,每個抽象數據類型的各個具體實現,底層又採用了不同的實現方式,比如ArrayList和LinkedList。

        除此之外,Java對於數據集合的遍歷,也提供了幾種不同的方式。開發人員必須要清楚的明白每一種遍歷方式的特點、適用場合、以及在不同底層實現上的表現。下麵就詳細分析一下這一塊內容。

 

 

 

數據元素是怎樣在記憶體中存放的?


        數據元素在記憶體中,主要有2種存儲方式:

1、順序存儲,Random Access(Direct Access):

        這種方式,相鄰的數據元素存放於相鄰的記憶體地址中,整塊記憶體地址是連續的。可以根據元素的位置直接計算出記憶體地址,直接進行讀取。讀取一個特定位置元素的平均時間複雜度為O(1)。正常來說,只有基於數組實現的集合,才有這種特性。Java中以ArrayList為代表。

2、鏈式存儲,Sequential Access:

        這種方式,每一個數據元素,在記憶體中都不要求處於相鄰的位置,每個數據元素包含它下一個元素的記憶體地址。不可以根據元素的位置直接計算出記憶體地址,只能按順序讀取元素。讀取一個特定位置元素的平均時間複雜度為O(n)。主要以鏈表為代表。Java中以LinkedList為代表。

 

 

 

Java中提供的遍歷方式有哪些?

 

1、傳統的for迴圈遍歷,基於計數器的:

        遍歷者自己在集合外部維護一個計數器,然後依次讀取每一個位置的元素,當讀取到最後一個元素後,停止。主要就是需要按元素的位置來讀取元素。這也是最原始的集合遍歷方法。

寫法為:

for (int i = 0; i < list.size(); i++) {
    list.get(i);
}

 


2、迭代器遍歷,Iterator:

        Iterator本來是OO的一個設計模式,主要目的就是屏蔽不同數據集合的特點,統一遍歷集合的介面。Java作為一個OO語言,自然也在Collections中支持了Iterator模式。

寫法為:

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.next();
}

 


3、foreach迴圈遍歷:

        屏蔽了顯式聲明的Iterator和計數器。

        優點:代碼簡潔,不易出錯。

        缺點:只能做簡單的遍歷,不能在遍歷過程中操作(刪除、替換)數據集合。

寫法為:

for (ElementType element : list) {
}

 

 

 


每個遍歷方法的實現原理是什麼?

 

1、傳統的for迴圈遍歷,基於計數器的:

        遍歷者自己在集合外部維護一個計數器,然後依次讀取每一個位置的元素,當讀取到最後一個元素後,停止。主要就是需要按元素的位置來讀取元素。

 

2、迭代器遍歷,Iterator:

        每一個具體實現的數據集合,一般都需要提供相應的Iterator。相比於傳統for迴圈,Iterator取締了顯式的遍歷計數器。所以基於順序存儲集合的Iterator可以直接按位置訪問數據。而基於鏈式存儲集合的Iterator,正常的實現,都是需要保存當前遍歷的位置。然後根據當前位置來向前或者向後移動指針。

 

3、foreach迴圈遍歷:

        根據反編譯的位元組碼可以發現,foreach內部也是採用了Iterator的方式實現,只不過Java編譯器幫我們生成了這些代碼。

 

 

 

 

各遍歷方式對於不同的存儲方式,性能如何?

 

1、傳統的for迴圈遍歷,基於計數器的:

        因為是基於元素的位置,按位置讀取。所以我們可以知道,對於順序存儲,因為讀取特定位置元素的平均時間複雜度是O(1),所以遍歷整個集合的平均時間複雜度為O(n)。而對於鏈式存儲,因為讀取特定位置元素的平均時間複雜度是O(n),所以遍歷整個集合的平均時間複雜度為O(n2)(n的平方)。

ArrayList按位置讀取的代碼:直接按元素位置讀取。

transient Object[] elementData;

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

 

LinkedList按位置讀取的代碼:每次都需要從第0個元素開始向後讀取。其實它內部也做了小小的優化。

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int 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;
    }
}

 


 

2、迭代器遍歷,Iterator:

        那麼對於RandomAccess類型的集合來說,沒有太多意義,反而因為一些額外的操作,還會增加額外的運行時間。但是對於Sequential Access的集合來說,就有很重大的意義了,因為Iterator內部維護了當前遍歷的位置,所以每次遍歷,讀取下一個位置並不需要從集合的第一個元素開始查找,只要把指針向後移一位就行了,這樣一來,遍歷整個集合的時間複雜度就降低為O(n);

        (這裡只用LinkedList做例子)LinkedList的迭代器,內部實現,就是維護當前遍歷的位置,然後操作指針移動就可以了:

代碼:

public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();

    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}

public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();

    lastReturned = next = (next == null) ? last : next.prev;
    nextIndex--;
    return lastReturned.item;
}

 

 

3、foreach迴圈遍歷:

        分析Java位元組碼可知,foreach內部實現原理,也是通過Iterator實現的,只不過這個Iterator是Java編譯器幫我們生成的,所以我們不需要再手動去編寫。但是因為每次都要做類型轉換檢查,所以花費的時間比Iterator略長。時間複雜度和Iterator一樣。

使用Iterator的位元組碼:

    Code:
       0: new           #16                 // class java/util/ArrayList
       3: dup
       4: invokespecial #18                 // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: invokeinterface #19,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      14: astore_2
      15: goto          25
      18: aload_2
      19: invokeinterface #25,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      24: pop
      25: aload_2
      26: invokeinterface #31,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      31: ifne          18
      34: return

 

 

使用foreach的位元組碼:

    Code:
       0: new           #16                 // class java/util/ArrayList
       3: dup
       4: invokespecial #18                 // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: invokeinterface #19,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      14: astore_3
      15: goto          28
      18: aload_3
      19: invokeinterface #25,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      24: checkcast     #31                 // class loop/Model
      27: astore_2
      28: aload_3
      29: invokeinterface #33,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      34: ifne          18
      37: return

 


 

 

 

 

各遍歷方式的適用於什麼場合?

 

1、傳統的for迴圈遍歷,基於計數器的:

        順序存儲:讀取性能比較高。適用於遍歷順序存儲集合。

        鏈式存儲:時間複雜度太大,不適用於遍歷鏈式存儲的集合。

2、迭代器遍歷,Iterator:

        順序存儲:如果不是太在意時間,推薦選擇此方式,畢竟代碼更加簡潔,也防止了Off-By-One的問題。

        鏈式存儲:意義就重大了,平均時間複雜度降為O(n),還是挺誘人的,所以推薦此種遍歷方式。

3、foreach迴圈遍歷:

        foreach只是讓代碼更加簡潔了,但是他有一些缺點,就是遍歷過程中不能操作數據集合(刪除等),所以有些場合不使用。而且它本身就是基於Iterator實現的,但是由於類型轉換的問題,所以會比直接使用Iterator慢一點,但是還好,時間複雜度都是一樣的。所以怎麼選擇,參考上面兩種方式,做一個折中的選擇。

 

 

 

 

Java的最佳實踐是什麼?

        Java數據集合框架中,提供了一個RandomAccess介面,該介面沒有方法,只是一個標記。通常被List介面的實現使用,用來標記該List的實現是否支持Random Access。

        一個數據集合實現了該介面,就意味著它支持Random Access,按位置讀取元素的平均時間複雜度為O(1)。比如ArrayList。

        而沒有實現該介面的,就表示不支持Random Access。比如LinkedList。

        所以看來JDK開發者也是註意到這個問題的,那麼推薦的做法就是,如果想要遍歷一個List,那麼先判斷是否支持Random Access,也就是 list instanceof RandomAccess。

比如:

if (list instanceof RandomAccess) {
    //使用傳統的for迴圈遍歷。
} else {
    //使用Iterator或者foreach。
}

 


 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 作者:白狼 出處:http://www.manks.top/article/yii2_captcha本文版權歸作者,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。 本來以為yii2框架驗證碼這塊很全面,嘗試百度google了一下,大多數教程寫 ...
  • 一、 Comparable<T>: Comparable是類內部的比較器,用於創建類的時候實現此介面,同時實現比較方法;對於不能修改源碼的類則無法應用此方式進行比較排序等。 源碼為: 1 public interface Comparable<T> { 2 public int compareTo( ...
  • 這是一個NB的安全認證機制。 1、這是一個安全認證機制 2、可以防止黑客截獲到客戶端發送的請求消息,避免了黑客冒充客戶端向伺服器發送操作的請求。 原理與步驟: 1、客戶端與伺服器端都會放著一份驗證用的token欄位,這欄位無論通過什麼方式前提是不能被黑客提前拿到。 2、客戶端在本地把時間戳和toke ...
  • 亂碼 上節說到亂碼出現的主要原因,即在進行編碼轉換的時候,如果將原來的編碼識別錯了,併進行了轉換,就會發生亂碼,而且這時候無論怎麼切換查看編碼的方式,都是不行的。 我們來看一個這種錯誤轉換後的亂碼,還是用上節的例子,二進位是(16進位表示):C3 80 C3 8F C3 82 C3 AD,無論按哪種 ...
  • 在騷擾了PayPal的技術支持好幾天之後終於成功對接了PayPal支付,非常感謝PayPal的技術支持人員,沒有她估計一周都搞不定。記錄一下這個過程。 接到這個任務聯繫了PayPal的技術之後,第一件事就是向她要了一些文檔。PayPal提供了一個demo商店https://demo.paypal.c ...
  • 前言:當我們進行大的項目書寫的時候或者我們選擇維護程式的時候,想知道幾點幾時我們錄入的數據有bug是那麼我們就採用 》log4j記錄日誌的信息 一、日誌及其分類 1、軟體運行的過程中離不開日誌。日誌主要用來記錄系統運行過程中的一些重要操作信息,便於監視系統運行的情況,幫助用戶避免和發現可能出現的問題 ...
  • 1、接收用戶輸入: input:接收用戶輸入的是合法的python表達式,比如字元串。 raw_input:把所有的輸入當做原始數據(raw data)。 除非對input有特別的需要,否則儘可能使用raw_input函數。 2、長字元串和原始字元串 長字元串常利用'\'經行轉義,例如: 這句話會打 ...
  • 原文地址:http://blog.csdn.net/morewindows/article/details/7421759 使用多線程其實是非常容易的,下麵這個程式的主線程會創建了一個子線程並等待其運行完畢,子線程就輸出它的線程ID號然後輸出一句經典名言——Hello World。整個程式的代碼非常 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...