list遍歷陷阱分析原理

35.Arraylist 的動態擴容機制是如何自動增長的?簡單說說你理解的增長流程!

解析:javascript

當在 ArrayList 中增長一個對象時 Java 會去檢查 Arraylist 以確保已存在的數組中有足夠的容量來存儲這個新對象,若是沒有足夠容量就新建一個長度更長的數組(原來的1.5倍),舊的數組就會使用 Arrays.copyOf 方法被複制到新的數組中去,現有的數組引用指向了新的數組。下面代碼展現爲 Java 1.8 中經過 ArrayList.add 方法添加元素時,內部會自動擴容,擴容流程以下:java

//確保容量夠用,內部會嘗試擴容,若是須要
ensureCapacityInternal(size + 1) //在未指定容量的狀況下,容量爲DEFAULT_CAPACITY = 10 //而且在第一次使用時建立容器數組,在存儲過一次數據後,數組的真實容量至少DEFAULT_CAPACITY private void ensureCapacityInternal(int minCapacity) { //判斷當前的元素容器是不是初始的空數組 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //若是是默認的空數組,則 minCapacity 至少爲DEFAULT_CAPACITY minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } //經過該方法進行真實準確擴容嘗試的操做 private void ensureExplicitCapacity(int minCapacity) { modCount++;//記錄List的結構修改的次數 //須要擴容 if (minCapacity - elementData.length > 0) grow(minCapacity); } //擴容操做 private void grow(int minCapacity) { //原來的容量 int oldCapacity = elementData.length; //新的容量 = 原來的容量 + (原來的容量的一半) int newCapacity = oldCapacity + (oldCapacity >> 1); //若是計算的新的容量比指定的擴容容量小,那麼就使用指定的容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //若是新的容量大於MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) //那麼就使用hugeCapacity進行容量分配 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; //建立長度爲newCapacity的數組,並複製原來的元素到新的容器,完成ArrayList的內部擴容 elementData = Arrays.copyOf(elementData, newCapacity); }

36.下面這些方法能夠正常運行嗎?爲何?

public void remove1(ArrayList<Integer> list) { for(Integer a : list){ if(a <= 10){ list.remove(a); } } } public static void remove2(ArrayList<Integer> list) { Iterator<Integer> it = list.iterator(); while(it.hasNext()){ if(it.next() <= 10) { it.remove(); } } } public static void remove3(ArrayList<Integer> list) { Iterator<Integer> it = list.iterator(); while(it.hasNext()) { it.remove(); } } public static void remove4(ArrayList<Integer> list) { Iterator<Integer> it = list.iterator(); while(it.hasNext()) { it.next(); it.remove(); it.remove(); } }

解析:算法

remove1 方法會拋出 ConcurrentModificationException 異常,這是迭代器的一個陷阱,foreach 遍歷編譯後實質會替換爲迭代器實現(普通for循環不會拋這個異常,由於list.size方法通常不會變,因此只會漏刪除),由於迭代器內部會維護一些索引位置數據,要求在迭代過程當中容器不能發生結構性變化(添加、插入、刪除,修改數據不算),不然這些索引位置數據就失效了,避免的方式就是使用迭代器的 remove 方法。數組

remove2 方法能夠正常運行,無任何錯誤。緩存

remove3 方法會拋出 IllegalStateException 異常,由於使用迭代器的 remove 方法前必須先調用 next 方法,next 方法會檢測容器是否發生告終構性變化,而後更新 cursor 和 lastRet 值,直接不調用 next 而 remove 會致使相關值不正確。安全

remove4 方法會拋出 IllegalStateException 異常,理由同 remove3,remove 調用一次後 lastRet 值會重置爲 -1,沒有調用 next 去設置 lastRet 的狀況下再直接調一次 remove 天然就狀態異常了。數據結構

固然了,上面四個寫法的具體官方解答可參見 ArrayList 中迭代器部分源碼,以下:框架

private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }

37.簡要解釋下面程序的執行現象和結果?

ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(3); Integer[] array1 = new Integer[3]; list.toArray(array1); Integer[] array2 = list.toArray(new Integer[0]); System.out.println(Arrays.equals(array1, array2)); //1 結果是什麼?爲何? Integer[] array = {1, 2, 3}; List<Integer> list = Arrays.asList(array); list.add(4); //2 結果是什麼?爲何? Integer[] array = {1, 2, 3}; List<Integer> list = new ArrayList<Integer>(Arrays.asList(array)); list.add(4); //3 結果是什麼?爲何?

解析:ide

1 輸出爲 true,由於 ArrayList 有兩個方法能夠返回數組Object[] toArray()<T> T[] toArray(T[] a),第一個方法返回的數組是經過 Arrays.copyOf 實現的,第二個方法若是參數數組長度足以容納全部元素就使用參數數組,不然新建一個數組返回,因此結果爲 true。函數

2 會拋出 UnsupportedOperationException 異常,由於 Arrays 的 asList 方法返回的是一個 Arrays 內部類的 ArrayList 對象,這個對象沒有實現 add、remove 等方法,只實現了 set 等方法,因此經過 Arrays.asList 轉換的列表不具有結構可變性。

3 固然能夠正常運行咯,不可變結構的 Arrays 的 ArrayList 經過構造放入了真正的萬能 ArrayList,天然就能夠操做咯。

38.簡單解釋一下 Collection 和 Collections 的區別?

解析:

java.util.Collection 是一個集合接口,它提供了對集合對象進行基本操做的通用接口方法,在 Java 類庫中有不少具體的實現,意義是爲各類具體的集合提供最大化的統一操做方式。 譬如 Collection 的實現類有 List、Set 等,List 的實現類有 LinkedList、ArrayList、Vector 等,Vector 的實現類有 Stack 等,不過切記 Map 是自立門戶的,其提供了轉換爲 Collection 的方法,可是本身不是 Collection 的子類。

java.util.Collections 是一個包裝類,它包含有各類有關集合操做的靜態多態方法,此類構造 private 不能實例化,就像一個工具類,服務於 Java 的 Collection 框架,其提供的方法大概能夠分爲對容器接口對象進行操做類(查找和替換、排序和調整順序、添加和修改)和返回一個容器接口對象類(適配器將其餘類型的數據轉換爲容器接口對象、裝飾器修飾一個給定容器接口對象增長某種性質)。

39.解釋一下 ArrayList、Vector、Stack、LinkedList 的實現和區別及特色和適用場景?

解析:

首先他們都是 List 家族的兒子,List 又是 Collection 的子接口,Collection 又是 Iterable 的子接口,因此他們都具有 Iterable 和 Collection 和 List 的基本特性。

ArrayList 是一個動態數組隊列,隨機訪問效率高,隨機插入、刪除效率低。LinkedList 是一個雙向鏈表,它也能夠被看成堆棧、隊列或雙端隊列進行操做,隨機訪問效率低,但隨機插入、隨機刪除效率略好。Vector 是矢量隊列,和 ArrayList 同樣是一個動態數組,可是 Vector 是線程安全的。Stack 繼承於 Vector,特性是先進後出(FILO, FirstIn Last Out)。

從線程安全角度看 Vector、Stack 是線程安全的,ArrayList、LinkedList 是非線程安全的。

從實現角度看 LinkedList 是雙向鏈表結構,ArrayList、Vector、Stack 是內存數組結構。

從動態擴容角度看因爲 ArrayList 和 Vector(Stack 繼承自 Vector,只在 Vector 的基礎上添加了幾個 Stack 相關的方法,故以後再也不對 Stack 作特別的說明)使用數組實現,當數組長度不夠時,其內部會建立一個更大的數組,而後將原數組中的數據拷貝至新數組中,而 LinkedList 是雙向鏈表結構,內存不用連續,因此用多少申請多少。

