Java進階08-集合框架

集合接口

Map接口

定義解釋

  1. 爲什麼要有集合框架?

早在 Java 2 中之前,Java 就提供了特設類。比如:Dictionary, Vector, Stack, 和 Properties 這些類用來存儲和操作對象組。

雖然這些類都非常有用,但是它們缺少一個核心的,統一的主題。由於這個原因,使用 Vector 類的方式和使用 Properties 類的方式有着很大不同。

集合框架被設計成要滿足以下幾個目標。

  1. 該框架必須是高性能的。基本集合(動態數組,鏈表,樹,哈希表)的實現也必須是高效的。

  2. 該框架允許不同類型的集合,以類似的方式工作,具有高度的互操作性。

  3. 對一個集合的擴展和適應必須是簡單的。

爲此,整個集合框架就圍繞一組標準接口而設計。你可以直接使用這些接口的標準實現,諸如: LinkedList, HashSet, 和 TreeSet 等,除此之外你也可以通過這些接口實現自己的集合。

  1. 集合框架有哪些東西?

主要是Collection和Map接口,和其子類 組成了完整的集合框架。

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

Java集合框架爲程序員提供了預先包裝的數據結構和算法來操縱他們。

集合是一個對象,可容納其他對象的引用。集合接口聲明對每一種類型的集合可以執行的操作。

集合框架的類和接口均在java.util包中。

任何對象加入集合類後,自動轉變爲Object類型,所以在取出的時候,需要進行強制類型轉換。

如何使用

想怎麼用就怎麼用,不過要根據實際情況,選擇合適的集合類。

源碼原理

源碼只能去各個實現類去看

總結
Collection 和 Map 是Java集合框架的根接口。 Set, List, Queue接口是Collection的子接口。 HashMap, Hashtable,SortedMap等都是Map接口的實現。

  1. Java集合分爲 Set, List, Queue, Map四種體系。

  2. Set : 無序,不可重複的集合;

  3. List: 有序,可以重複的集合;

  4. Queue: 隊列集合;

  5. Map: 有映射關係的集合。

  6. Collections和Arrays是2個工具類用於查找和排序等

所到底集合框架 考驗的就是 數據結構基礎。如果不熟悉 數組,鏈表,隊列,棧,樹,查找,排序等 就很難 看懂源碼。

List接口

定義解釋

  1. 什麼是list接口?

List接口是Collection的子類。主要有以下方法:
在這裏插入圖片描述

  1. list接口有哪些特點?

有序-數據存儲的順序就是你添加的順序
可重複
每個元素都有其對應的順序索引
可以添加Null元素

  1. list接口有哪些實現類?

有2個抽象子類:AbstractList和AbstractSequentialList。實現子類有:ArrayList LinkedList Stack Vector CopyOnWriteArrayList。

  1. list接口常用的有哪些?

ArrayList
LinkedList
Vector
Stack
CopyOnWriteArrayList

如何使用

public static <T> void println(T msg) {
	System.out.println("MainCollection=" + msg.toString());
}

public static void main(String[] args) {
	ArrayList<String> arrayList=new ArrayList<String>();
	arrayList.add("我");
	arrayList.add("是");
	arrayList.add("白");
	arrayList.add("嫖");
	println(arrayList.toString());
}

使用非常的簡單,基本沒什麼難度。不會的看下文檔就知道他們那些方法是什麼意思了。

源碼原理

  1. ArrayList

使用數組實現數據的存儲

在這裏插入圖片描述

第一個構造函數
在這裏插入圖片描述

第二個構造函數,無參數,默認數組大小其實是零

在這裏插入圖片描述

第三個 傳入集合參數,則把數據拷貝過來

在這裏插入圖片描述

最普通的添加元素方法put(Object obj):
在這裏插入圖片描述

首先檢測當前容量能否再加1,不能則擴容 。

在這裏插入圖片描述

如果當前的數組大小爲0,傳入進來的值小於默認的10,則取 10.

在這裏插入圖片描述

增加容量也很簡單

首先獲取一個新的容量值:原來的+原來的/2. 可以說是1.5倍。

然後判斷新容量值是否合法。如果超過最大值,則取最大值

