[Java]-集合框架
Java集合框架是非常普遍使用,也是非常重要的部分,同時也是很基礎的部分,熟練掌握很重要,它對於數據的操作提供了良好的接口,下面將從整個集合框架的體系介紹重要的集合框架類,使用方法,以及內部原理。
一、簡介:
1、集合框架分兩大類(接口):
- Collection:存儲單個數據或者對象。
- |-List:列表:
- |-LinkedList :基於鏈表的List
- |-ArrayList :基於數組的List
- |-SubList:一個List的視圖
- |-Vector:一個線程安全的List
-
-
- |-Stack:
-
- |-Queue:隊列 ,通常作為數據存儲結構,不作為操作容器。集合框架中應用比較少,主要在在並發包(java.util.concurrent)中實現的阻塞隊列(消息隊列)。
-
- |-ArrayDeque:基於數組實現的尾插隊列(包含head,tail頭尾指針).
- |-PriorityQueue:基於數組的自排序優先級隊列(基於數組實現存儲的二叉樹堆排)。
-
- |-Set:一個不允許重復數據的集合
- |-HashSet:基於Hash+數組+鏈表實現的Set。
- |-LinkedHashSet:一個基於LinkedHashMap實現的Set。
- |-TreeSet:一個基於TreeMap實現的Set。
- |-EnumSet:
- |-JumboEnumSet
- |-RegularEnumSet
- |-HashSet:基於Hash+數組+鏈表實現的Set。
- |-List:列表:
- Map:存儲一組K-V鍵值對。
- |-HashMap:基於Hash+數組+鏈表實現的Map。
- |-LinkedHashMap:基於HashMap實現的雙向鏈表。
- |-HashTable:基於Hash+數組+鏈表實現的線程安全(sync)Map。
- |-TreeMap:基於紅黑樹實現的有序Map。
- |-WeakHashMap:K為弱引用的HashMap,使用中,若k沒有被引用則會自動回收掉。
- |-HashMap:基於Hash+數組+鏈表實現的Map。
2、集合操作類:Conllections
- 提供了一系列的Collection接口實現類的靜態操作方法。
- 比如:集合排序,二分查找,列表反轉
- 比如:list初始化填充fill,復制,求最大最小值。。
- 比如:如何獲取一個線程安全的Map,List,Set:
二、整體結構類圖:點擊圖片看原圖。
三、接口實現類:
List接口實現類: |
Map接口實現類 |
Set接口實現類 | Queue接口實現 |
ArrayList |
HashMap |
HashSet | ArrayDeque |
LinkedList |
Hashtable | PriorityQueue | |
Vector |
TreeMap | TreeSet | |
Stack |
LinkedHashMap |
LinkedHashSet |
1、集合框架中的數據結構:
- 哈希算法:
-
*哈希算法: * 0、哈希算法即散列函數,又稱哈希函數。它是一種單向密碼體制,即它是一個從明文到密文的不可逆的映射,只有加密過程,沒有解密過程。同時,哈希函數 * 可以將任意長度的輸入經過變化以後得到固定長度的輸出。哈希函數的這種單向特征和輸出數據長度固定的特征使得它可以生成消息或者數據 * 1、哈希算法將任意長度的二進制值映射為較短的固定長度的二進制值,這個小的二進制值稱為哈希值。哈希值是一段數據唯一且極其緊湊的 * 數值表示形式。因此java中每一個對象都有一個hashcode值,用於表明對象的唯一性,可用於比較對象是否相同。 * 2、“===”和equals * 在對象比較的時候我們很容易就糾結用equals方法,還是“==”號,equals用於比較對象的內容或者值,而“==”不僅要比較值還要比較hashcode. * * 3、哈希算法的不可逆和無沖突<hash值,>的特性,讓它產生巨大的作用: * 3.1、不同的數據產生的hash值不同: * 用於校驗數據的完整性<我們經常下載官網的一些軟件時都會又有一個MD5校驗碼,它是用於校驗軟件是否被惡意更改>,當然 * 也可以做加密,信息摘要。 * 3.2、哈希表+哈希函數+碰撞沖突處理: * 用於數據的存儲和快速查找,<我們只需要一個輸入就可以找到數據存儲的位置,相比於遍歷速度幾何倍數提升> * 3.3、常見的hash表的方法 * 4、典型的Hash算法: * MD2、MD4、MD5 和 SHA-1 * 5、得到一個好的hash算法分為兩步: * 5.1 構造hash函數:計算值->散列得到坐標/位置<散列法> * 好的hash函數表現是數據沖突盡量少,hash表長度合理。不同的函數產生的表長度是不一樣的,hash函數的構造方法也很多,也可 * 以多方法組合使用,最常用的hash函數:http://blog.csdn.net/mycomputerxiaomei/article/details/7641221 * ------- * 除法散列法:ndex = value % 16 * 平方散列法:index = (value * value) >> 28 (右移,除以2^28。記法:左移變大,是乘。右移變小,是除。) * 斐波那契(Fibonacci)散列法 * 5.2 解決碰撞沖突: * 5.2.1開放定址法: * 線性探測再散列: * 二次探測在散列: * 為隨機探測再散列: * 5.2.2再哈希法: * 5.2.3鏈地址法: * 5.2.4建立公共溢出區
- 紅黑樹:http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html
- 鏈表,數組:
2、集合框架中唯一的兩個線程安全集合:
- Vector(Stack是Vector的子類也是安全的)
- Hashtable
3、為什麽線程安全的集合框架(Vector,Hashtable)現在越來越少用??
-
-
Vector實現了RandomAccess和List接口,可以隨機讀取,底層是數組實現,擁有和ArrayList性能,但是它實現了線程同步,使用同步鎖:sychonrized,在非同步情況下,完全沒必要使用,比ArrayList要慢很多。而hashTable也是采用的sychonrized對方法枷鎖,且鎖定的是整個哈希表,高並發情況下很容易出現瓶頸。相比於單線程使用Vector,hashtable無非是吃力不討好。而在於並發情況下,已經有更好的線程安全的Map,List,Set,在性能上遠超Vector和hashtable。
4、ArrayList,LinkedList,Vector和synchronizedList(List)異同點:
- List是數組的封裝接口,所有實現類都可與數組轉換,且進行index(索引下標)獲取,對於ArrayList和LinkedList只不過是不同的實現方式,自然性能也是不同的。ArrayList是對數組的封裝,使得對數組的操作更加方便,LinkedList是鏈表的實現。兩種結構的優缺點無非就是數組和鏈表的優缺點即在查找和刪除的性能差異。
- Arraylist:實現了RandomAccess,可隨機讀取。可指定初始空間大小,默認為10,超過此空間,數組空間增加以前的一半。
-
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8683452581122892189L; /** * 默認初始空間10 */ private static final int DEFAULT_CAPACITY = 10; /** * 數組的最大容量 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** *用於存儲數據的數組 */ transient Object[] elementData; // non-private to simplify nested class access
- 數組擴容:變為以前的1.5倍。
-
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; //新的容量=old容量+old/2 int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
- LinkedList:通過實現AbstractSequentialList來實現隨機的索引讀取。每個節點是Node,存有兩個指針Next,Prev,是個雙向鏈表。初始size=0,動態增加,不需預先分配空間,且實現了queue接口,,可以查看首(first)尾(last)元素.
-
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { transient int size = 0; /** * Pointer to first node.指向頭節點 * Invariant: (first == null && last == null) || * (first.prev == null && first.item != null) */ transient Node<E> first; /** * Pointer to last node.指向尾節點 * Invariant: (first == null && last == null) || * (last.next == null && last.item != null) */ transient Node<E> last;
-
private static class Node<E> { E item; Node<E> next;//指向下一個元素 Node<E> prev;//指向前一個元素 Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
- 獲取一個線程安全的List:通過集合的操作類Collections的synchronizedList方法將一個普通List變成一個線程安全的List。
-
/** * 獲取一個線程安全的LinkedList */ List synLinkedList = Collections.synchronizedList(new LinkedList()); /** * 獲取一個線程安全的ArrayList */ List synArrayList = Collections.synchronizedList(new ArrayList());
5、HashMap,HashSet,HashTable中的關聯和差異。
HashMap:
-
K,V都可為null,存在get返回null的方法並不能確定當前k是否存在(返回null一種代表V==null,一種代表未查詢到K,需用containskey)叠代器:Iterator。支持fail-fast
- HashMap:集合框架中尤為重要,高效的存儲和查詢能力使得他被廣泛的運用和考察,這得益於hash算法的高效,通過一次計算就可得到存儲的位置,非常方便。理想狀態的hash算法是哈希表中的元素都是正常分布(不存在沖突)的,get,put操作時間復雜度都是O(1).叠代一個哈希表所需要的時間與哈希表的容量成正比,因此如果叠代性能不是很重要,不要將初始容量設置太高(或負載因子太低).
- HashMap采用數組鏈表的結構<拉鏈法>來處理沖突,將散列到同一位置的數據頭插法插入一個鏈表。在java8中對處理沖突的方式進行了優化,當鏈表的長度大於8的時候將這個鏈表重構成紅黑樹。
- 初始容量initial capacity:16和負載因子local Factor:0.75:兩個影響性能的重要因素。
-
/** * The default initial capacity - MUST be a power of two. * 默認初始容量16,指的是桶的數量(數組的長度),必須是2的N次冪 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. * 最大的容量,可以在構造的時候傳入參數,但是必需是小於2的30次冪=1073741824 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 默認的負載因子0.75,即使用容量超過初始容量*0.75必須擴容 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 鏈表轉紅黑樹的閾值,大於這個值轉換 */ static final int TREEIFY_THRESHOLD = 8; /** * 紅黑樹轉鏈表的閾值,小於這個值轉換 */ static final int UNTREEIFY_THRESHOLD = 6;
負載因子的默認值是0.75, 它平衡了時間和空間復雜度。 負載因子越大會降低空間使用率,但提高了查詢性能(表現在哈希表的大多數操作是讀取和查詢)
- hashMap的結構:一個Node<K,V>類型的數組
-
/** * 哈希表,每個節點是Node<K,V>結構,容量總是保持2的N次冪。 */ transient Node<K,V>[] table;
數組的每個節點Node又是一個鏈表存儲下一個節點的位置Next,這個Next仍然是個Node:這種結構決定了頭插法來鏈接鏈表的節點。同時也解決了我們的一個困惑,以往在get的時候以為節點只存儲了Value,Node說明了它連K,V一起存儲的,碰撞的時候hash值相同,我們也可以比較K的值來確定要返回的Value
-
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; }
- 獲取數據的在哈希表的位置:散列過程
-
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 哈希表擴容:容量翻倍,threshold表示當HashMap的size(Node<K,V>的數量)大於threshold時會執行resize操作。 threshold=capacity*loadFactor
-
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length;//舊表容量 int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) {//容量達到最大值 threshold = Integer.MAX_VALUE;//把擴展閾值修改為最大,這樣以後就不能擴容了 return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 閾值翻倍 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
- get的實現:
-
/** * Implements Map.get and related methods * * @param hash hash for key * @param key the key * @return the node, or null if none */ final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode)//如果是樹形Node,則調用TreeNode的get方法 return ((TreeNode<K,V>)first).getTreeNode(hash, key); do {//否則直接查詢Next比較Key值。 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null;//查詢不到返回null }+
- Put的實現:
-
/** * Implements Map.put and related methods * * @param hash hash for key * @param key the key * @param value the value to put * @param onlyIfAbsent if true, don‘t change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//hash,k都相同,節點覆蓋掉 e = p; else if (p instanceof TreeNode)//如果是樹形Node調用putTreeNode方法 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {//否則鏈表Node鏈接 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st,當節點容量bincount>=轉化因子的時候轉化成紅黑樹結構 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashTable:
- 線程安全,K,V都不能為null,繼承Dictionry,擁有不同的叠代器:Enumeration(枚舉類)
- 同樣采用hash表存儲:
-
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { /** * The hash table data. */ private transient Entry<?,?>[] table; }
-
/** * Hashtable bucket collision list entry */ private static class Entry<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Entry<K,V> next; protected Entry(int hash, K key, V value, Entry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
- 初始容量:11,負載因子:0.75f
-
/** * Constructs a new, empty hashtable with a default initial capacity (11) * and load factor (0.75). */ public Hashtable() { this(11, 0.75f); }
- get方法:實現要比hashMap簡單的多但是效率問題並不知道,但是可以看到它是線程安全的。
-
public synchronized V get(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length;//索引位置計算 for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; }
HashSet:hashSet放在這裏是因為HashSet完全就是HashMap的包裝版本,內部直接是用了HashMap.
-
public HashSet() {//無參構造直接構造一個hashmap map = new HashMap<>(); } public HashSet(Collection<? extends E> c) {//傳入集合直接調用hashmap的默認參數構造map map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } HashSet(int initialCapacity, float loadFactor, boolean dummy) {//使用LinkedHashmap構造 map = new LinkedHashMap<>(initialCapacity, loadFactor); }
- add方法:將傳入的值作為K,一個靜態的final對象PRESENT作為Value存入map,那麽他的特點是不能存重復數據,因為map中會K相同會散列到同一位置覆蓋掉。跟Hashmap中不能存儲K相同的元素是一樣的。
-
// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
public boolean add(E e) { return map.put(e, PRESENT)==null; }
- 獲取安全的Map和Set:
-
Collections.synchronizedMap(new HashMap<Object, Object>()); Collections.synchronizedMap(new LinkedHashMap<Object, Object>()); Collections.synchronizedSet(new HashSet<Object>()); Collections.synchronizedSet(new TreeSet<Object>());
HashMap與HashTable最大的區別在於hashtable是線程安全的,HashMap數據結構也經過不斷地優化,就鏈表結構轉紅黑樹的在極端情況帶來的性能提升就是很大的進步。所以在線程安全的情況下首選hashMap存儲K-V鍵值對,存儲單值選hashSet(前提是數據不重復,否則就會丟失數據)查找速度理想是O(1)。hashMap K-V都可為null,hashTable K-V都不能夠為null.
6、Iterator和Enumeration的區別:
Enumeration 是JDK 1.0添加的接口。使用到它的函數包括Vector、Hashtable等類,這些類都是JDK 1.0中加入的,Enumeration存在的目的就是為它們提供遍歷接口。Enumeration本身並沒有支持同步,而在Vector、Hashtable實現Enumeration時,添加了同步。
Iterator 是JDK 1.2才添加的接口,它也是為了HashMap、ArrayList等集合提供遍歷接口。Iterator是支持fail-fast機制的:當多個線程對同一個集合的內容進行操作時,就可能會產生fail-fast事件。
7、TreeMap和TreeSet:
TreeMap:
- 類圖:
- 基於紅黑樹實現的有序Map。默認是K的字典比較排序,可傳入比較器。
-
/** * The comparator used to maintain order in this tree map, or * null if it uses the natural ordering of its keys. * * @serial */ private final Comparator<? super K> comparator; /** * 構造一個帶比較器的Treemap */ public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; }
- get操作:
-
final Entry<K,V> getEntry(Object key) { // Offload comparator-based version for sake of performance if (comparator != null) return getEntryUsingComparator(key); if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; Entry<K,V> p = root; while (p != null) { int cmp = k.compareTo(p.key); if (cmp < 0) p = p.left; else if (cmp > 0) p = p.right; else return p; } return null; }
完全是基於一個樹的查找。put操作也一樣是基於樹的插入操作。。
TreeSet
- 類圖:
- 基於TreeMap構造的Set,基本操作都是TreeMap的操作
-
public TreeSet() { this(new TreeMap<E,Object>()); }
四、線程安全並不安全:
1、為什麽並不安全:
- Collections類可以創建線程安全的集合類,當然他的每一個方法都是枷鎖的,可以保證每一步的操作是安全的,但是對於一個類似事務來說(包含多種操作)不能保證原子性。所以Collections創建的線程安全類,又叫做有條件的線程安全。如下:
-
Map synmap = Collections.synchronizedMap(new HashMap<Object, Object>()); if(synmap.containsKey(K)){//synmap.containsKey(K)安全的操作 ....... //這是個不安全的位置,因為這個空擋期間,可能有多個線程進入到了這裏,其中一個執行remove之後,其他線程就會報錯 synmap .remove(K);//安全的操作: }
所以要保證上面操作的安全性必須給整個操作枷鎖。等等一些和刪除,修改相關的操作都可能會出現類似的問題。
2、Collections類的線程安全集合相比於Vector,hashtable性能有多大的優勢?
- 優勢一個在於原本數據結構的優勢,在安全方面,collections采用的是Object 對象鎖,對代碼塊枷鎖,而Vector,hashtable是方法鎖,加在實例對象上。至於兩種方式有沒有性能差異,暫時沒有測試。
3、終極大殺招:java.util.concurrent並發包中的ConcurrentHashMap...從代碼上重構hashmap,實現並發安全。
[Java]-集合框架