Java中的集合
- 集合分為:List,Set,Map三種,其中List與Set是繼承自Collection,而Map不是。
一、List與Set的區別:
List中的元素有存放順序,並且可以存放重復元素,檢索效率高,插入刪除效率低;
Set沒有存放順序,而且不可以存放重復元素,后來的元素會把前面重復的元素替換掉,檢索效率低,插入刪除效率高。(Set存儲位置是由它的HashCode碼決定的,所以它存儲的對象必須有equals()方法,而且Set遍歷只能用迭代,因為它沒有下標。)(參考1)
二、ArrayList與LinkList的區別:
ArrayList基層是以數組實現的,可以存儲任何類型的數據,但數據容量有限制,超出限制時會擴增50%容量,把原來的數組復制到另一個內存空間更大的數組中,查找元素效率高。ArrayList是一個簡單的數據結構,因超出容量會自動擴容,可認為它是常說的動態數組。
LinkedList以雙向鏈表實現,鏈表無容量限制,但雙向鏈表本身使用了更多空間,每插入一個元素都要構造一個額外的Node對象,也需要額外的鏈表指針操作。允許元素為null,線程不安全。(參考2)
ArrayList和LinkedList都實現了List接口,LinkedList還額外實現了Deque接口。
綜合比較結論:(參考3)
LinkList在增、刪數據效率方面要優於ArrayList,而ArrayList在改和查方面效率要遠超LinkList。
ArrayList查詢直接根據下標查詢,而LinkedList需要遍歷才能查到元素。增加時直接增加放在隊尾,指定位置增加時,數組的元素會往后移動,而鏈表則需要進行先遍歷到指定的位置。
但是,雖然綜合比較之下LinkedList的優勢要比ArrayList要好,但是在java中,我們用的比較多的確實ArrayList,因為我們的業務通常是對數據的改和查用的比較多。線程是非安全的。
綜合比較結論:(參考3)
LinkList在增、刪數據效率方面要優於ArrayList,而ArrayList在改和查方面效率要遠超LinkList。
ArrayList查詢直接根據下標查詢,而LinkList需要遍歷才能查到元素。增加時直接增加放在隊尾,指定位置增加時,數組的元素會往后移動,而鏈表則需要進行先遍歷到指定的位置。
但是,雖然綜合比較之下LinkList的優勢要比ArrayList要好,但是在java中,我們用的比較多的確實ArrayList,因為我們的業務通常是對數據的改和查用的比較多。線程是非安全的。
三、Map之間的區別
HashMap
數組
數組存儲區間連續,占用內存比較嚴重,空間復雜度很大。但數組的二分查找時間復雜度小,為O(1);
數組的特點是:尋址容易,插入和刪除困難;
鏈表
鏈表存儲區間離散,占用內存比較寬松,空間復雜度很小,但時間復雜度很大,達O(N)。
鏈表的特點是:尋址困難,插入和刪除容易。
HashMap
(1.7 數組+鏈表;1.8 數組+鏈表+紅黑樹)是用得最多的一種鍵值對存儲的集合, 用一個數組來存儲元素,但是這個數組存儲的不是基本數據類型。HashMap實現巧妙的地方就在這里,數組存儲的元素是一個Entry類,這個類有三個數據域,key、value(鍵值對),next(指向下一個Entry) 。
特點:HashMap允許空鍵值,並且它是非線程安全的,所以插入、刪除和定位元素會比較快。
說一下HashMap的put方法
大體流程:
-
根據key通過哈希算法拿到一個HashCode結合與操作,與數組的長度-1進行運算,得到一個數組下標
-
如果得到的數組下標位置元素為空,則將key和value封裝成Entry對象(1.7為Entry對象,1.8為Node對象)並放入該位置。
-
如果下標元素不為空,需要分情況討論
a. 1.7 首先需要判斷需不需要擴容,需要的話先擴容,如果不需要擴容則將Key和Value封裝成Entry對象,采用頭插法插入當前位置鏈表中。
b. 1.8 需要先判斷是紅黑樹Node還是鏈表Node
- 如果是紅黑樹則需要將Key和Value封裝成紅黑樹節點添加到紅黑樹中,在添加的過程中會判斷是否包含節點,如果包含則更新
- 如果是鏈表則先將Key和Value封裝成Node節點,采用尾插法進行插入,如果插入的過程中包含此節點則更新,如果沒有則插入到最后。插入完之后如果節點個數大於等於8則轉為紅黑樹存儲。
- 插入完成之后則判斷是否需要擴容,需要擴容則擴容,不需要則結束PUT方法。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); //將Key進行hash
}
/**
* 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) //得到的hashcode與數組長度-1進行與運算 得到一個數組下標
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))))
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;
}
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;
}
Hashcode有什么作用
HashMap 的添加、獲取時需要通過 key 的 hashCode() 進行 hash(),然后計算下標 ( n-1 & hash),從而獲得要找的同的位置。當發生沖突(碰撞)時,利用 key.equals() 方法去鏈表或樹中去查找對應的節點。
Hash
Hash是散列的意思,就是把任意長度的輸入,通過散列算法變換成固定長度的輸出,該輸出就是散列值。關於散列值,有以下幾個關鍵結論:
- 如果散列表中存在和散列原始輸入K相等的記錄,那么K必定在f(K)的存儲位置上
- 不同關鍵字經過散列算法變換后可能得到同一個散列地址,這種現象稱為碰撞
- 如果兩個Hash值不同(前提是同一Hash算法),那么這兩個Hash值對應的原始輸入必定不同
HashCode
- HashCode的存在主要是為了查找的快捷性,HashCode是用來在散列存儲結構中確定對象的存儲地址的
- 如果兩個對象equals相等,那么這兩個對象的HashCode一定也相同
- 如果對象的equals方法被重寫,那么對象的HashCode方法也盡量重寫
- 如果兩個對象的HashCode相同,不代表兩個對象就相同,只能說明這兩個對象在散列存儲結構中,存放於同一個位置
總結
-
hashCode() 在散列表中才有用,在其它情況下沒用
-
哈希值沖突了場景,hashCode相等,但equals不等
-
hashcode:計算鍵的hashcode作為存儲鍵信息的數組下標用於查找鍵對象的存儲位置
-
equals:HashMap使用equals()判斷當前的鍵是否與表中存在的鍵相同。
-
如果兩對象equals()是true,那么它們的hashCode()值一定相等,
-
如果兩對象的hashCode()值相等,它們的equals不一定相等(hash沖突啦)
-
TreeMap
是基於紅黑樹實現的,適用於按自然順序火茲定於順序遍歷key。
HashTable
是基於HashCode實現的(數組+鏈表),但它是線程安全的,無論key還是value都不能為null,所以會比HashMap效率低,而且不允許null值。
ConcurrentHashMap
(分段數組+鏈表),線程安全。
四、Set中最常用的集合:HashSet
HashSet是使用Hash表實現的,集合里面的元素是無序得,可以有null值,但是不能有重復元素。(參考1)
特點:因為相同的元素具有相同的hashCode,所以不能有重復元素
五、線程安全與不安全的集合
線程安全就是多線程訪問時,采用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問直到該線程讀取完,其他線程才可使用。不會出現數據不一致或者數據污染。線程不安全就是不提供數據訪問保護,有可能出現多個線程先后更改數據造成所得到的數據是臟數據。List接口下面有兩個實現,一個是ArrayList,另外一個是vector。 從源碼的角度來看,因為Vector的方法前加了synchronized 關鍵字,也就是同步的意思,sun公司希望Vector是線程安全的,而希望arraylist是高效的,缺點就是另外的優點。
ArrayList為什么線程不安全
一個 ArrayList ,在添加一個元素的時候,它可能會有兩步來完成: 在 Items[Size] 的位置存放此元素; 增大 Size 的值。
在單線程運行的情況下,如果 Size = 0,添加一個元素后,此元素在位置 0,而且 Size=1;
而如果是在多線程情況下,比如有兩個線程,線程 A 先將元素存放在位置 0。但是此時 CPU 調度線程A暫停,線程 B 得到運行的機會。線程B也向此 ArrayList 添加元素,因為此時 Size 仍然等於 0 (注意哦,我們假設的是添加一個元素是要兩個步驟哦,而線程A僅僅完成了步驟1),所以線程B也將元素存放在位置0。然后線程A和線程B都繼續運行,都增加 Size 的值。 那好,現在我們來看看 ArrayList 的情況,元素實際上只有一個,存放在位置 0,而 Size 卻等於 2。這就是“線程不安全”了。
線程安全的工作原理
jvm中有一個main memory對象,每一個線程也有自己的working memory,一個線程對於一個變量variable進行操作的時候, 都需要在自己的working memory里創建一個copy,操作完之后再寫入main memory。 當多個線程操作同一個變量variable,就可能出現不可預知的結果。
而用synchronized的關鍵是建立一個監控monitor,這個monitor可以是要修改的變量,也可以是其他自己認為合適的對象(方法),然后通過給這個monitor加鎖來實現線程安全,每個線程在獲得這個鎖之后,要執行完加載load到working memory 到 use && 指派assign 到 存儲store 再到 main memory的過程。才會釋放它得到的鎖。這樣就實現了所謂的線程安全。
安全與不安全的類
Java中提供了很多的集合類,比如ArrayList、LinkedList、HashMap...等,集合類內部采用了諸多的方式進行存儲這些數據,目的是能夠讓增刪改查某些操作更快一些,不同的存儲方式被稱作數據結構。因為這些不同的存儲方式,導致了增刪改查效率的不同。
數據結構:容器存儲數據,管理數據的一種方式,數據結構常用的包括數組、鏈表、哈希表、樹。
- ArrayList主要是用數組來存儲元素,LinkedList主要是用鏈表來存儲元素,HashMap的底層實現主要是借助數組+鏈表+紅黑樹來實現。
- Vector、HashTable、Properties等集合類效率比較低但都是線程安全的。包java.util.concurrent下包含了大量線程安全的集合類,效率上有較大提升。
- CopyOnWriteArrayList 線程安全,適用於讀轉寫操作,每次操作時會復制一個新的List,在新的List上進行寫操作,操作完成之后,再將原來的List指向新的List。線程安全地遍歷,因為如果另外一個線程在遍歷的時候修改List的話,實際上會拷貝出一個新的List上修改,而不影響當前正在被遍歷的List。
- ConcurrentLinkedQueue 線程安全,是一個基於鏈接節點的、無界的、線程安全的隊列。此隊列按照 FIFO(先進先出)原則對元素進行排序,隊列的頭部 是隊列中時間最長的元素。隊列的尾部 是隊列中時間最短的元素。新的元素插入到隊列的尾部,隊列檢索操作從隊列頭部獲得元素。當許多線程共享訪問一個公共 collection 時,ConcurrentLinkedQueue 是一個恰當的選擇,此隊列不允許 null 元素。
- ConcurrentHashMap 線程安全 分段數組+鏈表,采用了分段鎖的設計, 將一個HashMap分成N段,使用key的hashCode來確定分配到那個字段,只有在同一分段內才存在競態關系,每個分段相當於一個HashTable,執行效率相當於提升了N倍。
- ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是線程不安全的。(線程不安全是指:當多個線程訪問同一個集合或Map時,如果有超過一個線程修改了ArrayList集合,則程序必須手動保證該集合的同步性。)
六、集合的遍歷
-
for循環遍歷,基於計數器
遍歷者自己在集合外部維護一個計數器,然后依次讀取每一個位置的元素,當讀取到最后一個元素后,停止。主要就是需要按元素的位置來讀取元素。
因為是基於元素的位置,按位置讀取。所以我們可以知道,對於順序存儲,因為讀取特定位置元素的平均時間復雜度是O(1),所以遍歷整個集合的平均時間復雜度為O(n)。而對於鏈式存儲,因為讀取特定位置元素的平均時間復雜度是O(n),所以遍歷整個集合的平均時間復雜度為O(n2)(n的平方)。
for (int i = 0; i < list.size(); i++) { list.get(i); }
-
迭代器遍歷
Iterator本來是OO的一個設計模式,主要目的就是屏蔽不同數據集合的特點,統一遍歷集合的接口。Java作為一個OO語言,自然也在Collections中支持了Iterator模式。
那么對於RandomAccess類型的集合來講,沒有太多意義,反而由於一些額外的操做,還會增長額外的運行時間。可是對於Sequential Access的集合來講,就有很重大的意義了,由於Iterator內部維護了當前遍歷的位置,因此每次遍歷,讀取下一個位置並不須要從集合的第一個元素開始查找,只要把指針向后移一位就好了,這樣一來,遍歷整個集合的時間復雜度就下降為O(n);
Iterator iterator = list.iterator(); while (iterator.hasNext()) { iterator.next(); }
-
foreach循環遍歷
屏蔽了顯式聲明的Iterator和計數器。分析Java字節碼可知,foreach內部實現原理,也是經過Iterator實現的,只不過這個Iterator是Java編譯器幫咱們生成的,因此咱們不須要再手動去編寫。可是由於每次都要作類型轉換檢查,因此花費的時間比Iterator略長。時間復雜度和Iterator同樣。
優點:代碼簡潔,不易出錯。
缺點:只能做簡單的遍歷,不能在遍歷過程中操作(刪除、替換)數據集合。
for (ElementType element : list) { }
-
遍歷map (也可以使用迭代)
Map<Integer, Integer> map = new HashMap<Integer, Integer>(); //遍歷map中的鍵 for (Integer key : map.keySet()) { System.out.println("Key = " + key); } //遍歷map中的值 for (Integer value : map.values()) { System.out.println("Value = " + value); }
七、針對ArrayList的操作
- List 包含的方法
-
add(Object element): 向列表的尾部添加指定的元素。
-
size(): 返回列表中的元素個數。
-
get(int index): 返回列表中指定位置的元素,index從0開始。
-
add(int index, Object element): 在列表的指定位置插入指定元素。
-
set(int i, Object element): 將索引i位置元素替換為元素element並返回被替換的元素。
-
clear(): 從列表中移除所有元素。
-
isEmpty():判斷列表是否包含元素,不包含元素則返回 true,否則返回false。
-
contains(Object o): 如果列表包含指定的元素,則返回 true。
-
remove(int index): 移除列表中指定位置的元素,並返回被刪元素。
-
remove(Object o): 移除集合中第一次出現的指定元素,移除成功返回true,否則返回false。
-
iterator(): 返回按適當順序在列表的元素上進行迭代的迭代器。
-
排序
Collections.sort(list); //針對一個ArrayList內部的數據排序 如果想自定義排序方式則需要有類來實現Comparator接口並重寫compare方法 調用sort方法時將ArrayList對象與實現Commparator接口的類的對象作為參數 -- 調用 Collections.sort(list, new SortByAge()); -- 實現接口 class SortByAge implements Comparator { public int compare(Object o1, Object o2) { Student s1 = (Student) o1; Student s2 = (Student) o2; return s1.getAge().compareTo(s2.getAge()); } } 注:compareTo方法比較字符串,但是也可以比較數字(數字為String類型) 原理:先比較字符串長度,再逐一轉成char類型去比較字符串里的每一個字符。
-
遍歷
-- for循環的遍歷方式 for (int i = 0; i < lists.size(); i++) { System.out.print(lists.get(i)); } -- foreach的遍歷方式 for (Integer list : lists) { System.out.print(list); } -- Iterator的遍歷方式 for (Iterator list = lists.iterator(); list.hasNext();) { System.out.print(list.next()); }
-
刪除
lists.remove(6); //指定刪除 List 刪除元素的邏輯是將目標元素之后的元素往前移一個索引位置, 最后一個元素置為 null,同時 size - 1。 遍歷刪除時,操作不當會導致異常, 原因:ArrayList 中兩個 remove() 方法都對 modCount 進行了自增, 那么我們在用迭代器迭代的時候,若是刪除末尾 的元素, 則會造成 modCount 和 expectedModCount 的不一致導致異常拋出。 Iterator 迭代遍歷刪除是最安全的方法。(參考4) -- 示例: public static void remove(List list, String target){ Iterator iter = list.iterator(); while (iter.hasNext()) { String item = iter.next(); if (item.equals(target)) { iter.remove(); } } System.out.println(list); }
-
去重(參考6)
-
利用HashSet(不保證元素順序一致)
-
利用LinkedHashSet (去重后順序一致),繼承的父類HashSet
參考資料
- Java中的幾種集合的區別及適用場景
- Java集合之ArrayList與LinkList
- Java中ArrayList與LinkList比較
- ArrayList中元素的刪除操作
- ArrayList常用方法總結
- ArrayList去重
- 講講HashCode的作用
- HashMap原理詳解
- JAVA中常用的高級集合類總結(包含Concurrent包下的並發集合類)
- Java線程(十四):Concurrent包中強大的並發集合類
- 線程安全,為什么說ArrayList,LinkedList是線程不安全的,以及CopyOnWriteArrayList和vector為什么安全
- 常用集合類簡介及線程安全和非線程安全的集合對象
- Java遍歷集合的幾種方法分析(實現原理、算法性能、適用場合)
- Map集合的五種遍歷方式及Treemap方法