1. 程式人生 > >Java容器/集合之實現原理

Java容器/集合之實現原理

集合框架中包含了一系列不同資料結構(線性表,查詢表...),是用來儲存一組資料的結構。

整個集合框架關係展現

image.png

原圖出處:http://pierrchen.blogspot.com/2014/03/java-collections-framework-cheat-sheet.html

處於圖片左上角的那一塊灰色裡面的四個類(Dictionary、HashTable、Vector、Stack)都是執行緒安全的,可是它們都是JDK的老的遺留類。如今都有了相應的取代類。

當中Map介面是用來取代圖片中左上角的那個Dictionary抽象類。

HashTable,官方推薦ConcurrentHashMap來取代。接著以下的Vector是List以下的一個實現類。

最上面的粉紅色部分是集合類全部介面關係圖。其中Collection有三個繼承介面:List、Queue和Set。

綠色部分則是集合類的主要實現類,也是我們常用的集合類。


在這裡,集合類分為了Map和Collection兩個大的類別。

1) Collection

一組"對立"的元素,通常這些元素都服從某種規則

   1.1) List必須保持元素特定的順序

   1.2) Set不能有重複元素

   1.3) Queue保持一個佇列(先進先出)的順序

2) Map

一組成對的"鍵值對"物件


集合分類:

依照實現介面分類:

實現Map介面的有:EnumMap、IdentityHashMap、HashMap、LinkedHashMap、WeakHashMap、TreeMap

實現List介面的有:ArrayList、LinkedList

實現Set介面的有:HashSet、LinkedHashSet、TreeSet

實現Queue介面的有:PriorityQueue、LinkedList、ArrayQueue


依據底層實現的資料結構分類:

底層以陣列的形式實現:EnumMap、ArrayList、ArrayQueue
底層以連結串列的形式實現:LinkedHashSet、LinkedList、LinkedHashMap
底層以hash table的形式實現:HashMap、HashSet、LinkedHashMap、LinkedHashSet、WeakHashMap、IdentityHashMap
底層以紅黑樹的形式實現:TreeMap、TreeSet
底層以二叉堆的形式實現:PriorityQueue


Collection常用方法

   int size():返回集合裡邊包含的物件個數

   boolean isEmpty():是否為空(不是null而是裡邊沒有元素)

   boolean contains(Object o):是否包含指定物件

   boolean clear():清空集合

   boolean add(E e):向集合中新增物件

   boolean remove(Object o):移出某個物件

   boolean addAll(Collection <?extends E> c):將另一個集合中的所有元素新增到集合中。

   boolean removeAll(Collection<?> c):移出集合中與另一個集合中相同的全部元素。

   Iterator<E> iterator():返回該集合的對應的迭代器。

list常用方法

List除了繼承Collection定義的方法外,還根據線性表的資料結構定義了一系列方法。

   1)get(int index)方法,獲取集合中索引的元素。

       注:這個方法是List中獨有的,返回的是Object

   2)Object set(int index,Object obj):將給定的元素替換集合中索引為index的元素,返回的是被替換的元素。        

   3)add和remove有方法過載        

       add(int index, Object obj):將給定的元素插入索引處,原位置上及後面的元素順序向後移(插隊)。

       Object remove(int index):刪除指定索引處的元素,該方法的返回只是被刪除的元素。    

  List還提供類似String的indexOf和lastIndexOf方法,用於在集合中檢索某個物件,其判斷邏輯為:(o==null?get(i)==null:o.equals(get(i)))

   1)int indexOf(Object obj):返回首次在集合中出現該元素的索引值。

   2)lastIndexOf(Object obj):返回最後一次在集合中出現該元素的索引值。

 還有可以將集合轉換為陣列的方法:    

   3)toArray():將集合轉化為陣列。這裡引數僅僅是告知集合要轉換的陣列型別,並不會使用我們提供的陣列,所以不需要給長度。

    集合中的元素應為同一個型別。

   String[] array = (String[])list.toArray(new String[0]);  

ArrayList底層實現方式

   ArrayList底層是用陣列實現的儲存。 特點:查詢效率高,增刪效率低,執行緒不安全。

ArrayList底層使用Object陣列來儲存元素資料。所有的方法,都圍繞這個核心的Object陣列來操作。

但是,陣列長度是有限的,而ArrayList是可以存放任意數量的物件,長度不受限制。

