Java基礎(3)|Collection
Java基礎(3)|Collection
目錄1、Collection介面繼承樹
2、基本操作
- add(Object o):增加元素
- addAll(Collection c):...
- clear():...
- contains(Object o):是否包含指定元素
- containsAll(Collection c):是否包含集合c中的所有元素
- iterator():返回Iterator物件,用於遍歷集合中的元素
- remove(Object o):移除元素
- removeAll(Collection c):相當於減集合c
- retainAll(Collection c):相當於求與c的交集
- size():返回元素個數
- toArray():把集合轉換為一個數組
3、Collection的遍歷
Collection的遍歷可以使用Iterator介面或者是foreach迴圈來實現
4、Set
Set集合不允許包含相同的元素,而判斷兩個物件是否相同則是根據物件的hashcode和equals方法。
4.1、HashSet
帶著問題去學習
1.HashSet底層是什麼資料結構?
2.HashSet允許有空值麼?
3.HashSet允許有重複值麼?
4.如果new兩個值一樣的字串,往HashSet集合中新增,是否能新增進去?
5.HashSet是如何保證元素的唯一性的?
6.HashSetadd方法其實是呼叫的那個方法?
7.HashSet是否是執行緒安全的呢?
上面的幾乎都沒有什麼難度,使用過集合的大多數人都瞭解。
那我們來看看哪些硬核的難點吧!
我們都知道HashSet底層其實上是new了一個HashMap集合,那我們就來看看,HashSet呼叫add方法的時候的一些問題。
1.HashMap的value部分值是否相同?
2.HashMap的初始化容量是多大?是在什麼時候進行初化容量?
3.在計算HashMap的key的HashCode值的時候是單純的時候hashCode方法計算出來的麼?
4.HashMap什麼時候進行擴容?
5.HashMap陣列轉紅黑樹需要滿足那些條件?
7.HashSet在新增重複元素的時候,具體是怎麼進行判斷該元素已經存在的?
8.使用HashSet集合的時候,需要重寫HashCode和equlas方法麼?
那我們來看看哪些硬核的難點吧!
1.HashMap的value部分值是否相同?
2.HashMap的初始化容量是多大?是在什麼時候進行初化容量?
3.在計算HashMap的key的HashCode值的時候是單純的時候hashCode方法計算出來的麼?
4.HashMap什麼時候進行擴容?
5.HashMap陣列轉紅黑樹需要滿足那些條件?
7.HashSet在新增重複元素的時候,具體是怎麼進行判斷該元素已經存在的?
8.使用HashSet集合的時候,需要重寫HashCode和equlas方法麼?
1、HashSet底層是什麼資料結構?
HashSet底層採用的是陣列+連結串列+紅黑樹,在new HashSet的時候實際底層是new了一個HashMap,把HashMap的key部分,作為HashSet的Value部分。
2、HashSet允許有空值麼?
準確的來說是允許的(也就是程式碼不會出現異常),但是只能有一個空值,如果有第二個空值,那麼第二個空值將加不進HashSet集合。
3.HashSet允許有重複值麼?
肯定是不允許的,因為HashSet的value部分是HashMap的key部分,因為HashMap的key本身就是無序不可重複的,所以HashSet也就不可能重複。
4.如果new兩個值一樣的字串,往HashSet集合中新增,是否能新增進去?
是不可以加入進去的,因為在進行新增元素的時候會進行判斷,通過hashCode方法和equals方法進行比對,String這個類,重寫了這兩個方法,比較的是字串的值,而不是使用繼承自Object的equlash和hashCode方法去進行比較。
5.HashSet是如何保證元素的唯一性的?
依賴於hashCode()和equals()這兩個方法,所有在我們比較兩個我們自定義的物件的時候,需要我們重寫這兩個方法,自定義比較規則,否則就是使用繼承自Object的進行比對,比對的是物件的記憶體地址。
6.HashSet的add方法其實是呼叫的哪個方法?
其實呼叫的是HashMap的map.put方法。
7.HashSet是否是執行緒安全的呢?
HashSet是執行緒不安全的,所以呢?他的執行效率比較高,因為HashSet和HashMap的原始碼中的方法都有沒有加synchronized關鍵字。
那我們來看看哪些硬核的難點吧!
1.HashMap的value部分值是否相同?
都是相同的,因為value部分是使用了一個靜態的Object物件進行佔位,這個物件只是用於佔位操作,並沒有多大的實際意義。
private static final Object PRESENT = new Object();
2.HashMap的初始化容量是多大?是在什麼時候進行初化容量?
初始化容量是16,是在第一次呼叫resize()方法的時候進行擴容的,並不是new HashMap方法的時候就進行擴容。
3.在計算HashMap的key的HashCode值的時候是單純的時候hashCode方法計算出來的麼?
不是,而是通過一個表示式進行計算後的結果(
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
),並不是單純的hashCode值。
4.HashMap什麼時候進行擴容?
底層陣列超過臨界值12的時候就會進行擴容,那麼為什麼不是到16才進行擴容呢?試下一下,他是一個執行緒不安全的集合,萬一此時突突來了很多物件,要加入到這個集合,那麼這個集合不就炸了麼?擴容的機制就是:
當前陣列容量乘以2再乘以載入因子0.75
每次新增元素的時候都會++Size,並不是說,這個陣列中滿了12個單向連結串列的時候進行擴容。
5.HashMap陣列轉紅黑樹需要滿足那些條件?
首先判斷該連結串列是否已經到達8個節點,如果滿足該條件,再次進行判斷這個陣列連結串列的值是否大於64,如果小於64,還不會轉化為紅黑樹,而是進行陣列的擴容,大於64再轉紅黑樹。
7.HashSet在新增重複元素的時候,具體是怎麼進行判斷該元素已經存在的?
進行equlas方法和HashCode方法進行比對,如果比對不出來再進行判斷該連結串列是不是一顆紅黑樹,是的話進行紅黑樹的方式進行判斷,如果不是,那麼就遍歷該連結串列,依次進行比對,如果比對到匹配的值,那麼新增失敗,如果沒有比對到相等的值,那就把該元素新增到該連結串列的末尾。
8.使用HashSet集合的時候,為什麼要重寫HashCode和equlas方法?
因為底層新增元素的時候會呼叫這兩個方法進行比對,而這個兩個方法就是需要我們自定義比對規則,不然預設繼承Object的。
原始碼分析,證明答案
new HashSet的原始碼:
//執行構造器
public HashSet() {
map = new HashMap<>();
}
1.第一次呼叫add方法的原始碼分析:
// 第一次add方法的執行過程:
// 2.add方法 :呼叫map的put方法 PRESENT:靜態的一個Object物件 用於佔位,每一個map的value都是用一個物件
* public boolean add(E e) {
* return map.put(e, PRESENT)==null; //如果return null的時候就代表執行成功了
* }
* // 呼叫hash方法獲取到key的hash值
* 3. public V put(K key, V value) {
* return putVal(hash(key), key, value, false, true);
* }
* // 通過hash演算法獲取的key的hash值 此hash值並不等於key原本的hash值
* static final int hash(Object key) {
* int h;
* return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
* }
*
* 4.得出hash值後 然後去putValue方法判斷是否應該把這個值新增進去
* 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成立,呼叫resize()
* if ((tab = table) == null || (n = tab.length) == 0) //table 其實就是HashMap裡面的那個Node陣列[] 存放連結串列的那個陣列
* n = (tab = resize()).length; //resize())執行完後,返回一個初始化容量為16的table[]陣列
*
* // 通過key的hash值計算出元素應該存放到table陣列的那個索引位置
* //並把這個位置的物件賦值給臨時變數p,判斷p是否為null
* //如果p為空,代表這個位置還沒有存放過元素,就建立一個node物件,key和value都放進去,next為null,留給第後來新增的元素存放Node物件
* 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))))
* 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) //判斷當前這個table陣列是否超過了12這個最大容量值,如果超過進行擴容
* resize();
* // 這個方法其實是一個空方法,是留給子類去實現的
* afterNodeInsertion(evict);
* return null; //程式走到這兒,就代表我們第一次新增的元素已經成功了
* }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//只有第一次add的時候才會執行這個 if
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 此時這個 方法為false 因為這次新增的元素是我們上次已經新增過的元素,所以算出來的下標1肯定是和上一次算出的下標一致
// 判斷這個陣列的下標位置中是否已經有連結串列元素存在
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//新增重複值的時候執行:
Node<K,V> e; K k;
// 此時的這個p就是指向的上面算出來的陣列下標裡的那個Node物件
//如果當前索引位置對應的連結串列的第一個元素和準備新增的這個key的hash值hash值相同
if (p.hash == hash &&
//如果hash值相同的情況下 當前準備要加入的key和剛剛計算出來的陣列下標對應的那個Node物件的key是同一個物件 或者
// 當前的這個key不為null然後在和計算出來的那個陣列下標對應的那個Node物件裡的key進行equals比較,
//如果沒有重寫那麼呼叫的就是繼承自Object的equals方法,如果重寫過,那麼就呼叫重寫後的,hashcode方法也是一樣,所以建議兩個方法都重寫
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果上面一個條件為假 再判斷 這個p是不是一顆紅黑樹,如果是紅黑樹的話再按照紅黑樹的方式進行比較
// 如果是紅黑樹 呼叫:putTreeVal(); 方法進行新增
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 假如不是紅黑樹,那就是第三種情況:按照上面第一個情況的方式依次和整個連結串列進行比較,如果找到條件滿足的那就直接break(此元素已經存在);
// 結束遍歷,return oldValue 那麼就代表著新增失敗,如果說,比較完後都沒有滿足條件的(該元素不存在),那就掛載到這個連結串列的末尾
// 在把元素新增到最後,立即判斷 該連結串列是否已經到達8個節點,如果到達,呼叫treeifyBin(tab, hash);方法把當前這個連結串列轉化為紅黑樹
判斷條件如下: if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();
// 上面條件不成立才進行樹化 再進行轉紅黑樹時還進行判斷這個陣列連結串列的值是否大於64,如果小於64,還不會轉化為紅黑樹,而是進行陣列的擴容,大於64再轉紅黑樹
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;
}
// 如果 比的過程中找到一個值與準備新增的元素的值一致,那麼就直接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;
4.2、LinkedHashSet
LinkedHashSet類也是根據元素的hashCode值來決定元素的儲存位置,但它同時使用連結串列維護元素的次序。與HashSet相比,特點:
- 對集合迭代時,按增加順序返回元素。
- 效能略低於HashSet,因為需要維護元素的插入順序。但迭代訪問元素時會有好效能,因為它採用連結串列維護內部順序。