一文講透為什么遍歷LinkedList要用增強型for循環(huán)
for循環(huán)和鏈表介紹
我們都知道java中有個增強型for循環(huán),這個for循環(huán)很方便,如果不需要知道當前遍歷到第幾個的話可以跟普通for循環(huán)替換使用,也有人知道這倆好像有那么一點點不一樣,但為什么不一樣就不知道了。
我們還知道LinkedList是一個雙向鏈表,這個集合應(yīng)該是唯一一個既實現(xiàn)了List接口又實現(xiàn)了Queue接口的集合類。 鏈表這種數(shù)據(jù)結(jié)構(gòu),跟數(shù)組相比,優(yōu)勢在插入,劣勢在遍歷,那如果要遍歷一個鏈表,就要從頭開始遍歷,否則根本不知道下一個Node是什么。
增強for循環(huán)為什么遍歷LinkedList那么快
其實這個標題不合適,應(yīng)該是為什么普通for循環(huán)遍歷LinkedList為什么那么慢。我們寫代碼驗證一下時間:
public void test() {
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 100000; i++) {//插入100000條數(shù)據(jù)
list.add(i);
}
int index = 0;//記錄最后一個元素
long time1 = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {//普通for循環(huán)遍歷
index = list.get(i);
}
long time2 = System.currentTimeMillis();
LogUtil.Companion.d("1:" + (time2 - time1) + " index->" + index);
for (int i : list) {//增強for循環(huán)遍歷
index = i;
}
long time3 = System.currentTimeMillis();
LogUtil.Companion.d("2:" + (time3 - time2) + " index->" + index);
Iterator<Integer> iterator = list.listIterator();
while (iterator.hasNext()) {//iterator遍歷
index = iterator.next();
}
long time4 = System.currentTimeMillis();
LogUtil.Companion.d("3:" + (time4 - time3) + " index->" + index);
}
運行結(jié)果:
1:5056 index->99999
2:12 index->99999
3:1 index->99999
其實增強型for循環(huán)底層就是用iterator實現(xiàn)的,可以分析兩者的字節(jié)碼得出這個結(jié)論,這里我們不分析,算作一致結(jié)論。來看上面的結(jié)果,發(fā)現(xiàn)普通for循環(huán)遍歷的時間跟增強for循環(huán)和iterator相比簡直令人發(fā)指。 為什么會這樣呢?我們看LinkedList的源碼一探究竟。
LinkedList是一個雙向鏈表,用Head跟Tail兩個Node記錄了頭尾節(jié)點。
LinkedList相關(guān)源碼分析
普通for循環(huán)
我們看到其實普通for循環(huán)只是調(diào)用了LinkedList的 get(index) 方法:
//LinkedList.java
public E get(int index) {
checkElementIndex(index); //只是檢測是否數(shù)組越界
return node(index).item; //調(diào)用了node(index)方法
/**
* Returns the (non-null) Node at the specified element index.
*/
Node<E> node(int index) {
// assert isElementIndex(index);
//判斷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;
}
}
}
get方法很簡單,只是調(diào)用了node方法,node方法也很簡單,只是判斷了index是否是小于size/2,小于說明離Head近一點,否則說明離tail近,離哪個近就從哪一頭開始暴力遍歷,所以如果LinkedList有100000個Node,那最遠的那個Node如果調(diào)用get方法就需要遍歷50000次。
所以普通for循環(huán)遍歷一次n個節(jié)點的LinkedList需要1+2+3+...+n/2+n/2+...+3+2+1次,時間復(fù)雜度可以寫作O(n^2^)。
增強for循環(huán)
可以看到最終是調(diào)用了LinkedList的內(nèi)部類ListItr
//LinkedList.java
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
...
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
這里我們忽略一部分代碼先只看for循環(huán)涉及的方法,代碼其實也很簡單。如果 hasNext() 存在,就調(diào)用next() ,兩個Node:lastReturned和next。
每次獲取index的時候調(diào)用 ListItr(int index) 后next會指向當前index的Node。
調(diào)用next的時候lastReturned會指向next也就是當前index的Node,next指向next.next,所以每次遍歷的時候只要賦值一次就可以得到next的節(jié)點,所以遍歷一個n個節(jié)點的LinkedList就是需要n次。
所以用iterator遍歷的話時間復(fù)雜度就是O(n)。
這就是為什么兩個for循環(huán)的方式這么區(qū)別這么大了~我們也可以直觀的看出來當n到達十萬這個級別的時候O(n^2^)和O(n)差別有多大了。
不知道各位發(fā)現(xiàn)沒有,Iterator里面每個操作都先調(diào)用了 checkForComodification() 方法,判斷 (modCount != expectedModCount) 是否相等。
各位應(yīng)該發(fā)現(xiàn)了ListItr有一個賦值,把modCount賦值給了expectedModCount,但每次調(diào)用遍歷或者addsetget的時候都會判斷這兩個值是否相等。
modCount是父類AbstractList的屬性,而每次調(diào)用add(),remove()方法的時候這個值都會變,也就是如果集合里面內(nèi)容修改了modCount都會發(fā)生改變。
So,在使用Iterator的時候不能調(diào)用add()或者remove()這些會改變集合內(nèi)容的方法。兩種情況:
- 在增強型for循環(huán)里面不能有add或者remove操作,使用Iterator迭代的時候不能做add或者remove操作。
- 如果有其他線程操作集合,需要加鎖避免改變集合,等待循環(huán)結(jié)束之后再修改。
否則都會報ConcurrentModificationException。
以上就是一文講透為什么遍歷LinkedList要用增強型for循環(huán)的詳細內(nèi)容,更多關(guān)于遍歷LinkedList增強型for循環(huán)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實現(xiàn)批量導(dǎo)出導(dǎo)入數(shù)據(jù)及附件文件zip包
這篇文章主要為大家詳細介紹了Java實現(xiàn)批量導(dǎo)出導(dǎo)入數(shù)據(jù)及附件文件zip包的方法,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一2022-09-09
SpringBoot基于Redis實現(xiàn)生成全局唯一ID的方法
在項目中生成全局唯一ID有很多好處,生成全局唯一ID有助于提高系統(tǒng)的可用性、數(shù)據(jù)的完整性和安全性,同時也方便數(shù)據(jù)的管理和分析,所以本文給大家介紹了SpringBoot基于Redis實現(xiàn)生成全局唯一ID的方法,文中有詳細的代碼講解,需要的朋友可以參考下2023-12-12
jdk17+springboot使用webservice的踩坑實戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于jdk17+springboot使用webservice踩坑的相關(guān)資料,網(wǎng)上很多教程是基于jdk8的,所以很多在17上面跑不起來,折騰兩天,直接給答案,需要的朋友可以參考下2024-01-01
java -jar設(shè)置添加啟動參數(shù)實現(xiàn)方法
這篇文章主要介紹了java -jar設(shè)置添加啟動參數(shù)實現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02
spring mvc常用注解_動力節(jié)點Java學(xué)院整理
這篇文章主要介紹了spring mvc常用注解,詳細的介紹了@RequestMapping, @RequestParam, @ModelAttribute等等這樣類似的注解,有興趣的可以了解一下2017-08-08