從效率方面來講 Vector、ArrayList、Stack 是基於數組實現的,是根據索引來訪問元素,Vector(Stack)和 ArrayList 最大的區別就是 synchronization 同步的使用,拋開兩個只在序列化過程當中使用的方法不說,沒有一個 ArrayList 的方法是同步的,相反,絕大多數 Vector(Stack)的方法法都是直接或者間接的同步的,所以就形成 ArrayList 比 Vector(Stack)更快些,不過在最新的 JVM 中,這兩個類的速度差異是很小的,幾乎能夠忽略不計;而 LinkedList 是雙向鏈表實現,根據索引訪問元素時須要遍歷尋找,性能略差。因此 ArrayList 適合大量隨機訪問,LinkList 適合頻繁刪除插入操做。

從差別角度看 LinkedList 還具有 Deque 雙端隊列的特性,其實現了 Deque 接口,Deque 繼承自 Queue 隊列接口,其實也挺好理解,由於 LinkedList 是的實現是雙向鏈表結構,因此實現隊列特性實在是太容易了。

40.簡單介紹下 List 、Map、Set、Queue 的區別和關係?

解析:

List、Set、Queue 都繼承自 Collection 接口,而 Map 則不是(繼承自 Object),因此容器類有兩個根接口,分別是 Collection 和 Map,Collection 表示單個元素的集合,Map 表示鍵值對的集合。

List 的主要特色就是有序性和元素可空性,他維護了元素的特定順序,其主要實現類有 ArrayList 和 LinkList。ArrayList 底層由數組實現,容許元素隨機訪問,可是向 ArrayList 列表中間插入刪除元素須要移位複製速度略慢;LinkList 底層由雙向鏈表實現,適合頻繁向列表中插入刪除元素,隨機訪問須要遍歷因此速度略慢,適合當作堆棧、隊列、雙向隊列使用。

Set 的主要特性就是惟一性和元素可空性,存入 Set 的每一個元素都必須惟一,加入 Set 的元素都必須確保對象的惟一性,Set 不保證維護元素的有序性,其主要實現類有 HashSet、LinkHashSet、TreeSet。HashSet 是爲快速查找元素而設計,存入 HashSet 的元素必須定義 hashCode 方法,其實質能夠理解爲是 HashMap 的包裝類,因此 HashSet 的值還具有可 null 性;LinkHashSet 具有 HashSet 的查找速度且經過鏈表保證了元素的插入順序(實質爲 HashSet 的子類),迭代時是有序的,同理存入 LinkHashSet 的元素必須定義 hashCode 方法;TreeSet 實質是 TreeMap 的包裝類,因此 TreeSet 的值不備可 null 性,其保證了元素的有序性,底層爲紅黑樹結構,存入 TreeSet 的元素必須實現 Comparable 接口;不過特別注意 EnumSet 的實現和 EnumMap 沒有一點關係。

Queue 的主要特性就是隊列和元素不可空性,其主要的實現類有 LinkedList、PriorityQueue。LinkedList 保證了按照元素的插入順序進行操做;PriorityQueue 按照優先級進行插入抽取操做,元素能夠經過實現 Comparable 接口來保證優先順序。Deque 是 Queue 的子接口,表示更爲通用的雙端隊列,有明確的在頭或尾進行查看、添加和刪除的方法,ArrayDeque 基於循環數組實現,效率更高一些。

Map 自立門戶,可是也提供了嫁接到 Collection 相關方法,其主要特性就是維護鍵值對關聯和查找特性,其主要實現類有 HashTab、HashMap、LinkedHashMap、TreeMap。HashTab 相似 HashMap,可是不容許鍵爲 null 和值爲 null,比 HashMap 慢,由於爲同步操做;HashMap 是基於散列列表的實現,其鍵和值均可覺得 null;LinkedHashMap 相似 HashMap,其鍵和值均可覺得 null,其有序性爲插入順序或者最近最少使用的次序(LRU 算法的核心就是這個),之因此能有序,是由於每一個元素還加入到了一個雙向鏈表中;TreeMap 是基於紅黑樹算法實現的,查看鍵值對時會被排序,存入的元素必須實現 Comparable 接口,可是不容許鍵爲 null,值能夠爲 null;若是鍵爲枚舉類型可使用專門的實現類 EnumMap,它使用效率更高的數組實現。

從數據結構角度看集合的區別有以下:

動態數組:ArrayList 內部是動態數組,HashMap 內部的鏈表數組也是動態擴展的,ArrayDeque 和 PriorityQueue 內部也都是動態擴展的數組。

