集合包是java中最經常使用的包,最經常使用的有Collection和Map接口的實現類,前者用於存放多個單對象,後者用於存放key-value形式的鍵值對。html
java集合包經常使用實現類結構圖以下所示(I表示接口)java
線性表,有序集合,元素能夠重複。程序員
動態數組,底層即數組,能夠用來容納任何對象,要求連續存放。ArrayList的重要成員是Object[] elementDate int Size表示有效元素的個數,數組的初始大小是10,擴容操做示意圖以下,以前(上面)這塊內存變成垃圾。 所以在初始化時儘可能指定初始容量,避免擴容產生的內存垃圾,影響性能。面試
public ArrayList(int initialCapacity) { super(); if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); this.elementData = new Object[initialCapacity]; }// 指定初始化大小
(1)foreach中可否對其基本類型元素值(包括數字、字符串、布爾)進行賦值改變?算法
不能,高級For在JDK 5.0開始引入,用其迭代代碼簡潔,在foreach中修改的只是元素的副本,並不會改變原值(反編譯以後可發現這一點),因此高級For循環能夠用來遍歷查詢,不可修改當前取回的元素自己。swift
參考連接:foreach的一個「奇怪」現象—實現原理分析數組
可是對象類型的元素有所不一樣,若是對對象數組進行遍歷,則能夠修改元素的狀態,在每一輪循環中均可以拿到對象引用對其狀態(成員變量、類變量)進行修改。緩存
(2)如何正確遍歷並刪除ArrayList元素值?安全
問題背景:給定字符串集合["a","b","b","c"],刪除其中元素"b"。數據結構
常見錯誤寫法1:
public static void remove(List<String> list) { for (int i = 0; i < list.size(); i++) { String s = list.get(i); if (s.equals("b")) { list.remove(s); } } } // 錯誤的緣由:這種最普通的循環寫法執行後會發現第二個「b」的字符串沒有刪掉。
常見錯誤寫法2:
public static void remove(List<String> list) { for (String s : list) { if (s.equals("b")) { list.remove(s); } } } // 使用加強的for循環 在循環過程當中從List中刪除元素之後,繼續循環List時會報ConcurrentModificationException,但刪除以後立刻就跳出的也不會出現異常
思考及對策
爲什麼和出現錯誤1,怎麼解決?
public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) if (elementData[index] == null) { fastRemove(index); return true; } } else { for (int index = 0; index < size; index++) if (o.equals(elementData[index])) { fastRemove(index); return true; } } return false; } private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index,numMoved); elementData[--size] = null; // Let gc do its work }
能夠看到會執行System.arraycopy方法,致使刪除元素時涉及到數組元素的移動。針對錯誤寫法1,在遍歷第一個字符串b時由於符合刪除條件,因此將該元素從數組中刪除,而且將後一個元素移動(也就是第二個字符串b)至當前位置,致使下一次循環遍歷時後一個字符串b並無遍歷到,因此沒法刪除。針對這種狀況能夠倒序刪除的方式來避免:
public static void remove(ArrayList<String> list) { for (int i = list.size() - 1; i >= 0; i--) { String s = list.get(i); if (s.equals("b")) { list.remove(s); } } }
爲什麼會出現錯誤2怎麼解決?
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
這裏會作迭代器內部修改次數檢查,由於上面的remove(Object)方法修改了modCount的值,因此纔會報出併發修改異常。要避免這種狀況的出現則在使用迭代器迭代時(顯示或for-each的隱式)不要使用ArrayList的remove,改成用Iterator的remove便可:
public static void remove(List<String> list) { Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("b")) { it.remove(); } } }
部分參考:ArrayList循環遍歷並刪除元素的常見陷阱; 如何正確遍歷刪除List中的元素,你會嗎?
(3)ArrayList非線程安全,如何解決?
Collections給出瞭解決方案,提供了synchronizedCollection方法,該方法返回一個線程安全容器。
*Java中經常使用的集合框架中的實現類HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap、TreeMap都是線程不安全的,若是有多個線程同時訪問它們,且同時有多個線程修改他們的時候,將會出現如讀髒數據等錯誤。Collections給出瞭解決方案,提供了synchronizedCollection方法來實現線程安全,該方法返回一個線程安全容器。
(4)contains方法contains()方法是經過將傳入的實際參數和集合中已有的元素進行equals()比較來實現的,Object類中的equals()方法比較的是兩個對象的地址,所以須要根據實際須要重寫equals()方法。
ArrayList和LinkedList是List(線性表)的兩種典型實現:基於數組和基於雙向鏈表的線性表。通常而言,因爲數組以一塊連續內存區來保存全部的數組元素,因此數組在隨機訪問時性能最好,全部的內部以數組做爲底層實現的集合在隨機訪問時性能都比較好;而內部以鏈表做爲底層實現的集合在執行插入、刪除操做時有較好的性能。但整體來講,ArrayList的性能要更好,所以大部分使用時都應該考慮使用ArrayList。
Vector是基於synchronized機制實現的線程安全的ArrayList,但在插入元素容量擴充時機制略有不一樣,經過傳入的capacityIncrement來控制容量的擴充。
Stack繼承自Vector,在此基礎上實現了棧的彈出以及壓入和彈出操做,push、pop、peek(只返回,不出棧)
鍵值對、鍵惟1、值不惟一
底層實現
JDK7實現,數組+鏈表;JDK8實現,數組+紅黑樹。
存儲過程
當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部。流程圖以下所示:
讀取實現
若是 HashMap 的每一個 bucket 裏只有一個 Entry 時,HashMap 能夠根據索引、快速地取出該 bucket 裏的 Entry;在發生「Hash 衝突」的狀況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,系統只能必須按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那系統必須循環到最後才能找到該元素,時間複雜度取決於鏈表的長度,爲 O(n)。爲了下降這部分的開銷,在 Java8 中當鏈表中的元素超過了 8 個且數組容量大於64之後會將鏈表轉換爲紅黑樹(Hollis-HashMap中傻傻分不清楚的那些概念、郭霖:面試必問的HashMap,你真的瞭解嗎?),在這些位置進行查找的時候能夠下降時間複雜度爲 O(logN)。
HashMap 在底層將 key-value 當成一個總體進行處理,這個總體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存全部的 key-value 對,當須要存儲一個 Entry 對象時,會根據 Hash 算法來決定其存儲位置;當須要取出一個 Entry 時,也會根據 Hash 算法找到其存儲位置,直接取出該 Entry。
性能選項
實際容量(capacity):大於initial capacity最小的2的n次方,如10<16
初始容量(initial capacity):能夠經過HashMap的構造函數指定
size:變量保存了該 HashMap 中所包含的 key-value 對的數量
threshold:該變量包含了 HashMap 能容納的 key-value 對的極限,它的值等於 HashMap 的容量乘以負載因子(load factor)
負載因子(load factor):決定了Hash表的最大填滿程度
當建立 HashMap 時,有一個默認的負載因子(load factor),其默認值爲 0.75,這是時間和空間成本上一種折衷:增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間。掌握了上面知識以後,咱們能夠在建立 HashMap 時根據實際須要適當地調整 load factor 的值;若是程序比較關心空間開銷、內存比較緊張,能夠適當地增長負載因子;若是程序比較關心時間開銷,內存比較寬裕則能夠適當的減小負載因子。一般狀況下,程序員無需改變負載因子的值。
採用紅黑樹的數據結構來管理key-value對(紅黑樹的每一個節點就是一個key-value對)
存儲key-value對須要根據key排序,排序方式與TreeSet相同(兩種排序方式)。
判斷兩個key相等的標準是:經過compareTo()返回0即認爲這兩個key相等。
對於自定義類做爲key的狀況,最好作到:兩個key經過equals()方法比較返回true時,compareTo()方法也返回0。
兩種排序的比較器:
java.lang.Comparable,TreeMap使用無參構造函數,那麼容納的對象必須實現Comparable接口。
public int compareTo(T o);
java.util.Comparator,TreeSet在構造時使用Comparator做爲構造函數的參數。(兩種排序方式同時存在時,該方式優先權更高)
int compare(T o1, T o2);
LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,HashMap+LinkedList,即它既使用HashMap操做數據結構,又使用LinkedList維護插入元素的前後順序,經過維護一個運行於全部條目的雙向鏈表,LinkedHashMap保證了元素迭代的順序。該迭代順序能夠是插入順序或者是訪問順序。
能夠經過LinkedHashMap實現LRU算法緩存。構造參數accessOrder爲true則全部的Entry按照訪問的順序排列,爲false則全部的Entry按照插入的順序排列,若是設置爲true,每次訪問都把訪問的那個數據移到雙向隊列的尾部去,那麼每次要淘汰數據的時候,雙向鏈表最頭的那個數據就是要淘汰的數據。
public class LRUCache extends LinkedHashMap { public LRUCache(int maxSize) { super(maxSize, 0.75F, true); maxElements = maxSize; } protected boolean removeEldestEntry(java.util.Map.Entry eldest) { return size() > maxElements; } private static final long serialVersionUID = 1L; protected int maxElements; }
Hashtable 是遺留類,不少映射的經常使用功能與 HashMap 相似,不一樣的是它承自 Dictionary 類,而且是線程安全的,任一時間只有一個線程能寫 Hashtable,併發性不如 ConcurrentHashMap,由於 ConcurrentHashMap 引入了分段鎖。Hashtable 不建議在新代碼中使用,不須要線程安全的場合能夠用 HashMap 替換,須要線程安全的場合能夠用 ConcurrentHashMap 替換。
無序、元素不可重複
HashSet的底層實現用到HashMap,HashSet操做的就是HashMap。(Java源碼就是先實現HashMap、TreeMap,而後包裝一個value爲null的Map集合來實現Set集合的)
存儲過程
兩個對象的equals()方法返回true,可是hashCode值不相等,這時HashSet會把這兩個對象存儲在Hash表的不一樣位置,兩個對象都可以添加成功,但這會與Set集合的規則相沖突。(這就是不重寫相應hashCode()方法的後果,也可閱讀參考:爲何重寫equals方法,必定要重寫HashCode方法?)
因此阿里Java規範這樣寫道:
HashSet如何判斷對象是不是相同
兩個對象經過equals()方法比較相等,而且兩個對象的hashCode方法的返回值也相等。
所以須要注意:
當把一個對象放入HashSet中時,若是須要重寫此對象對應類的equals()方法,則同時須要重寫其hashCode()方法。具體規則是:若是兩個對象經過equals()方法比較返回true時,則它們的hashCode值也應該相等(Object規範:相等的對象必須具備相等的hashcode)。
底層是TreeMap,集合中的元素處於排序狀態。採用紅黑樹的數據結構來存儲集合元素。TreeSet支持兩種排序方式:天然排序(默認狀況)和定製排序。
當把一個對象添加到TreeSet中時,TreeSet會調用該對象的compareTo(Object obj)方法與容器中的其它對象比較大小,而後根據紅黑樹結構找到它的存儲位置。若是compareTo(Object obj)返回0,意味着兩個對象相同將沒法添加到TreeSet集合中。
LikedHashSet是HashSet的子類,它也是根據元素的HashCode值進來決定元素的存儲位置,但它可以同時使用鏈表來維護元素的添加次序,使得元素能以插入順序保存。
附:阿里Java規範-集合
import java.util.*; class A { @Override public int hashCode() { return UUID.randomUUID().hashCode(); } @Override public boolean equals(Object obj) { return true; } } public class Main { public static void main(String[] args) throws Exception { List<A> list = new ArrayList<>(); A a1 = new A(); A a2 = new A(); list.add(a1); // list contains()方法是經過將傳入的實際參數和集合中已有的元素進行equals()比較來實現的 System.out.println(list.contains(a2)); Map<A, Object> map = new HashMap<>(); map.put(a1, null); System.out.println(map.containsKey(a2)); Set<A> set = new HashSet<>(); set.add(a1); System.out.println(set.contains(a2)); } } // output: true false false
那麼HashSet是如何判斷對象是不是相同的呢?
兩個對象經過equals()方法比較相等,而且兩個對象的hashCode方法的返回值也相等。
所以須要注意:
當把一個對象放入HashSet中時,若是須要重寫此對象對應類的equals()方法,則同時須要重寫其hashCode()方法。具體規則是:若是兩個對象經過equals()方法比較返回true時,則它們的hashCode值也應該相等(Object規範:相等的對象必須具備相等的hashcode)。
Map與Set相似,List稍有不一樣,其contains()方法是經過將傳入的實際參數和集合中已有的元素進行equals()比較來實現的,Object類中的equals()方法比較的是兩個對象的地址,所以須要根據實際須要重寫equals()方法。