在這裏插入圖片描述

clear方法直接把大小至爲0,同時會把每個元素也置空,防止內存泄漏。

在這裏插入圖片描述

add(int index,E element)方法也很簡單,首先判斷index的範圍是否合法。 然後保證正確的容量。然後用System.arraycopy的方法,把目標的位置給空出來 放新元素。

在這裏插入圖片描述

remove方法有2個參數爲index和object。先看index
在這裏插入圖片描述

很簡單,先檢測參數的合法性,然後獲取目標位置的元素,最後返回出去。還是利用System.arraycopy方法把目標位置給填了,再把最後一個重複的元素置空。object一樣很簡單就不看了。

總的來說ArrayList的源碼還是很簡單的,從源碼來看 數據量很大的時候就不要使用了。因爲它不會縮小容量。如果知道數據大概的大小,建議傳入準確值,提高效率。

  1. LinkedList

底層使用鏈表實現:
在這裏插入圖片描述

是雙向鏈表 :
在這裏插入圖片描述

添加元素:

在這裏插入圖片描述

很簡單,就鏈表的尾部插入,注意是雙向。

  1. Vector

這個就是 AarrayList的 同步版本,但是由於它是直接在方法上增加synchronized關鍵字去同步所以效率不高,目前已經很少使用

  1. Stack

是Vector的子類,不過它可以作爲數據結構棧使用,就是可以入棧和出棧。但是效率也不高。

總結

  1. 查找比較多,添加刪除少 時使用ArrayList
  2. 有重複元素添加時使用List
  3. ArrayList底層是通過數組實現,LinkedList是雙向鏈表實現
  4. 要注意線程同步問題

Set接口

定義解釋

Set 接口實例存儲的是無序的,不重複的數據
Set檢索效率低下,刪除和插入效率高,插入和刪除不會引起元素位置改變
接口方法有:
在這裏插入圖片描述

主要實現類:HashSet,TreeSet,LinkedHashSet,EnumSet
如何使用

public static void main(String[] args) {
	HashSet<String> hashSet=new HashSet<>();
	hashSet.add("我");
	hashSet.add("愛");
	hashSet.add("太");
	hashSet.add("陽");
	println(hashSet.toString());
}

使用 大同小異。這裏的運行結果是:我愛陽太。和加入的順序是不同的。但是其實順序也是固定的。這個和其內部的數據結構有關。

源碼原理

  1. HashSet

這個基本是個皮包類,裏面是通過 關聯了一個HashMap實現所有的Set接口功能

HashSet按照Hash算法存儲集合中的元素。 HashSet有以下特點:

不能保證元素的排列順序 HashSet不是同步的,多個線程訪問時,如果有多線程同時修改,需要代碼來保證同步。 集合元素值可以是null 向HashSet 添加元素時,會調用該對象的 hashCode()得到該對象的hashCode值,根據hashCode值決定 HashSet的存儲位置。 判斷兩個元素相等的標準是 通過 equals()方法比較相等,並且hashCode()返回的值也相等。

  1. TreeSet

這個基本也是個皮包類,裏面是通過 關聯了一個TreeMap實現所有的Set接口功能

SortedSet 是 Set的子接口。 TreeSet是 SortedSet接口的實現類,保證集合元素處於排序狀態。

TreeSet支持兩種排序規則: 自然排序和定製排序。 TreeSet會調用集合元素的 compareTo()方法比較元素的大小關係,然後按照升序排列, 這種爲自然排序。 定製排序需要創建TreeSet時,提供一個 Comparator對象, 實現排序邏輯。

  1. LinkedHashSet

LinkedHashSet是 HashSet的子類,LinkedHashSet根據元素的hashCode值決定元素的存儲位置,同時使用鏈表維護元素的次序。使元素看起來是按照插入順序保存的。 由於要維護插入順序,性能略低於HashSet性能,但迭代訪問時有很好的性能。

  1. EnumSet

EnumSet是專門給枚舉類設計的集合類。 所有元素必須是枚舉類型的枚舉值

總結

  1. Set底層都是通過HashMap實現。
  2. 不能存儲重複元素
  3. 無序,因爲是用的哈希表。
  4. 我們可以利用特點 來過濾相同的元素。