鏈表:LinkedList 是用雙向鏈表實現的,HashMap 中映射到同一個鏈表數組的鍵值對是經過單向鏈表連接起來的,LinkedHashMap 中每一個元素還加入到了一個雙向鏈表中以維護插入或訪問順序。

哈希表:HashMap 是用哈希表實現的,HashSet, LinkedHashSet 和 LinkedHashMap 基於 HashMap,內部固然也是哈希表。

排序二叉樹:TreeMap 是用紅黑樹(基於排序二叉樹)實現的,TreeSet 內部使用 TreeMap,固然也是紅黑樹,紅黑樹能保持元素的順序且綜合性能很高。

堆:PriorityQueue 是用堆實現的,堆邏輯上是樹,物理上是動態數組,堆能夠高效地解決一些其餘數據結構難以解決的問題。

循環數組:ArrayDeque 是用循環數組實現的,經過對頭尾變量的維護,實現了高效的隊列操做。

位向量:EnumSet 是用位向量實現的,對於只有兩種狀態且須要進行集合運算的數據使用位向量進行表示、位運算進行處理,精簡且高效。

41.簡單說說 HashMap 的底層原理?

答案:

當咱們往 HashMap 中 put 元素時,先根據 key 的 hash 值獲得這個元素在數組中的位置(即下標),而後把這個元素放到對應的位置中,若是這個元素所在的位子上已經存放有其餘元素就在同一個位子上的元素以鏈表的形式存放,新加入的放在鏈頭,從 HashMap 中 get 元素時先計算 key 的 hashcode,找到數組中對應位置的某一元素,而後經過 key 的 equals 方法在對應位置的鏈表中找到須要的元素,因此 HashMap 的數據結構是數組和鏈表的結合。

解析:

HashMap 底層是基於哈希表的 Map 接口的非同步實現,實際是一個鏈表散列數據結構(即數組和鏈表的結合體)。 首先因爲數組存儲區間是連續的,佔用內存嚴重,故空間複雜度大,但二分查找時間複雜度小(O(1)),因此尋址容易,插入和刪除困難。而鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度小,但時間複雜度大(O(N)),因此尋址困難,插入和刪除容易。 因此就產生了一種新的數據結構------哈希表,哈希表既知足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便,哈希表有多種不一樣的實現方法,HashMap 採用的是鏈表的數組實現方式,具體以下:

首先 HashMap 裏面實現了一個靜態內部類 Entry(key、value、next),HashMap 的基礎就是一個 Entry[] 線性數組,Map 的內容都保存在 Entry[] 裏面,而 HashMap 用的線性數組倒是隨機存儲的緣由以下:

// 存儲時
int hash = key.hashCode(); //每一個 key 的 hash 是一個固定的 int 值 int index = hash % Entry[].length; Entry[index] = value; // 取值時 int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index];

當咱們經過 put 向 HashMap 添加多個元素時會遇到兩個 key 經過hash % Entry[].length計算獲得相同 index 的狀況,這時具備相同 index 的元素就會被放在線性數組 index 位置,而後其 next 屬性指向上個同 index 的 Entry 元素造成鏈表結構(譬如第一個鍵值對 A 進來,經過計算其 key 的 hash 獲得的 index = 0,記作 Entry[0] = A,接着第二個鍵值對 B 進來,經過計算其 index 也等於 0,這時候 B.next = A, Entry[0] = B,若是又進來 C 且 index 也等於 0 則 C.next = B, Entry[0] = C)。 當咱們經過 get 從 HashMap 獲取元素時首先會定位到數組元素,接着再遍歷該元素處的鏈表獲取真實元素。 當 key 爲 null 時 HashMap 特殊處理老是放在 Entry[] 數組的第一個元素。 HashMap 使用 Key 對象的 hashCode() 和 equals() 方法去決定 key-value 對的索引,當咱們試着從 HashMap 中獲取值的時候,這些方法也會被用到,因此 equals() 和 hashCode() 的實現應該遵循如下規則: 若是o1.equals(o2)o1.hashCode() == o2.hashCode()必須爲 true,或者若是o1.hashCode() == o2.hashCode()則不意味着o1.equals(o2)會爲true。