其本質上就是通過定義新的更大的陣列,將舊陣列中的內容拷貝到新陣列,來實現擴容。 ArrayList的Object陣列初始化長度為10,如果我們儲存滿了這個陣列,需要儲存第11個物件,就會定義新的長度更大的陣列,並將原陣列內容和新的元素一起加入到新陣列中。

image.png

LinkedList底層實現

LinkedList底層用雙向連結串列實現的儲存。特點:查詢效率低,增刪效率高,執行緒不安全。

雙向連結串列也叫雙鏈表,是連結串列的一種,它的每個資料節點中都有兩個指標,分別指向前一個節點和後一個節點。 所以,從雙向連結串列中的任意一個節點開始,都可以很方便地找到所有節點。每個節點都應該有3部分內容:

  class  Node {
        Node  previous;     //前一個節點
        Object  element;    //本節點儲存的資料
        Node  next;         //後一個節點
}

image.png

private static class Node<E> {
    //業務資料
        E item;
    //指向下個node
        Node<E> next;
    //指向上個node
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

如果原來firstNode為空的話,說明這個list為空,那麼這時FirstNode也就是lastNode,這個連結串列只有一個node。

首節點的prev和lastNode的next為null


HashMap實現原理

Map就是用來儲存“鍵(key)-值(value) 對”的。 Map類中儲存的“鍵值對”通過鍵來標識,所以“鍵物件”不能重複。


雜湊表

        

  雜湊表(hash table)也叫散列表,是一種非常重要的資料結構,應用場景及其豐富,許多快取技術(比如memcached)的核心其實就是在記憶體中維護一張大的雜湊表,而HashMap的實現原理也常常出現在各類的面試題中,重要性可見一斑。

      HashMap底層實現採用了雜湊表,這是一種非常重要的資料結構。

      資料結構中由陣列和連結串列來實現對資料的儲存,他們各有特點。

      (1) 陣列:佔用空間連續。 定址容易,查詢速度快。但是,增加和刪除效率非常低。

      (2) 連結串列:佔用空間不連續。 定址困難,查詢速度慢。但是,增加和刪除效率非常高。


      而“雜湊表”具備了陣列和連結串列的優點。 雜湊表的本質就是“陣列+連結串列”。

        在雜湊表中進行新增,刪除,查詢等操作,效能十分之高,不考慮雜湊衝突的情況下,僅需一次定位即可完成,時間複雜度為O(1),接下來我們就來看看雜湊表是如何實現達到驚豔的常數階O(1)的。

  資料結構的物理儲存結構只有兩種:順序儲存結構和鏈式儲存結構(像棧,佇列,樹,圖等是從邏輯結構去抽象的,對映到記憶體中,也這兩種物理組織形式),而在陣列中根據下標查詢某個元素,一次定位就可以達到,雜湊表利用了這種特性,雜湊表的主幹就是陣列。

  比如我們要新增或查詢某個元素,我們通過把當前元素的關鍵碼通過某個函式對映到陣列中的某個位置,通過陣列下標一次定位就可完成操作。

        儲存位置 = f(關鍵碼)

  其中,這個函式f一般稱為雜湊函式,通過關鍵碼就可以直接定位到元素的儲存位置。

雜湊衝突

  如果兩個不同的元素,通過雜湊函式得出的實際儲存地址相同怎麼辦?也就是說,當我們對某個元素進行雜湊運算,得到一個儲存地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的雜湊衝突,也叫雜湊碰撞。雜湊函式的設計至關重要,好的雜湊函式會盡可能地保證 計算簡單和雜湊地址分佈均勻。但是,我們需要清楚的是,陣列是一塊連續的固定長度的記憶體空間,再好的雜湊函式也不能保證得到的儲存地址絕對不發生衝突。那麼雜湊衝突如何解決呢?雜湊衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的儲存地址),再雜湊函式法,鏈地址法,而HashMap即是採用了鏈地址法,也就是陣列+連結串列的方式。

image.png

或者

image.png

HashMap的主幹是一個Entry陣列。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。

一個Entry物件儲存了:

      1. key:鍵物件 value:值物件

      2. next:下一個節點

      3. hash: 鍵物件的hash值