queue接口

定義
Queue用於模擬隊列數據結構。 通常指先進先出(FIFO)的容器。 新元素插入到隊尾, 獲取元素會返回隊頭的元素, 通常,不允許隨機訪問隊列中的元素。

Queue 是繼承於 Collection接口。

Queue接口中定義瞭如下的方法:

add() :指定元素加入到隊列的尾部
element(): 獲取頭部元素,但不刪除該元素
offer():指定元素加入到隊列尾部,如果發現隊列已滿無法添加的話,會直接返回false。
peek(): 獲取頭部元素, 但是不刪除, 隊列爲空,返回 null
poll(): 獲取頭部元素,並刪除該元素, 隊列爲空,返回Null
remove():獲取頭部元素,並刪除該元素, 隊列爲空時拋出異常NoSuchElementException
主要的實現類:ArrayBlockQueue,LinkedBlockingQueue,DelayQueue,PriorityQueue等。

Deque集合

Deque接口 是 Queue 接口的子接口。 代表雙端隊列。

Deque 還可當做棧來使用, 包含了 pop() 出棧 和 push()入棧兩個方法。 主要包括以下方法:

addFirst() :插入開頭
addLast(): 插入末尾
descendingIterator(): 返回對應的迭代器,以逆向順序來迭代隊列
getFirst(): 獲取不刪除第一個元素
getLast(): 獲取不刪除最後一個元素
offerFirst(): 插入開頭
offerLast(): 插入末尾
peekFirst():獲取不刪除第一個元素
peekLast(): 獲取不刪除最後一個元素
pollFirst(): 獲取,刪除第一個
pollLast(): 獲取, 刪除最後一個
removeFirst(): 獲取, 刪除一個
removeFirstOccurrence(): 刪除第一次出現的元素
removeLast():獲取,刪除最後一個
removeLastOccurrence(): 刪除最後一個出現的元素
ArrayDeque 是 Deque的實現類。是基於數組實現的雙端隊列。 採用動態,可重分配的Object[]數組來存儲元素。 可以指定Object[]數組長度,默認爲16.

主要的實現類有:ArrayDeque,LinkedBlockingDeque,LinkedList。

當然後還有阻塞隊列。

整個Queue接口的結構如下:
在這裏插入圖片描述

使用

和使用 列表類似

比如使用PriorityQueue:

PriorityQueue priorityQueue = new PriorityQueue();
    priorityQueue.offer(4);
    priorityQueue.offer(-2);
    priorityQueue.offer(8);
    priorityQueue.offer(6);

    System.out.println(priorityQueue);
    System.out.println(priorityQueue.poll());

PriorityQueue 不是絕對標準的隊列實現。 PriorityQueue 保存元素的順序不是按照加入隊列的順序, 而是按照元素的大小重新排序。

上面輸出並沒有完全按照大小排序輸出,這只是受到toString()方法返回值的影響, 實際上多次 poll(),元素是按照從小到大移出隊列。 PriorityQueue 不允許插入 null元素,需要對元素進行排序, 排序有 自然排序 和 定製排序 兩種, 和 TreeSet 基本一致。

比如使用ArrayDeque :

ArrayDeque<String> stack = new ArrayDeque<>();
    stack.push("A");
    stack.push("B");
    stack.push("C");
    System.out.println(stack);
    System.out.println(stack.peek());
    System.out.println(stack.pop());
    System.out.println(stack);

推薦使用 ArrayDeque代替 Stack,Stack是古老的集合, 性能較差。

ArrayDeque 也可以當做隊列使用, 先進先出。

ArrayDeque queue = new ArrayDeque();
    queue.offer("A");
    queue.offer("B");
    queue.offer("C");
    System.out.println(queue);
    System.out.println(queue.peek());
    System.out.println(queue);
    System.out.println(queue.poll());
    System.out.println(queue);

LinkedList 是 List接口的實現類,可以根據索引隨機訪問, LinkedList還是實現類 Deque的接口。 可以當做雙端隊列,即可以當 棧, 也可以當 隊列

原理

整體和上面的Arrylist差不多

