1. 程式人生 > >《Java編程思想》筆記 第十七章 容器深入研究

《Java編程思想》筆記 第十七章 容器深入研究

let 類庫 font 有關 有一個 www. 強引用 容器類 刪除

1 容器分類

  • 容器分為Collection集合類,和Map鍵值對類2種
  • 使用最多的就是第三層的容器類,其實在第三層之上還有一層Abstract 抽象類,如果要實現自己的集合類,可以繼承Abstract類,而不必實現接口中的所有方法。
  1. Collection 接口
    1.  List 接口 (按插入順序保存,元素可以重復)
      1.  ArrayList (相當於大小可變的數組,隨機訪問快,插入移除慢)
      2. LinkedList(插入移除快,隨機訪問慢,也實現了Queue接口)
    2.  set 接口(不能有重復元素)
      1.  HashSet(元素無序,查詢速度非常快)
        1.  LinkedHashSet(按插入順序保存,同時有HashSet的查詢速度)
      2. TreeSet(按元素升序保存對象,或者根據元素實現的比較器排序)
    3. Queue接口(一頭進一頭出)
      1.  PriorityQueu(優先級高的先出,也實現了List接口)
  2. Map接口
    1.  HashMap (查找速度快,內部無規則排序)
      1.  LinkedHashMap(按插入順序排序,查找速度快)
    2. TreeMap(按升序保存對象,或者根據元素實現的比較器排序)

2 填充容器

  • 所有Collection的構造器都可以接收另一個Collection(可以不同類型)來填充自己。

2.1 Collections

  • 數組有Arrays類填充,容器也有Collections類填充,這種工具類中一般都是靜態方法不用創建它們的對象直接調用,所以很方便。
  1. fill(list, T obj)方法都只是復制一份對象的引用,並沒有額外創建對象,並且只能填充List,它會將容器內的元素清空再添加元素。
  2. nCopies(int n, T o) 返回一個List 功能和fill一模一樣。
  3. addAll( list, T ... obj) 將元素添加到集合,集合本身也有addAll()方法並且還可以指定位置開始添加

3 Collection

  • Collection中的方法在List和Set中都實現了,List還添加了額外的方法,如get(),這在Collection和Set中都沒有,因為Set無序所以無法確定位置。

4 collection中的可選操作

  • 可選的方法就是該方法在父類中會拋出異常,如果子類不需要該方法就不必重寫它,一但調用則拋出異常,如果需要就去重寫它的功能。
  • Collection中的 各種添加 移除方法都是可選的。AbstractList ,AbstractSet,AbstractQueue中就是實現了可選功能,調用這些抽象類中的方法就會拋出異常。

5 List

  1. jdk1.8 中ArrayList的Add方法實現原理: 如果elementData中元素數大於10個則復制一份舊數組並擴容再將元素添加就去
      public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //關鍵代碼:elementData = Arrays.copyOf(elementData, newCapacity);
            elementData[size++] = e;
            return true;
        }
  2. remove方法 使用本地方法直接操作內存改變, numMoved = size - index - 1; 最後將數組最後一個元素設為null

         System.arraycopy(elementData, index+1, elementData, index,
                 numMoved);
                 elementData[--size] = null; // clear to let GC do its work
  3. 值得註意的是 size是ArrayList內元素個數,並不是數組長度,ArrayList內數組長度默認最小是10

  4. LinkedList 內部是由一個內部靜態類實現的一個雙向鏈表。
     private static class Node<E> {
            E item;
            Node<E> next;
            Node<E> prev;
    
            Node(Node<E> prev, E element, Node<E> next) {
                this.item = element;
                this.next = next;
                this.prev = prev;
            }
        }

6 Set

  1. HashSet底層是由HashMap實現的,add進去的值就put在了map中作為key值,為了減小開銷,所有value值為同一個new Object() 。
    private static final Object PRESENT = new Object();
    
     public boolean add(E e) {
            return map.put(e, PRESENT)==null;
        }
  2. LinkedHashSet也是由 LinkedHashMap實現
  3. TreeSet底層由TreeMap實現。TreeSet 實現了NavigableSet接口,而NavigableSet接口繼承了SortedSet接口 ,NavigableSet提供了搜索功能方法,SortedSet 提供了排序功能方法,
  4. TreeSet(Comparator<? super E> comparator) ,凡是帶有比較排序功能容器都有一個能傳入比較器Comparator對象的構造方法,使用這個構造器可以根據自己實現的Comparator排序。

7 Queue

  • 除了並發應用外,Queue的實現只有LinkedList 和 PriorityQueue,這兩個Queue的差異在於排序行為,而不是性能。隊列行為在於add是添加到隊頭,remove移除隊尾元素。
  1. PriorityQueue(Comparator<? super E> comparator) 可以使用自己的比較器比較,根據對象內某個屬性設置在隊列中的優先級。

8 Map

  1. HashMap的底層數據結構:

    1.    散列表(哈希表)即 數組+單鏈表 默認數組大小是16,數組大小一直保持是2n, 數組下標是 key的hash值進行擾亂 再與 數組大小減1 按位 做 & 運算得出。
    2.   擾亂的目的:為了使得到的值更加均勻。
    3.   減1的目的: 2n - 1 之後低位都是1,進行與運算後只要有一位不同那麽就能的到不同的結果,減小了哈希沖突。同時結果範圍也正好在數組大小相同。
  2. 鏈表的結構

    1.   jdk1.7 Enty
       static class Entry<K,V> implements Map.Entry<K,V> {
              final K key;
              V value;
              Entry<K,V> next;
              int hash;
    2.   jdk1.8 Node
      static class Node<K,V> implements Map.Entry<K,V> {
              final int hash;
              final K key;
              V value;
              Node<K,V> next;

      可以看出只是改了個名字而已。

  3. 單鏈表也稱桶(bucket)

    1.   單鏈表是為了解決哈希沖突,不同的key可能計算出相同的hash值,如果哈希值沖突了那麽就用頭插法將新值插入鏈表的頭,作為數組中的元素,不用尾插法能省去了遍歷鏈表的開銷。
  4. 負載因子

    1.   默認0.75 當map的桶位數(鍵值對數量)達到 數組容量*0.75 時就會進行擴容 ,擴容後的數組是原來的2倍。
  5. 擴容時的操作:

    1.   對舊鏈表正向遍歷,遍歷後插入新表,這樣一來就是正向遍歷,逆向插入,新表的鏈表順序和舊表相反。
    2.   如果多個線程同時擴容容易循環鏈表,所以多線程下put時是不安全的
    3.   擴容後新表和舊表不一定有相同的鏈,也許某一部分會被拆到擴容增加的部分,這完全取決於Hash值。
  6. jdk1.7 的HashMap存在的問題

    1.   有可能出現很多hash值相等的key那麽數組還沒有填滿而,某一位置的鏈表非常長,put/get操作時要遍歷整個鏈表,時間復雜度變為O(n)
  7. jdk1.8 對HashMap作出的改變:

    1.   底層數據結構變為 數組+鏈表+紅黑樹。解決了鏈表可能過長的問題,時間復雜度變為O(log n)
  8. jdk1.8 鏈表轉化紅黑樹

    1. 限定了一些閥值,

      1.  桶的樹化閥值: TREEIFY_THRESHOLD = 8 當鏈表長度大於8時將鏈表轉為紅黑樹。
      2.  桶的鏈表還原閾值:UNTREEIFY_THRESHOLD = 6 當擴容後重新計算位置後若紅黑樹內結點少於6個則紅黑樹轉化為鏈表
      3.  最小樹化閥值 MIN_TREEIFY_CAPACITY = 64 只有當桶位數大於改64時才進行樹化,並且以後樹化不能小於 4*TREEIFY_THRESHOLD
      4.  查看紅黑樹
    2. 左子樹右子樹的判定

      1.  先通過comparableClassFor 反射獲取key對象的所有接口查看有沒有實現comparable接口
      2.  如果key值實現了comparable接口並且compareTo能正確執行並且key值和父結點是同一類型那麽執行compareTo方法比較大小,如果大於父節點那麽作為右孩子,如果小於父節點作為左孩子。
      3.  如果比較是相等的,或者沒有實現接口則進入決勝局方法tieBreakOrder(),先比較他們的類名字符串如果是同一類型則類名比較就比較不出來,再調用本地方法identityHashCode()生成hash值,比較哈希值,如果哈希值相等返回-1,說明這兩個對象在同一個對象,在同一塊內存位置。
      4. 關於jdk1.8 的性能問題就在這裏,如果Key 值沒有實現comparable接口或者comparaTo方法不能的到正確結果,那麽實現紅黑樹的性能沒有只使用鏈表的高。
  9. get()取值:

    1.    計算出hash(key) 再通過key.equals()比較取出值
  10. 那些類做key值比較好?

    1.    String ,Integer 包裝器類型,因為他們是final 型 key不會改變,不會出現放進map之後key被改變的情況,並且重寫了hashCode()和equals()方法不會出現計算錯誤。
    2.   Object 對象做key值時要註意到上面的點。
  11. hashCode()和 equals()方法重寫

    1.   重寫equals方法也要重寫hashCode方法這是因為 Object 中equals方法比較的是對象的地址,hashCode方法是根據對象地址生成的。
    2.   如果重寫equals方法時是使用對象中某個條件判斷他們相等,那麽你再創建一個你認為相等的對象,但他兩地址不一樣,所以在沒有重寫hashCode方法後,他們的hashCode就不一樣,這樣存入map後使用後者取值就無法的到正確結果。
    3.   重寫hashCode方法要保證 如果 equals 相同 那麽hashCode一定相同,hashcode相同equals不一定相同,但這樣會有哈希沖突,所以一個產生一個盡可能散列的hashCode方法非常重要。
  12. HashMap參考博客

    1. HashMap實現原理及源碼分析
    2. HashMap在Java1.7與1.8中的區別
    3. JDK8:HashMap源碼解析:comparableClassFor、compareComparables、tieBreakOrder方法
    4. JDK1.8源碼閱讀系列之四:HashMap (原創)
    5. 關於 HashMap 1.8 的重大更新

9 散列與散列碼

10 接口的不同實現

  1. ArrayList底層是數組所以隨機訪問非常快,但添加刪除時要復制數組,添加刪除的代價較大。
  2. LinkedList 內部是雙向鏈表所以插入刪除代價低,對於隨機訪問會有順著結點一個一個查找過程,所以速度較慢,但如果在2個端點插入或刪除,會做特殊處理速度較快。
  3. TreeSet存在的唯一原因就是他可以維持元素的排序狀態。

11 實用方法

  1. List的排序與查詢所使用的方法與對象數組使用的方法有相同的名字和語法,Collections的static方法相當於代替了Arrays的靜態方法而已。
  2. Collection 和 Map 可以設置為只讀 。Collections的UnmodifiableXXX( )方法設置不同collection和Map為只讀。
  3. Java容器類庫的容錯機制: 但遍歷一個容器或者拿到了該容器的叠代器後改變容器狀態如添加一個元素刪除一個元素修改某個元素則會拋出異常。

12 持有引用

  • Java.lang.ref類庫包含了一組類,這些類為垃圾回收提供了靈活性。
  • 三個繼承Reference抽象類的類:SoftReference, WeakReference, PhantomReference,如果某個對象只能通過這三個對象才可以獲得,那麽GC會對這個對象作出不同的回收。
  • 這三個容器類用來保存對象的引用。
  1. 對象可獲得:棧中有一個普通引用可以直接指向這個對象,或者通過不同的對象間接指向一個對象,那麽這個對象就是可獲得的或者可達的,可獲得的對象是不能被回收的。
  2. 普通引用:也稱強引用,沒有被Reference包裝的引用,通過普通引用可獲得的對象不能被釋放。
  3. 如果一個對象被普通引用指向,那麽他就不能被釋放,一直占據內存,如果沒有引用指向那麽就會被回收,如果有個對象希望以後還能訪問到但是也希望內存不足時可以回收那麽對這類對象的引用就可以放在Reference裏
  4. Reference對象的可獲得性由強到弱,越強越不容易被回收:
    1.  SoftReference 軟引用 ,用來實現內存敏感的高速緩存,如果內存即將溢出時就回收對象。
        Object obj = new Object();
              SoftReference<Object> sf = new SoftReference<Object>(obj);
              obj = null;
              sf.get();//有時候會返回null
    2.  WeakReference 弱引用 用來“規範映射”而設計的,WeekHashMap中的key就是WeekReference。
    3. PhantomReference 虛引用 如果有個對象只有虛引用了那麽他就會被回收。
  5. ReferenceQueue :GC時會在這種隊列(可以自己創建,jvm也會自動創建)中查找虛引用,然後把虛引用的對象清理,softreference 和 weekreference 可以放也可以不放如ReferenceQueue中,但PhantomReference必須在ReferenceQueue中。
  6. WeakHashMap :key 保存弱引用,value 保存其他對象,當key弱引用指向的對象沒有其他強引用引用那麽key-value就會被回收。如果是普通HashMap那麽key指向的對象除了多了一個HashMap的引用,還需要手動清理HashMap。
  7. 查看有關垃圾回收的知識點

知識點:

  1. 兩種比較器Comparable和Comparator
  2. 判斷為空和為0非常重要,引用為空則沒有指向一個實際對象,大小為0說明有對象沒元素。
    Node<K,V>[] tab; 
    if (tab  == null ||  tab.length == 0)

《Java編程思想》筆記 第十七章 容器深入研究