儲存資料

  我們的目的是將”key-value兩個物件”成對存放到HashMap的Entry[]陣列中。

      (1) 獲得key物件的hashcode

           首先呼叫key物件的hashcode()方法,獲得hashcode。

      (2) 根據hashcode計算出hash值(要求在[0, 陣列長度-1]區間)

           hashcode是一個整數,我們需要將它轉化成[0, 陣列長度-1]的範圍。我們要求轉化後的hash值儘量均勻地分佈在[0,陣列長度-1]這個區間,減少“hash衝突”

           i. 一種極端簡單和低下的演算法是:

           hash值 = hashcode/hashcode;

           也就是說,hash值總是1。意味著,鍵值對物件都會儲存到陣列索引1位置,這樣就形成一個非常長的連結串列。相當於每儲存一個物件都會發生“hash衝突”,HashMap也退化成了一個“連結串列”。

           ii. 一種簡單和常用的演算法是(相除取餘演算法):

           hash值 = hashcode%陣列長度

           這種演算法可以讓hash值均勻的分佈在[0,陣列長度-1]的區間。 早期的HashTable就是採用這種演算法。但是,這種演算法由於使用了“除法”,效率低下。JDK後來改進了演算法。首先約定陣列長度必須為2的整數冪,這樣採用位運算即可實現取餘的效果:hash值 = hashcode&(陣列長度-1)。

      (3) 生成Entry物件

          如上所述,一個Entry物件包含4部分:key物件、value物件、hash值、指向下一個Entry物件的引用。我們現在算出了hash值。下一個Entry物件的引用為null。

      (4) 將Entry物件放到table陣列中

          如果本Entry物件對應的陣列索引位置還沒有放Entry物件,則直接將Entry物件儲存進陣列。如果對應索引位置已經有Entry物件,則將已有Entry物件的next指向本Entry物件,形成連結串列。


總結

      當新增一個元素(key-value)時,首先計算key的hash值,以此確定插入陣列中的位置,但是可能存在同一hash值的元素已經被放在陣列同一位置了,這時就新增到同一hash值的元素的後面,他們在陣列的同一位置,就形成了連結串列,同一個連結串列上的Hash值是相同的,所以說陣列存放的是連結串列。

▪ 取資料過程get(key)

      我們需要通過key物件獲得“鍵值對”物件,進而返回value物件。明白了儲存資料過程,取資料就比較簡單了,參見以下步驟:

      (1) 獲得key的hashcode,通過hash()雜湊演算法得到hash值,進而定位到陣列的位置。

      (2) 在連結串列上挨個比較key物件。 呼叫equals()方法,將key物件和連結串列上所有節點的key物件進行比較,直到碰到返回true的節點物件為止。

      (3) 返回equals()為true的節點物件的value物件。

      明白了存取資料的過程,我們再來看一下hashcode()和equals方法的關係:

      Java中規定,兩個內容相同(equals()為true)的物件必須具有相等的hashCode。因為如果equals()為true而兩個物件的hashcode不同;那在整個儲存過程中就發生了悖論。

▪ 擴容問題

      HashMap的位桶陣列,初始大小為16。實際使用時,顯然大小是可變的。如果位桶陣列中的元素達到(0.75*陣列 length), 就重新調整陣列大小變為原來2倍大小。

      擴容很耗時。擴容的本質是定義新的更大的陣列,並將舊陣列內容挨個拷貝到新陣列中。

▪ JDK8將連結串列在大於8情況下變為紅黑二叉樹

      JDK8中,HashMap在儲存一個元素時,當對應連結串列長度大於8時,連結串列就轉換為紅黑樹,這樣又大大提高了查詢的效率。


HashMap原理借鑑https://www.cnblogs.com/chengxiao/p/6059914.html


TreeMap原理實現


首先介紹一下二叉樹和紅黑二叉樹


二叉樹的定義

      二叉樹是樹形結構的一個重要型別。 許多實際問題抽象出來的資料結構往往是二叉樹的形式,即使是一般的樹也能簡單地轉換為二叉樹,而且二叉樹的儲存結構及其演算法都較為簡單,因此二叉樹顯得特別重要。

      二叉樹(BinaryTree)由一個節點及兩棵互不相交的、分別稱作這個根的左子樹和右子樹的二叉樹組成。下圖中展現了五種不同基本形態的二叉樹。

image.png

      (a) 為空樹。

      (b) 為僅有一個結點的二叉樹。

      (c) 是僅有左子樹而右子樹為空的二叉樹。

      (d) 是僅有右子樹而左子樹為空的二叉樹。

      (e) 是左、右子樹均非空的二叉樹。

注:二叉樹的左子樹和右子樹是嚴格區分並且不能隨意顛倒的,圖 (c) 與圖 (d) 就是兩棵不同的二叉樹。

