Home >Web Front-end >JS Tutorial >Analysis of Java collection traversal method (implementation principle, algorithm performance, applicable occasions)_javascript skills

Analysis of Java collection traversal method (implementation principle, algorithm performance, applicable occasions)_javascript skills

WBOY
WBOYOriginal
2016-05-16 15:04:071638browse

概述

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) &#63; last : next.prev;
nextIndex--;
return lastReturned.item;
}

3、foreach循环遍历:

分析Java字节码可知,foreach内部实现原理,也是通过Iterator实现的,只不过这个Iterator是Java编译器帮我们生成的,所以我们不需要再手动去编写。但是因为每次都要做类型转换检查,所以花费的时间比Iterator略长。时间复杂度和Iterator一样。

使用Iterator的字节码:

Code:
new # // class java/util/ArrayList
dup
invokespecial # // Method java/util/ArrayList."<init>":()V
astore_
aload_
invokeinterface #, // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
astore_
goto 
aload_
invokeinterface #, // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
pop
aload_
invokeinterface #, // InterfaceMethod java/util/Iterator.hasNext:()Z
ifne 
return 

使用foreach的字节码:

Code:
new # // class java/util/ArrayList
dup
invokespecial # // Method java/util/ArrayList."<init>":()V
astore_
aload_
invokeinterface #, // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
astore_
goto 
aload_
invokeinterface #, // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
checkcast # // class loop/Model
astore_
aload_
invokeinterface #, // InterfaceMethod java/util/Iterator.hasNext:()Z
ifne 
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。
}

以上所述是小编给大家介绍的Java遍历集合方法分析(实现原理、算法性能、适用场合),希望对大家有所帮助!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn