前言
Java集合中的List、Set和Map作爲Java集合食物鏈的頂級,可謂是各有千秋。本文將對於List、Set和Map之間的聯繫與區別進行介紹,以及這三者衍生出來的問題進行介紹(若無特地說明,jdk版本皆爲1.8):
- List、Set和Map的聯繫和區別是什麼?
- List、Set和Map的使用場景有哪些?
- List與Set之間的怎麼轉換?
- Set是怎麼保證元素不重複?
- 如何在遍歷的同時刪除ArrayList中的元素?
1. List、Set和Map的聯繫和區別
List和Set是Collection的實現類,而Map與Collection是屬於“同級“。
List
List的特性:
- List允許插入重複元素;
- List允許插入多個null元素;
- List作爲有序集合,保證了元素按照插入的順序進行排列;
- List提供ListIterator迭代器,可以提供雙向訪問的功能;
- List常用的實現類有:可隨意訪問元素的ArrayList、應用於增刪頻繁的LinkedList、利用synchronized關鍵字實現線程安全的Vector等。
Set
Set的特性:
- Set不包含重複元素;
- Set只允許一個null元素的存在;
- Set接口較爲流行的實現類有:基於HashMap實現的HashSet、實現SortedSet接口且能更具compare()和compareTo()的定義進行排序的TreeSet等。
Map
Map的特性:
- 存儲結構是鍵值對,一個鍵對應一個值;
- 不允許包含重複的鍵,Map可能會持有相同的值對象,但鍵對象必須是唯一的;
- 在Map中可以有多個null值,但最多只能有一個null鍵;
- Map不是Collection的子接口或實現類**,Map是跟Collection“同級”的接口**;
- Map中比較流行的實現類是採用散列函數的HashMap、以及利用紅黑樹實現排序的TreeMap等。
2. List、Set和Map的使用場景
上文我們介紹完了List、Set和Map之間的聯繫和區別,接下來我們來看下這三者在使用場景上的差異。
List
如果經常使用索引來訪問元素,或者是需要能夠按照插入順序進行存儲,List會是不錯的選擇。
- 需要使用索引來訪問容器的元素,ArrayList可以提供更快速的訪問(底層是數組實現);
- 需要經常增刪元素,LinkedList則會是最佳的選擇(底層是鏈表實現);
- 數據量不大,並且有線程安全(synchronized關鍵字)的要求,可以選擇Vector;
- 有線程安全(ReentrantLock實現)和性能的要求,讀多寫少的情況,CopyOnWriteArrayList會是更好的選擇。
Set
想要保證插入元素的唯一性,可以選擇Set的實現類。
- 需要快速查詢元素,可以使用HashSet(採用散列函數);
- 如果有排序元素的需要,可以使用TreeSet(採用紅黑樹的樹結構排序元素);
- 急需要加快查詢速度,還需要按插入順序來存儲數據,LinkedHashSet是最好的選擇(採用散列函數的同時,還使用鏈表維護元素的次序)。
Map
如果需要按鍵值對<key, value>的形式進行數據存儲,那麼Map是正確的選擇。
- 需要快速查詢鍵值元素,可以使用HashMap(採用散列函數);
- 如果需要將鍵進行排序,可以使用TreeMap(按照紅黑樹對鍵進行排序);
- 在存儲數據少,不允許有null值,又有線程安全(synchronized關鍵字)的要求,可以選擇Hashtable(父類是Dictionary);
- 如果需要線程安全(Node+CAS+Synchronized),且有數據量和性能要求,ConcurrentHashMap是最佳的選擇。
3. List與Set之間的轉換
因爲List和Set都實現了Collection接口中的addAll(Collection<? extends E> c)方法,而且List和Set也提供了Collection<? extends E> c爲參數的構造函數,所以可以採用構造函數的形式,完成List和Set的互相轉換。
addAll(Collection<? extends E> c)方法
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
以Set接口的實現類HashSet爲例,其提供了Collection<? extends E> c爲參數的構造函數。
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
以List接口的實現類ArrayList爲例,也可以看到它提供了Collection<? extends E> c爲參數的構造函數。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
.......
}
所以我們可以得到Set與List之間的轉換方式:
Set<Integer> set = new HashSet<>(list); //List轉Set
List<Integer> list = new ArrayList<>(set); //Set轉List
4. Set是怎麼保證元素不重複?
我們以Set接口最流行的實現類HashSet爲例,對Set保證元素不重複的原因進行介紹。
private transient HashMap<E,Object> map;
public boolean add(E e) {
//如果return true,則表示不包含此元素
return map.put(e, PRESENT)==null;
}
從上可知,HashSet是依賴HashMap得以實現,其中添加的元素作爲HashMap的鍵來存儲。所以接下來就是在介紹“HashMap是怎麼保證不允許有相同的鍵存在”了。
public V put(K key, V value) {
//倒數第二個參數爲false,表示允許舊值替換
//最後一個參數爲true,表示HashMap不處於創建模式
return putVal(hash(key), key, value, false, true);
}
在這裏,我們可以看到在進行putVal()方法之前,會將key代入hash()方法中進行散列。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果哈希表爲空,調用resize()方法創建一個哈希表,並用n記錄哈希表的長度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果指定參數hash(key的hashCode()值)在表中沒有對應的桶,即沒有碰撞
//(n-1)&hash計算key將被放置的槽位
//(n-1)&hash本質上是hash%n,只是位運算更快
if ((p = tab[i = (n - 1) & hash]) == null)
//如果沒有碰撞,直接將鍵值對插入到map中即可
tab[i] = newNode(hash, key, value, null);
else { //如果桶中已經存在了元素
Node<K,V> e; K k;
//比較桶中的第一個元素(數組中的結點)的hash值、key是否相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果相等,則將第一個元素p用e來記錄
e = p;
else if (p instanceof TreeNode) //當前桶中無該鍵值對,且桶的結構爲紅黑樹,則按照紅黑樹結構的規則插入元素
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { //如果桶中無該鍵值對,且桶的結構爲鏈表,則按照鏈表結構將元素插入到尾部
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
treeifyBin(tab, hash);
break;
}
//鏈表節點中的元素<key, value>與put操作控制的元素<key, value>相同時,不做重複操作,直接跳出程序
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果put操作控制的元素的key和hashCode,與已經插入的元素相等時,執行以下操作
if (e != null) { // existing mapping for key
// oldValue記錄e的value
V oldValue = e.value;
// onlyIfAbsent爲false,或舊值爲null時,允許替換舊值,否則無需替換
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//訪問後回調
afterNodeAccess(e);
//返回舊值
return oldValue;
}
}
// 更新結構化修改信息
++modCount;
// 鍵值對數目如果超過閾值時,執行resize()方法
if (++size > threshold)
resize();
// 插入後回調
afterNodeInsertion(evict);
return null;
}
從以上源碼中我們可以看出,將一個鍵值對<key, value>放入HashMap時,首先會根據key的hashCode()返回值和HashMap的長度決定該元素的存儲位置,如果兩個key的hash值相同,那麼它們的存儲位置相同。如果這兩個key的equals比較返回true,那麼新添加的元素newValue就會覆蓋原來的元素oldValue,key不會被覆蓋。
當HashSet中的add()方法裏,map.put(e, PRESENT) == null爲false時,HashSet添加元素失敗。所以如果向HashSet中添加一個已經存在的元素,新添加的元素不會覆蓋原來已有的元素。
5. 如何在遍歷的同時刪除ArrayList中的元素?
平時我們可能會覺得遍歷ArrayList並刪除其中元素是一件很簡單的事情,但其實這個操作很容易出bug,接下來我們一起看下怎麼樣繞過這些坑。
從後向前遍歷元素
我們先從前向後遍歷的同時,進行刪除元素:
public static void main(String[] args){
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(3);
list.add(4);
for(int i=0; i<list.size()-1; i++){
if(list.get(i) == 3){
list.remove(new Integer(3));
}
}
System.out.println(list);
}
運行結果爲:
[1, 2, 3, 4]
造成這個現象的原因,在【Java集合】ArrayList的使用及原理中筆者稍有提及。在於ArrayList執行remove()操作時,將既定元素刪除時還把該元素後的所有元素向前移動一位。這就導致了在遍歷[1,2,3,3,4]時,刪除前一個元素“3”後,將其後元素向前移動一位,因下標[2]已經被遍歷過了,所以就遺漏了第二個“3”。
對於這個問題,我們只需要換個遍歷的角度即可——從後往前遍歷:
for(int i=list.size()-1; i>=0; i--){
if(list.get(i) == 3){
list.remove(new Integer(3));
}
}
運行結果爲:
[1, 2, 4]
從後往前遍歷,在刪除某一元素之後,也不用擔心在遍歷過程中會遺漏元素。
Iterator.remove()
除了上述遍歷方法,還有一種遍歷方式是我們經常使用的——for-each遍歷:
for(Integer i : list){
if(i == 3){
list.remove(new Integer(3));
}
}
運行結果:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
...
我們知道,for-each的遍歷方式其實是Iterator、hashNext()、next()的複合簡化版。當點開ArrayList.checkForComodification()方法可以看到:
private class Itr implements Iterator<E> {
......
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
這裏的modCount是ArrayList的,而expectedModCount是Itr的,所以其實出錯的地方在於,運行ArrayList.remove()方法時改變了modCount,這就打破了原本modCount == expectedModCount之間和平友好的關係,導致報出併發修改異常。
所以在使用迭代器迭代時(顯示或for-each的隱式)不要使用ArrayList.remove(),改爲使用Iterator.remove()即可:
Iterator<Integer> i = list.iterator();
while(i.hasNext()){
Integer integer = i.next();
if(integer == 3){
i.remove();
}
}
結語
本來昨天就已經寫好了,然而電腦一卡,啥都沒了,只能重寫…
如果本文對你有幫助,請給一個贊吧,這會是我最大的動力~
參考資料: