1. 程式人生 > >集合相關知識

集合相關知識

cast hashset類 指向 lec 很好 當前位置 集合類型 shm 二進制表示

摘要:本文主要介紹了幾種集合類型以及有關的一些知識點。

集合類圖

類圖

技術分享圖片

類圖說明

所有集合類都位於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方法也判斷相等。

集合相關知識