集合相關知識
摘要:本文主要介紹了幾種集合類型以及有關的一些知識點。
集合類圖
類圖
類圖說明
所有集合類都位於java.util包下。Java的集合類主要由兩個接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口,這兩個接口又包含了一些子接口或實現類。
接口用短虛線表示,表示不同集合類型,是集合框架的基礎。例如Collection,Map,List,Set,Iterator等。
抽象類用長虛線表示,對接口的部分實現。例如AbstractMap,AbstractCollection,AbstractList,AbstractSet等。
實現類用實線表示,對接口的具體實現。例如ArrayList,LinkedList,HashSet,HashMap等。
集合概述
Collection接口是一組允許重復的對象,屬於單列集合。
List接口繼承自Collection接口,允許插入重復的數據,是有序的集合,可以通過索引訪問元素。
Set接口繼承自Collection接口,不允許插入重復的數據,是無序的集合。
Map接口是一組保存了key-value鍵值對的對象,屬於雙列集合,只能根據每個鍵值對的key訪問value。
Collection接口
Collection是一個接口,是高度抽象出來的集合,它包含了集合的基本操作和屬性。Collection包含了List和Set兩大分支。
常用方法
添加單個元素:boolean add(Object object);
添加一個集合裏的所有元素:boolean addAll(Collection<? extends E> collection);
刪除單個元素:boolean remove(Object object);
刪除指定集合裏有的元素:boolean removeAll(Collection collection);
刪除兩個集合都有的元素:boolean retainAll(Collection collection);
判斷是否包含某個元素:boolean contains(Object object);
判斷是否包含指定集合的所有元素:boolean containsAll(Collection<?> collection);
判斷集合是否為空:boolean isEmpty();
清除集合裏的元素:void clear();
獲取集合元素個數:int size();
將集合轉換為數組:Object[] toArray();
將集合轉換為指定類型的數組:<T> T[] toArray(T[] array);
獲取集合叠代器:Iterator iterator();
集合同數組的比較
數組長度一旦固定,不能再改變,集合的長度是可以改變的。
數組只能保存相同類型的數據,集合可以保存指定類型或其子類型的數據。
數組在使用的時候相對比較麻煩,集合可以利用多種方法,還有工具類。
List接口
List接口繼承自Collection接口,允許定義一個重復的有序集合,集合中的每個元素都有對應的一個索引,可以通過索引訪問List中的元素。
實現List接口的實現類主要有:ArrayList、LinkedList、Vector、Stack。
特點
允許重復。
有序,取出的順序和插入的順序一致。
為每一個元素提供一個索引值,默認從0開始。
常用方法
在指定索引位置添加單個元素:void add(int index, Object object);
在指定索引位置添加一個集合:boolean addAll(int index, Collection<? extends E> collection);
刪除指定位置的單個元素:Object remove(int index);
獲取指定位置的單個元素:Object get(int index);
替換指定位置的單個元素:Object set(int index, Object object);
獲取指定元素的出現的第一個索引:int indexOf(Object object);
獲取指定元素的出現的最後一個索引:int lastIndexOf(Object object);
獲取指定位置的集合,包含起始位置,不包含結束位置:List<E> subList(int fromIndex, int toIndex);
獲取集合叠代器:ListIterator<E> listIterator();
ArrayList類
特點
ArrayList是動態數組結構,也是我們最常用的集合,允許任何符合規則的元素插入,包括null。
ArrayList提供了索引機制,可以通過索引迅速查找元素,查找效率高。但是每次增加或刪除元素時,身後的元素都要移動,所以增刪效率低。
ArrayList的操作是非同步的,是線程不安全的。
擴容機制
數組結構都會有容量的概念,ArrayList的初始容量為10,加載因子是1,當快插入元素後長度超出原有長度時會進行擴增,擴容增量是0.5,擴增後容量為1.5倍,可使用方法手動擴容和縮減。
如果一開始就明確所插入元素的多少,最好指定一個初始容量值,避免過多的進行擴容操作而浪費時間和效率。
Vector類
特點
與ArrayList相似,它的操作與ArrayList幾乎一樣。
Vector是同步的,是線程安全的動態數組,但是效率低。
擴容機制
初識容量為10,加載因子為1,擴容增量是1,擴增後容量為原來長度的2倍,適用於數據量大的環境。
LinkedList類
特點
LinkedList是雙向鏈表結構,額外提供了操作列表首尾元素的方法,因為不是數組結構,所以不存在擴容機制。
LinkedList使用了鏈表結構,通過修改前後兩個元素的鏈接指向實現增加和刪除操作,增刪效率高,但是查找操作必須從開頭或者結尾便利整個列表,所以查找效率低。
LinkedList的操作是非同步的,是線程不安全的。
特殊方法
在開頭位置插入元素:void addFirst(Object object);
在結尾位置插入元素:void addLast(Object object);
刪除開頭位置的元素並返回:Object removeFirst();
刪除結尾位置的元素並返回:Object removeLast();
獲取開頭位置的元素:Object getFirst();
獲取結尾位置的元素:Object getLast();
Set接口
Set接口繼承自Collection接口,允許定義一個不重復的無序集合,集合中只允許存在一個null值。
實現Set接口的實現類主要有:HashSet、LinkedHashSet、TreeSet。
特點
不可以重復,只能插入一個空值。
無序,不能保證插入的順序和輸出的順序一致。
沒有索引。
HashSet類
特點
HashSet的底層是HashMap。
HashSet使用了一個散列集存儲數據,通過元素的Hash值進行排序,不能保證插入和輸出的順序一致。
HashSet不能插入重復的元素,只能存在一個null值。
HashSet內部通過哈希表進行排序,具有很好的查找和存取功能。
HashSet是非同步的,線程不安全。
擴容機制
和HashMap相同。
LinkedHashSet類
特點
LinkedHashSet繼承自HashSet,其底層是基於LinkedHashMap來實現的。
LinkedHashSet使用鏈表維護元素的次序,同時根據元素的hash值來決定元素的存儲位置,遍歷集合時候,會以元素的添加順序訪問集合的元素。
LinkedHashSet不能插入重復的元素,只能存在一個null值。
LinkedHashSet插入性能略低於HashSet,但在叠代訪問Set裏的全部元素時有很好的性能。
LinkedHashSet是非同步的,線程不安全。
擴容機制
和HashMap相同。
TreeSet類
特點
TreeSet的底層是TreeMap。
TreeSet基於二叉樹結構,可以實現自然排序。
TreeSet通過比較方法的返回值來判斷元素是否相等,因此不能添加null的數據,不能添加重復元素,只能插入同一類型的數據。
TreeSet支持兩種排序方式,自然排序和定制排序。
自動排序:添加自定義對象的時候,必須要實現Comparable接口,並要覆蓋compareTo方法來自定義比較規則。
定制排序:創建TreeSet對象時,傳入Comparator接口的實現類。要求Comparator接口的compare方法的返回值和兩個元素的equals方法具有一致的返回值。
Map接口
Map與List、Set接口不同,它是由一系列鍵值對組成的集合,提供了key到value的映射.
同時它也沒有繼承Collection。
特點
一個key對應一個value,不能存在相同的key值,value值可以相同。
key和value之間存在單向一對一關系,即通過指定的key總能找到唯一確定的value。
實現Map接口的實現類主要有:HashMap、LinkedHashMap、TreeMap。
常用方法
插入一個鍵值對並返回value的值:V put(K key, V value);
插入一個鍵值對集合:void putAll(Map<? extends K,? extends V> m);
根據key值移除對應的元素:V remove(Object key);
根據key值獲取對應的元素:V get(Object key);
獲取Entry的Set集合:Set<Map.Entry<K,V>> entrySet();
獲取key的Set集合:Set<K> keySet();
獲取value的Collection集合:Collection<V> values();
遍歷Map
通過Key遍歷
1 Set set = map.keySet(); 2 for(Object obj : set) { 3 System.out.println(obj + " -> " + map.get(obj)); 4 }
通過Entry遍歷
1 Set set = map.entrySet(); 2 for(Object obj : set) { 3 Map.Entry entry = (Map.Entry)obj; 4 System.out.println(entry); 5 System.out.println(entry.getKey() + " -> " + entry.getValue()); 6 }
HashMap類
特點
HashMap底層使用數組結構和鏈表結構。
HashMap根據鍵的Hash值存儲數據,根據鍵可以直接獲取它的值,具有很快的訪問速度,遍歷時,取得數據的順序是完全隨機的。
因為鍵對象不可以重復,所以HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。
HashMap是非同步的,線程不安全。
擴容機制
JDK1.7及以前,默認初始容量16,加載因子是0.75,擴容增量是1。
當同時滿足兩個條件時才會擴容:當前數據存儲的數量大小必須大於等於閾值。當前加入的數據發生了hash沖突。
這就有可能導致存儲超多值得時候仍沒有擴容:一開始存儲的11個值全部hash碰撞,導致存入了同一個鏈表,後面存入的15個值全部沒有hash碰撞,這時存入的個數為26個,但是並不會擴容。
JDK1.8,默認初始容量16,加載因子是0.75,擴容增量是1。
當存入個數超過8時,會將鏈表轉換為紅黑樹。
當存入個數超過容量的0.75倍時,就會進行擴容,並且擴容增量也變成了一倍。
底層實現原理
HashMap的底層是Entry類型的,名字叫table的數組。
Entry數組中的一個元素保存了一組Key-Value映射。
存儲結構中使用了數組結構和鏈表結構。
添加元素時,根據key所在類的hashCode()得到key的hashCode值,並通過hash算法得到在底層數組中的存放位置,如果hashCode相同,那麽位置也是相同的。
如果此位置上沒有其他元素,則添加成功。如果此位置上已經有元素存在,則調用key所在類的equals()方法比較key是否相同。
如果返回true,使用添加的entry的value替換原有位置entry的value,返回原值。
如果返回false,表示發生了hash沖突,新的entry仍能添加成功,與舊的entry之間使用鏈表存儲,新添加的在首部。
Hashtable類
特點
Hashtable是Map的古老實現類,使用哈希表算法。
不能存儲null的鍵和值。
線程安全,但是效率低。
擴容機制
默認初始容量為11。
LinkedHashMap
特點
LinkedHashMap繼承自HashMap,使用哈希表和鏈表實現。
LinkedHashMap使用鏈表維護元素的次序,保留了元素的插入順序,可以按照順序遍歷。
LinkedHashMap允許使用null值和null鍵。
LinkedHashMap需要維護元素的插入順序,因此性能略低於HashMap的性能,但在叠代訪問Map裏的全部元素時將有很好的性能,因為它以鏈表來維護內部順序。
LinkedHashMap是非同步的,線程不安全。
擴容機制
和HashMap相同。
TreeMap
特點
TreeMap是基於紅黑樹實現的,TreeMap存儲時會進行排序,按照添加進Map中的元素的Key的指定屬性進行排序。
TreeMap的排序方式有兩種,自然排序和定制排序。
自然排序:TreeMap中所有的key必須實現Comparable接口,並且所有的key都應該是同一個類的對象,否則會報ClassCastException異常。
定制排序:定義TreeMap時,創建一個comparator對象,該對象對所有的treeMap中所有的key值進行排序,采用定制排序的時候不需要TreeMap中所有的key必須實現Comparable接口。
TreeMap判斷兩個Key相等的標準是通過compareTo()方法或者compare()方法。
TreeMap是非同步的,線程不安全。
HashMap的擴容機制以及默認大小為何是2次冪
hash方法
在Object類中有一個hashCode()方法,用來獲取對象的hashCode值,它被native修飾,意味著這個方法和平臺有關,對於有些JVM,hashCode()返回的就是對象的地址,但大多數情況下是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作為散列值。
對於包含容器類型的程序設計語言來說,基本上都會涉及到hashCode。在Java中也一樣,hashCode的主要作用是為了配合基於散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable。
當向集合中插入對象時,如果調用equals()逐個進行比較,雖然可行但是這樣做的效率很低。因此,先調用hashCode()進行判斷,如果相同再調用equals()判斷,會快很多。
因為在計算元素在HashMap中的下標時,是通過 hash & (length-1) 計算得到的,通過和長度減1進行與運算只會用到低位,所以在使用hashCode之前需要再次進行處理生成新的hash,保證對象的hashCode的32位值只要有一位發生改變,整個hash就會改變,高位的變化也會影響低位,這時再使用低位計算下標就能使元素的分布更加合理。
◆ 在JDK1.7及以前的hash()是進行一系列的移位和按位或運算
1 final int hash(Object k) { 2 int h = hashSeed; 3 if (0 != h && k instanceof String) { 4 return sun.misc.Hashing.stringHash32((String) k); 5 } 6 h ^= k.hashCode(); 7 h ^= (h >>> 20) ^ (h >>> 12); 8 return h ^ (h >>> 7) ^ (h >>> 4); 9 }
◆ 在JDK1.8以後的hash()只需要進行一次異或運算
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
為什麽默認長度為2的次冪
長度為2的次冪,是因為要通過hash計算一個合適的下標,使用的方法是 hash & (length-1) 將hash和長度減1進行按位與運算,如果長度為2的次冪,那麽長度減1得到的二進制表示的每個位上的數字都是1,任何數字同1進行與運算得到的結果都是它本身。
如果長度改為其他不為2的次冪的數字,長度減1得到的二進制表示的某個位上會是0,0同任何數字相與都是0。這個位上為1的位置永遠都不會被放入元素,而且hash在這個位置上不管為1還是0,得到的位置都是一樣的,造成了額外的碰撞。
比如,長度為15時,15減1得到的二進制表示為1110,那麽1001,0101,0011這種末位為1的位置上將永遠都不會有元素,造成位置浪費,而且hash為1101和hash為1100得到的位置都是1100,產生了碰撞,還需要進一步判斷。
只有當所有位置都是1,也就是長度為2的次冪時,才會讓所有位置都有可能被用到,並且每個二進制末4位不同的數字都能有唯一的位置,減少了碰撞的產生。
如果一開始設置的長度不是2的次冪
如果手動設置了長度,那麽HashMap會對傳入的長度進行處理,通過調用tableSizeFor方法,將長度轉為二進制位都為1的並且大於傳入長度的一個數字,然後加1返回就得到了一個2次冪的數字。
何時會進行擴容
◆ 在JDK1.7及以前,判斷是否要擴容的條件是: (size >= threshold) && (null != table[bucketIndex]) 。
第一個條件是長度的閾值,閾值用threshold表示,一般是長度和加載因子的乘積,加載因子默認是0.75。
第二個條件是當前位置上不能為空,也就是說發生了hash碰撞。
只有同時滿足了這兩個條件,才會進行擴容,這就有可能導致在當前長度超出閾值的情況下仍不會進行擴容操作。
◆ 在JDK1.8以後,判斷條件是: ++size > threshold 。
只要在插入之後的長度超過了當前的閾值,就會進行擴容操作。
擴容機制resize方法
在JDK1.7及以前,HashMap使用的是數組加鏈表的方式存儲的。在進行擴容後,原來的元素都要重新計算hash,通過重新計算索引位置後,如果在新表的索引位置相同,則鏈表元素會倒置 。
1 while(null != e) { 2 Entry<K,V> next = e.next; 3 if (rehash) { 4 e.hash = null == e.key ? 0 : hash(e.key); 5 } 6 int i = indexFor(e.hash, newCapacity); 7 e.next = newTable[i]; 8 newTable[i] = e; 9 e = next; 10 }
在JDK1.8以後,使用的是數組加紅黑樹的存儲方式。在進行擴容後,不再進行重新計算hash,而是通過將原hash同原長度進行按位與運算,判斷hash的高位是否為0,如果為0則放回原位置,如果不為0則放在高位。
1 Node<K,V> e; 2 if ((e = oldTab[j]) != null) { 3 oldTab[j] = null; 4 if (e.next == null) 5 newTab[e.hash & (newCap - 1)] = e; 6 else if (e instanceof TreeNode) 7 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 8 else { // preserve order 9 Node<K,V> loHead = null, loTail = null; 10 Node<K,V> hiHead = null, hiTail = null; 11 Node<K,V> next; 12 do { 13 next = e.next; 14 if ((e.hash & oldCap) == 0) { 15 if (loTail == null) 16 loHead = e; 17 else 18 loTail.next = e; 19 loTail = e; 20 } 21 else { 22 if (hiTail == null) 23 hiHead = e; 24 else 25 hiTail.next = e; 26 hiTail = e; 27 } 28 } while ((e = next) != null); 29 if (loTail != null) { 30 loTail.next = null; 31 newTab[j] = loHead; 32 } 33 if (hiTail != null) { 34 hiTail.next = null; 35 newTab[j + oldCap] = hiHead; 36 } 37 } 38 }
重寫equals方法和hashCode方法
在發生hash碰撞的時候,一個桶裏的兩個元素key值不相等,但是他們的hashCode是相等的,如果兩個key值也相等,則說明兩個key相等。也就是說:
◆ 如果兩個對象equals相等,那麽這兩個對象的HashCode一定也相同。
◆ 如果兩個對象的HashCode相同,不代表兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放於同一個位置。
一般在重寫equals方法的時候,也會盡量重寫hashCode方法,就是為了在equals方法判斷相等的時候也保證讓hashCode方法也判斷相等。
集合相關知識