1. 程式人生 > 其它 >Java基礎(3)|Collection

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相比,特點:

  1. 對集合迭代時,按增加順序返回元素。
  2. 效能略低於HashSet,因為需要維護元素的插入順序。但迭代訪問元素時會有好效能,因為它採用連結串列維護內部順序。