35.Arraylist 的動態擴容機制是如何自動增加的?簡單說說你理解的增加流程!
解析:
當在 ArrayList 中增加一個對象時 Java 會去檢查 Arraylist 以確保已存在的數組中有足夠的容量來存儲這個新對象,如果沒有足夠容量就新建一個長度更長的數組(原來的1.5倍),舊的數組就會使用 Arrays.copyOf 方法被復制到新的數組中去,現有的數組引用指向了新的數組。下面代碼展示為 Java 1.8
中通過 ArrayList.add
方法添加元素時,內部會自動擴容,擴容流程如下:
//確保容量夠用,內部會嘗試擴容,如果需要
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 結果是什么?為什么?
解析:
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; } }
本文參與騰訊雲自媒體分享計划,歡迎正在閱讀的你也加入,一起分享。