總結

  1. 需要排隊的時候可以使用
  2. 有阻塞隊列和非阻塞隊列
  3. 生產者和消費者

Map接口

定義

Map集合用於保存映射關係的數據,Map集合中保存了兩組值,一組是 key, 一組是 value。 Map的key不能重複。 key和value之間存在單向一對一的關係, 通過key,能找到唯一的,確定的value。

map接口中定義的方法:
在這裏插入圖片描述

使用

public static void main(String[] args) {
	HashMap<String, String> hashMap=new HashMap<>();
	hashMap.put("a", "我");
	hashMap.put("b", "愛");
	hashMap.put("c", "太");
	hashMap.put("d", "陽");
	println(hashMap.toString());
	Set<Entry<String, String>> entrySet = hashMap.entrySet();
	Iterator<Entry<String, String>> iterator = entrySet.iterator();
	while (iterator.hasNext()) {
		Entry<String, String> entry=iterator.next();
		println(entry.getKey()+"---"+entry.getValue());
	}
}

運行結果:

在這裏插入圖片描述

使用也是非常簡單的,就是 put 和get。關鍵還是源碼。Key的存儲是根據hashcode來的。

原理

構造函數
在這裏插入圖片描述
默認的話。構造因子就是0.75,意思就是 數組的容量是100,如果當前已有75個數據時就要 擴容數組了。初始默認的數組容量是16.最大容量是2的30次方。10億。 整個結構類似:
在這裏插入圖片描述

怎麼實現存儲數據?
其實就是看Put方法的實現:
在這裏插入圖片描述
計算Hash值。

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)
        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;
}

這是整個Put方法。我來解釋下

  1. 首先判斷hash表是否爲空,是則resize()擴容,這個方法也比較複雜
  2. 根據i = (n - 1) & hash 獲取下標如果沒有元素則把新的節點插入完事
  3. 如果有元素了,那就是去解決衝突問題了。首先判斷key是否相等,如果相等則直接替換完事
  4. 如果當前節點是樹節點 就是已經是紅黑樹結構則插入樹對應的位置。
  5. 如果是還是鏈表節點則插入末尾
  6. 插入後如果節點數大於等於了7 則轉化爲樹
  7. 最後如果元素數量達到了臨界值,則調用resize()擴容

怎麼擴容?
擴容就是resize()方法:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

這個方法也很長,需要解釋下:

  1. 判斷原來的表如果大於0,並且已經大於最大值2的30次方,10億,MAXIMUM_CAPACITY,則把容量賦值爲2147483647 2的31次方,20億。
  2. 否則左移一位,翻倍擴容。
  3. 重新複製臨界值大小
  4. 接着就要把原表的數據 拷貝過來。
  5. 循環遍歷舊錶,如果元素不爲空,則把原表元素置空。
  6. 如果此節點沒有下一個節點那好辦用 e.hash & (newCap - 1) 獲取新表的下標,把元素放入就好了。
  7. 如果是紅黑樹節點了,則分割紅黑樹節點到新表
  8. 則分解 鏈表節點到新表
    總的來說還是很複雜的。

怎麼解決哈希衝突?

使用的是 鏈地址法。通過一個鏈表維護。如果節點數量超過7個則轉化爲紅黑樹 提高性能。轉化過程也是相當複雜,涉及了紅黑的所有知識。旋轉,平衡等

get的實現?

get方法相對 remove方法就簡單點了。

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
  1. 首先獲取傳入進來的key的hash值。
  2. 判斷當前的表 不爲空,有元素,且根據hash獲取的第一個節點不爲空
  3. 如果此節點的hash值和傳入的是一樣的。則返回此節點
  4. 如果不是,則判斷此節點是屬於鏈表,還是紅黑樹。
  5. 則在鏈表中查詢返回,後者是紅黑樹中查詢返回。
  6. 紅黑樹是根據hash大小來排序的。

怎麼遍歷?
使用迭代器模式進行遍歷

最終實現:HashIterator類中

在這裏插入圖片描述

總結

  1. 哈希表
  2. 紅黑樹
  3. 單鏈表
  4. 代碼實現的還是有難度,暫時只能掌握思想。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章