關於 HashMap 的 hash 函數算法巧妙之處能夠參見本文連接:http://pengranxiang.iteye.com/blog/543893

42.簡單解釋一下 Comparable 和 Comparator 的區別和場景?

解析:

Comparable 對實現它的每一個類的對象進行總體排序,這個接口須要類自己去實現,若一個類實現了 Comparable 接口,實現 Comparable 接口的類的對象的 List 列表(或數組)能夠經過 Collections.sort(或 Arrays.sort)進行排序,此外實現 Comparable 接口的類的對象能夠用做有序映射(如TreeMap)中的鍵或有序集合(如TreeSet)中的元素,而不須要指定比較器, 實現 Comparable 接口必須修改自身的類(即在自身類中實現接口中相應的方法),若是咱們使用的類沒法修改(如SDK中一個沒有實現Comparable的類),咱們又想排序,就得用到 Comparator 這個接口了(策略模式)。 因此若是你正在編寫一個值類,它具備很是明顯的內在排序關係,好比按字母順序、按數值順序或者按年代順序,那你就應該堅定考慮實現 Comparable 這個接口, 若一個類實現了 Comparable 接口就意味着該類支持排序,而 Comparator 是比較器,咱們若須要控制某個類的次序,能夠創建一個該類的比較器來進行排序。 Comparable 比較固定,和一個具體類相綁定,而 Comparator 比較靈活,能夠被用於各個須要比較功能的類使用。

43.簡單說說 Iterator 和 ListIterator 的區別?

解析:

ListIterator 有 add() 方法,能夠向 List 中添加對象,而 Iterator 不能。

ListIterator 和 Iterator 都有 hasNext() 和 next() 方法,能夠實現順序向後遍歷,可是 ListIterator 有 hasPrevious() 和 previous() 方法,能夠實現逆向(順序向前)遍歷,Iterator 就不能夠。

ListIterator 能夠定位當前的索引位置,經過 nextIndex() 和 previousIndex() 能夠實現,Iterator 沒有此功能。

均可實現刪除對象,可是 ListIterator 能夠實現對象的修改,經過 set() 方法能夠實現,Iierator 僅能遍歷,不能修改。

容器類提供的迭代器都會在迭代中間進行結構性變化檢測,若是容器發生告終構性變化,就會拋出 ConcurrentModificationException,因此不能在迭代中間直接調用容器類提供的 add、remove 方法,如需添加和刪除,應調用迭代器的相關方法。

44.請實現一個極簡 LRU 算法容器?

解析:

看起來是一道很難的題目,其實靜下來你會發現想考察的其實就是 LRU 的原理和 LinkedHashMap 容器知識,固然,你要是厲害不依賴 LinkedHashMap 本身純手寫擼一個也不介意。 LinkedHashMap 支持插入順序或者訪問順序,LRU 算法其實就要用到它訪問順序的特性,即對一個鍵執行 get、put 操做後其對應的鍵值對會移到鏈表末尾,因此最末尾的是最近訪問的,最開始的最久沒被訪問的。 LRU 是一種流行的替換算法,它的全稱是 Least Recently Used,最近最少使用,它的思路是最近剛被使用的很快再次被用的可能性最高,而最久沒被訪問的很快再次被用的可能性最低,因此被優先清理。 下面給出極簡 LRU 緩存算法容器:

public class LRUCache<K, V> extends LinkedHashMap<K, V> { private int maxEntries; //maxEntries 最大緩存個數 public LRUCache(int maxEntries){ super(16, 0.75f, true); this.maxEntries = maxEntries; } //在添加元素到 LinkedHashMap 後會調用這個方法,傳遞的參數是最久沒被訪問的鍵值對,若是這個方法返回 true 則這個最久的鍵值對就會被刪除,LinkedHashMap 的實現老是返回 false,全部容量沒有限制。 @Override protected boolean removeEldestEntry(Entry<K, V> eldest) { return size() > maxEntries; } } 

本文參與騰訊雲自媒體分享計劃,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索