排序二叉樹特性如下:

      (1) 左子樹上所有節點的值均小於它的根節點的值。

      (2) 右子樹上所有節點的值均大於它的根節點的值。

      比如:我們要將資料【14,12,23,4,16,13, 8,,3】儲存到排序二叉樹中,如下圖所示:

image.png

      排序二叉樹本身實現了排序功能,可以快速檢索。但如果插入的節點集本身就是有序的,要麼是由小到大排列,要麼是由大到小排列,那麼最後得到的排序二叉樹將變成普通的連結串列,其檢索效率就會很差。 比如上面的資料【14,12,23,4,16,13, 8,,3】,我們先進行排序變成:【3,4,8,12,13,14,16,23】,然後儲存到排序二叉樹中,顯然就變成了連結串列,如下圖所示:

image.png

平衡二叉樹(AVL)

      為了避免出現上述一邊倒的儲存,科學家提出了“平衡二叉樹”。

      在平衡二叉樹中任何節點的兩個子樹的高度最大差別為1,所以它也被稱為高度平衡樹。 增加和刪除節點可能需要通過一次或多次樹旋轉來重新平衡這個樹。

      節點的平衡因子是它的左子樹的高度減去它的右子樹的高度(有時相反)。帶有平衡因子1、0或 -1的節點被認為是平衡的。帶有平衡因子 -2或2的節點被認為是不平衡的,並需要重新平衡這個樹。

      比如,我們儲存排好序的資料【3,4,8,12,13,14,16,23】,增加節點如果出現不平衡,則通過節點的左旋或右旋,重新平衡樹結構,最終平衡二叉樹如下圖所示:

image.png

      平衡二叉樹追求絕對平衡,實現起來比較麻煩,每次插入新節點需要做的旋轉操作次數不能預知。

紅黑二叉樹

      紅黑二叉樹(簡稱:紅黑樹),它首先是一棵二叉樹,同時也是一棵自平衡的排序二叉樹。

      紅黑樹在原有的排序二叉樹增加了如下幾個要求:

      1. 每個節點要麼是紅色,要麼是黑色。

      2. 根節點永遠是黑色的。

      3. 所有的葉節點都是空節點(即 null),並且是黑色的。

      4. 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的路徑上不會有兩個連續的紅色節點)

      5. 從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點。

      這些約束強化了紅黑樹的關鍵性質:從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。這樣就讓樹大致上是平衡的。

      紅黑樹是一個更高效的檢索二叉樹,JDK 提供的集合類 TreeMap、TreeSet 本身就是一個紅黑樹的實現。

image.png


      紅黑樹的基本操作:插入、刪除、左旋、右旋、著色。 每插入或者刪除一個節點,可能會導致樹不在符合紅黑樹的特徵,需要進行修復,進行 “左旋、右旋、著色”操作,使樹繼續保持紅黑樹的特性。


TreeMap是紅黑二叉樹的典型實現

private transient Entry<K,V> root = null;

root用來儲存整個樹的根節點。我們繼續跟蹤Entry(是TreeMap的內部類)的程式碼:

image.png


     可以看到裡面儲存了本身資料、左節點、右節點、父節點、以及節點顏色。

 TreeMap的put()/remove()方法大量使用了紅黑樹的理論。

 TreeMap和HashMap實現了同樣的介面Map,因此,用法對於呼叫者來說沒有區別。HashMap效率高於TreeMap;在需要排序的Map時才選用TreeMap。


HashSet實現原理

     HashSet是採用雜湊演算法實現,底層實際是用HashMap實現的(HashSet本質就是一個簡化版的HashMap),因此,查詢效率和增刪效率都比較高。

image.png

      發現裡面有個map屬性,這就是HashSet的核心祕密。我們再看add()方法,發現增加一個元素說白了就是在map中增加一個鍵值對,鍵物件就是這個元素,值物件是名為PRESENT的Object物件。

本質就是把這個元素作為key加入到了內部的map中”。

      由於map中key都是不可重複的,因此,Set天然具有“不可重複”的特性。


TreeSet實現原理

      TreeSet底層實際是用TreeMap實現的,內部維持了一個簡化版的TreeMap,通過key來儲存Set的元素。 TreeSet內部需要對儲存的元素進行排序,因此,我們對應的類需要實現Comparable介面。這樣,才能根據compareTo()方法比較物件之間的大小,才能進行內部排序。

      (1) 由於是二叉樹,需要對元素做內部排序。 如果要放入TreeSet中的類沒有實現Comparable介面,則會丟擲異常:java.lang.ClassCastException。

      (2) TreeSet中不能放入null元素。