【查漏補缺】Java 集合詳解!

前言

數據結構做爲每個開發者不可迴避的問題,而 Java 對於不一樣的數據結構提供了很是成熟的實現,這一個又一個實現既是面試中的難點,也是工做中必不可少的工具,在此,筆者經歷漫長的剖析,將其抽絲剝繭的呈現出來,在此僅做拋磚引玉,望得諸君高見,若君能有所獲則在下甚是不亦樂乎,如有疑惑亦願與諸君共求之!本文一共 3.5 W字,25 張圖,預計閱讀 2h。能夠收藏這篇文章,用的時候防止找不到,這多是你能看到的最詳細的一篇文章了。整理了2021年Java面試題java

一、集合框架

Java整個集合框架如上圖所示(這兒不包括Map,Map的結構將放在集合後邊進行講述),可見全部集合實現類的最頂層接口爲Iterable和Collection接口,再向下Collection分爲了三種不一樣的形式,分別是List,Queue和Set接口,而後就是對應的不一樣的實現方式。node

1.1頂層接口Iterable

//支持lambda函數接口
import java.util.function.Consumer;
public interface Iterable<T> {
  //iterator()方法
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}
複製代碼

Iterable接口中只有iterator()一個接口方法,Iterator也是一個接口,其主要有以下兩個方法hasNext()和next()方法。面試

package java.util;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean removeAll(Collection<?> c);
    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }
    boolean retainAll(Collection<?> c);
    void clear();
    int hashCode();
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}
複製代碼

可見Collection的主要接口方法有: 數組

二、List

List表示一串有序的集合,和Collection接口含義不一樣的是List突出有序的含義。安全

2.1 List接口

package java.util;

import java.util.function.UnaryOperator;
public interface List<E> extends Collection<E> {
    <T> T[] toArray(T[] a);
    boolean addAll(Collection<? extends E> c);
    boolean addAll(int index, Collection<? extends E> c);
    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    boolean equals(Object o);
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    ListIterator<E> listIterator();
    List<E> subList(int fromIndex, int toIndex);
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }
}
複製代碼

可見List其實比Collection多了添加方法add和addAll查找方法get,indexOf,set等方法,而且支持index下標操做Collection 與 List 的區別?a. 從上邊能夠看出Collection和List最大的區別就是Collection是無序的,不支持索引操做,而List是有序的。Collection沒有順序的概念。b. List中Iterator爲ListIterator。c. 由a推導List能夠進行排序,因此List接口支持使用sort方法。d. 兩者的Spliterator操做方式不同。**爲何子類接口裏重複申明父類接口呢?**官方解釋: 在子接口中重複聲明父接口是爲了方便看文檔。好比在 java doc 文檔裏,在 List 接口裏也能看到 Collecion 聲明的相關接口。markdown

2.2 List實現ArrayList

ArrayList是List接口最經常使用的一個實現類,支持List接口的一些列操做。數據結構

2.2.1 ArrayList繼承關係

2.2.2 ArrayList組成

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}
//真正存放元素的數組
transient Object[] elementData; // non-private to simplify nested class access
private int size;
複製代碼

必定要記住ArrayList中的transient Object[] elementData,該elementData是真正存放元素的容器,可見ArrayList是基於數組實現的。app

2.2.3 ArrayList構造函數

>public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
複製代碼
  1. Object[] elementData 是ArrayList真正存放數據的數組。
  2. ArrayList支持默認大小構造,和空構造,當空構造的時候存放數據的Object[] elementData是一個空數組{}。

2.2.4 ArrayList中添加元素

>public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
}
複製代碼

注意ArrayList中有一個modCount的屬性,表示該實例修改的次數。(全部集合中都有modCount這樣一個記錄修改次數的屬性),每次增改添加都會增長一次該ArrayList修改次數,而上邊的add(E e)方法是將新元素添加到list尾部。框架

2.2.4 ArrayList擴容

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //DEFAULT_CAPACITY是10
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
複製代碼

可見當初始化的list是一個空ArrayList的時候,會直接擴容到DEFAULT_CAPACITY,該值大小是一個默認值10。而當添加進ArrayList中的元素超過了數組能存放的最大值就會進行擴容。注意到這一行代碼jvm

int newCapacity = oldCapacity + (oldCapacity >> 1);
複製代碼

採用右移運算,就是原來的通常,因此是擴容1.5倍。好比10的二進制是1010,右移後變成101就是5。

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼

2.2.5 數組copy

Java 是沒法本身分配空間的,是底層C和C++的實現。以 C 爲例,咱們知道 C 中數組是一個指向首部的指針,好比咱們 C 語言對數組進行分配內存。Java 就是經過 arraycopy 這個 native 方法實現的數組的複製。

public static native void arraycopy(Object src, int  srcPos,
                                        Object dest, int destPos,
                                        int length);
複製代碼
p = (int *)malloc(len*sizeof(int));
複製代碼

這樣的好處何在呢?**Java裏內存是由jvm管理的,而數組是分配的連續內存,而arrayList不必定是連續內存,固然jvm會幫咱們作這樣的事,jvm會有內部的優化,會在後續的例子中結合問題來講明。

2.2.6 why?elementData用transient修飾?

  1. transient的做用是該屬性不參與序列化。

  2. ArrayList繼承了標示序列化的Serializable接口

  3. 對arrayList序列化的過程當中進行了讀寫安全控制。是如何實現序列化安全的呢?

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();

    // Write out size as capacity for behavioural compatibility with clone()
    s.writeInt(size);

    // Write out all elements in the proper order.
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }

    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

/**
 * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
 * deserialize it).
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;

    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in capacity
    s.readInt(); // ignored

    if (size > 0) {
        // be like clone(), allocate array based upon size not capacity
        int capacity = calculateCapacity(elementData, size);
        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
        ensureCapacityInternal(size);

        Object[] a = elementData;
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}
複製代碼

在序列化方法writeObject()方法中能夠看到,先用默認寫方法,而後將size寫出,最後遍歷寫出elementData,由於該變量是transient修飾的,全部進行手動寫出,這樣它也會被序列化了。那是否是畫蛇添足呢?

protected transient int modCount = 0;
複製代碼

固然不是,其中有一個關鍵的modCount, 該變量是記錄list修改的次數的,當寫入完以後若是發現修改次數和開始序列化前不一致就會拋出異常,序列化失敗。這樣就保證了序列化過程當中是未經修改的數據,保證了序列化安全。(java集合中都是這樣實現)

2.3 LinkedList

衆所周知LinkedList是一種鏈表結構,那麼Java裏LinkedList是如何實現的呢?

2.3.1 LinkedList繼承關係

可見LinkedList既是List接口的實現也是Queue的實現(Deque),故其和ArrayList相比LinkedList支持的功能更多,其可視做隊列來使用,固然下文中不強調其隊列的實現。

2.3.2 LinkedList的結構

transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 * (last.next == null && last.item != null)
 */
transient Node<E> last;
複製代碼

LinkedList由一個頭節點和一個尾節點組成,分別指向鏈表的頭部和尾部。 LinkedList中Node源碼以下,由當前值item,和指向上一個節點prev和指向下個節點next的指針組成。而且只含有一個構造方法,按照(prev, item, next)這樣的參數順序構造。

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;
    }
}
複製代碼

那LinkedList裏節點Node是什麼結構呢? LinkedList由一個頭節點,一個尾節點和一個默認爲0的size構成,可見其是雙向鏈表。

<pre style="margin: 0px; padding: 0px; max-width: 100%; background-image: none; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; box-sizing: border-box !important; overflow-wrap: break-word !important;">transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public LinkedList() {
}
複製代碼

數據結構中鏈表的頭插法linkFirst和尾插法linkLast

/**
 * Links e as first element. 頭插法
 */
private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

/**
 * Links e as last element. 尾插法
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
複製代碼

2.3.3 LinkedList查詢方法

按照下標獲取某一個節點**:get方法,獲取第index個節點。

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
複製代碼

node(index)方法是怎麼實現的呢?判斷index是更靠近頭部仍是尾部,靠近哪段從哪段遍歷獲取值。

Node<E> node(int index) {
    // assert isElementIndex(index);
    //判斷index更靠近頭部仍是尾部
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
複製代碼

查詢索引修改方法,先找到對應節點,將新的值替換掉老的值。

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}
複製代碼

這個也是爲何ArrayList隨機訪問比LinkedList快的緣由**,LinkedList要遍歷找到該位置才能進行修改,而ArrayList是內部數組操做會更快。

2.4.3 LinkedList修改方法

新增一個節點,能夠看到是採用尾插法將新節點放入尾部。

public boolean add(E e) {
    linkLast(e);
    return true;
}
複製代碼

2.5 Vector

和ArrayList同樣,Vector也是List接口的一個實現類。其中List接口主要實現類有ArrayLIst,LinkedList,Vector,Stack,其中後二者用的特別少。

2.5.1 vector組成

和ArrayList基本同樣。

//存放元素的數組
protected Object[] elementData;
//有效元素數量,小於等於數組長度
protected int elementCount;
//容量增長量,和擴容相關
protected int capacityIncrement;
複製代碼

2.5.2 vector線程安全性

vector是線程安全的,synchronized修飾的操做方法。

2.5.3 vector擴容

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //擴容大小
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
複製代碼

看源碼可知,擴容當構造沒有capacityIncrement時,一次擴容數組變成原來兩倍,不然每次增長capacityIncrement。

2.5.4 vector方法經典示例

移除某一元素

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
      //複製數組,假設數組移除了中間某元素,後邊有效值前移1位
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
                         //引用null ,gc會處理
    elementData[--elementCount] = null; // Let gc do its work

    return oldValue;
}
複製代碼

這兒主要有一個兩行代碼須要注意,筆者在代碼中有註釋。數組移除某一元素而且移動後,必定要將原來末尾設爲null,且有效長度減1。整體上vector實現是比較簡單粗暴的,也不多用到,隨便看看便可。

2.6 Stack

Stack也是List接口的實現類之一,和Vector同樣,由於性能緣由,更主要在開發過程當中不多用到棧這種數據結構,不過棧在計算機底層是一種很是重要的數據結構,下邊將探討下Java中Stack。

2.6.1 Stack的繼承關係

Stack繼承於Vector,其也是List接口的實現類。以前提到過Vector是線程安全的,由於其方法都是synchronized修飾的,故此處Stack從父類Vector繼承而來的操做也是線程安全的。

2.6.2 Stack的使用

正如Stack是棧的實現,故其主要操做爲push入棧和pop出棧,而棧最大的特色就是LIFO(Last In First Out)。

Stack<String> strings = new Stack<>();
strings.push("aaa");
strings.push("bbb");
strings.push("ccc");
System.err.println(strings.pop());
複製代碼

上邊代碼能夠看到,最後push入棧的字符串"ccc"也最早出棧。

2.6.3 Stack源碼

/**
 * Stack源碼(Jdk8)
 */
public
class Stack<E> extends Vector<E> {
    public Stack() {
    }
  //入棧,使用的是Vector的addElement方法。
  public E push(E item) {
        addElement(item);
        return item;
    }
    //出棧,找到數組最後一個元素,移除並返回。
    public synchronized E pop() {
        E obj;
        int     len = size();
        obj = peek();
        removeElementAt(len - 1);
        return obj;
    }
    public synchronized E peek() {
        int     len = size();
        if (len == 0)
            throw new EmptyStackException();
        return elementAt(len - 1);
    }
    public boolean empty() {
        return size() == 0;
    }
    public synchronized int search(Object o) {
        int i = lastIndexOf(o);
        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }
    private static final long serialVersionUID = 1224463164541339165L;
}
複製代碼

從Stack的源碼中可見,其用的push方法用的是Vector的addElement(E e)方法,該方法是將元素放在集合的尾部,而其pop方法使用的是Vector的removeElementAt(Index x)方法,移除並獲取集合的尾部元素,可見Stack的操做就是基於線性表的尾部進行操做的。

三、Queue

正如數據結構中描述,queue是一種先進先出的數據結構,也就是first in first out。能夠將queue看做一個只能夠從某一段放元素進去的一個容器,取元素只能從另外一端取,整個機制以下圖所示,不過須要注意的是,隊列並無規定是從哪一端插入,從哪一段取出。

3.1 什麼是Deque

Deque英文全稱是Double ended queue,也就是俗稱的雙端隊列。就是說對於這個隊列容器,既能夠從頭部插入也能夠從尾部插入,既能夠從頭部獲取,也能夠從尾部獲取,其機制以下圖所示。

3.1.1 Java中的Queue接口

此處須要注意,Java中的隊列明確有從尾部插入,頭部取出,因此Java中queue的實現都是從頭部取出。

package java.util;
public interface Queue<E> extends Collection<E> {
     //集合中插入元素
    boolean add(E e);
    //隊列中插入元素
    boolean offer(E e);
    //移除元素,當集合爲空,拋出異常
    E remove();
    //移除隊列頭部元素並返回,若是爲空,返回null
    E poll();
    //查詢集合第一個元素,若是爲空,拋出異常
    E element();
    //查詢隊列中第一個元素,若是爲空,返回null
    E peek();
}
複製代碼

 Java queue經常使用的方法如表格所示,對於表格中接口和表格中沒有的接口方法區別爲:隊列的操做不會由於隊列爲空拋出異常,而集合的操做是隊列爲空拋出異常。

3.1.2 Deque接口

package java.util;

public interface Deque<E> extends Queue<E> {
  //deque的操做方法
    void addFirst(E e);
    void addLast(E e);
    boolean offerFirst(E e);
    boolean offerLast(E e);
    E removeFirst();
    E removeLast();
    E pollFirst();
    E pollLast();
    E getFirst();
    E getLast();
    E peekFirst();
    E peekLast();

    boolean removeFirstOccurrence(Object o);
    boolean removeLastOccurrence(Object o);
    // *** Queue methods ***
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
    // 省略一堆stack接口方法和collection接口方法
}
複製代碼

和Queue中的方法同樣,方法名多了First或者Last,First結尾的方法即從頭部進行操做,Last即從尾部進行操做。

3.1.3 Queue,Deque的實現類

Java中關於Queue的實現主要用的是雙端隊列,畢竟操做更加方便自由,Queue的實現有PriorityQueue,Deque在java.util中主要有ArrayDeque和LinkedList兩個實現類,二者一個是基於數組的實現,一個是基於鏈表的實現。在以前LinkedList文章中也提到過其是一個雙向鏈表,在此基礎之上實現了Deque接口。

3.2 PriorityQueue

PriorityQueue是Java中惟一一個Queue接口的直接實現,如其名字所示,優先隊列,其內部支持按照必定的規則對內部元素進行排序。

3.2.1 PriorityQueue繼承關係

先看下PriorityQueue的繼承實現關係,可知其是Queue的實現類,主要使用方式是隊列的基本操做,而以前講到過Queue的基本原理,其核心是FIFO(First In First Out)原理。 Java中的PriorityQueue的實現也是符合隊列的方式,不過又略有不一樣,卻別就在於PriorityQueue的priority上,其是一個支持優先級的隊列,當使用了其priority的特性的時候,則並不是FIFO。

3.2.2 PriorityQueue的使用

案列1:

PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(20);queue.add(14);queue.add(21);queue.add(8);queue.add(9);
queue.add(11);queue.add(13);queue.add(10);queue.add(12);queue.add(15);
while (queue.size()>0){
    Integer poll = queue.poll();
    System.err.print(poll+"->");
}
複製代碼

上述代碼作的事爲往隊列中放入10個int值,而後使用Queue的poll()方法依次取出,最後結果爲每次取出來都是隊列中最小的值,說明 了PriorityQueue內部確實是有必定順序規則的。

案例2:

<pre style="margin: 0px; padding: 0px; max-width: 100%; background-image: none; background-position: // 必須實現Comparable方法,想String,數值自己便可比較
private static class Test implements Comparable<Test>{
    private int a;
    public Test(int a) {
        this.a = a;
    }
    public int getA() {
        return a;
    }
    public void setA(int a) {
        this.a = a;
    }
    @Override
    public String toString() {
        return "Test{" +
                "a=" + a +
                '}';
    }

    @Override
    public int compareTo(Test o) {
        return 0;
    }
}

public static void main(String[] args) {
    PriorityQueue<Test> queue = new PriorityQueue<>();
    queue.add(new Test(20));queue.add(new Test(14));queue.add(new Test(21));queue.add(new Test(8));queue.add(new Test(9));
    queue.add(new Test(11));queue.add(new Test(13));queue.add(new Test(10));queue.add(new Test(12));queue.add(new Test(15));
    while (queue.size()>0){
        Test poll = queue.poll();
        System.err.print(poll+"->");
    }
}
複製代碼

上述代碼重寫了compareTo方法都返回0,即不作優先級排序。發現咱們返回的順序爲Test{a=20}->Test{a=15}->Test{a=12}->Test{a=10}->Test{a=13}->Test{a=11}->Test{a=9}->Test{a=8}->Test{a=21}->Test{a=14},和放入的順序仍是不一樣,因此這兒須要注意在實現Comparable接口的時候必定要按照必定的規則進行優先級排序,關於爲何取出來的順序和放入的順序不一致後邊將從源碼來分析。

3.2.3 PriorityQueue組成

/**
 * 默認容量大小,數組大小
 */
private static final int DEFAULT_INITIAL_CAPACITY = 11;

/**
 * 存放元素的數組
 */
transient Object[] queue; // non-private to simplify nested class access

/**
 * 隊列中存放了多少元素
 */
private int size = 0;

/**
 * 自定義的比較規則,有該規則時優先使用,不然使用元素實現的Comparable接口方法。
 */
private final Comparator<? super E> comparator;

/**
 * 隊列修改次數,每次存取都算一次修改
 */
transient int modCount = 0; // non-private to simplify nested class access
複製代碼

能夠看到PriorityQueue的組成很簡單,主要記住一個存放元素的數組,和一個Comparator比較器便可。

3.2.4 PriorityQueue操做方法

offer方法

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    //i=size,當queue爲空的時候
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}
複製代碼

首先能夠看到當Queue中爲空的時候,第一次放入的元素直接放在了數組的第一位,那麼上邊案例二中第一個放入的20就在數組的第一位。而當queue中不爲空時,又使用siftUp(i, e)方法,傳入的參數是隊列中已有元素數量和即將要放入的新元素,如今就來看下究竟siftUp(i, e)作了什麼事。

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
複製代碼

還記得上邊提到PriorityQueue的組成,是一個存放元素的數組,和一個Comparator比較器。這兒是指當沒有Comparator是使用元素類實現compareTo方法進行比較。其含義爲優先使用自定義的比較規則Comparator,不然使用元素所在類實現的Comparable接口方法。

private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
      //爲何-1, 思考數組位置0,1,2。0是1和2的父節點
        int parent = (k - 1) >>> 1;
        //父節點
        Object e = queue[parent];
        //當傳入的新節點大於父節點則不作處理,不然兩者交換
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}
複製代碼

能夠看到,當PriorityQueue不爲空時插入一個新元素,會對其新元素進行堆排序處理(對於堆排序此處不作描述),這樣每次進來都會對該元素進行堆排序運算,這樣也就保證了Queue中第一個元素永遠是最小的(默認規則排序)。

pool方法

public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];
    //s = --size,即原來數組的最後一個元素
    E x = (E) queue[s];
    queue[s] = null;
    if (s != 0)
        siftDown(0, x);
    return result;
}
複製代碼

此處可知,當取出一個值進行了siftDown操做,傳入的參數爲索引0和隊列中的最後一個元素

private void siftDownComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>)x;
    int half = size >>> 1; // loop while a non-leaf
    while (k < half) {
        int child = (k << 1) + 1; // assume left child is least
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
          //c和right是parent的兩個子節點,找出小的那個成爲新的c。
            ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
            c = queue[child = right];
        if (key.compareTo((E) c) <= 0)
            break;
        //小的變成了新的父節點 
        queue[k] = c;
        k = child;
    }
    queue[k] = key;
}
複製代碼

當沒有Comparator比較器是採用的siftDown方法如上,由於索引0位置取出了,找索引0的子節點比它小的做爲新的父節點並在循環內遞歸。PriorityQueue是否是很簡單呢,其餘細節就再也不詳解,待諸君深刻。

3.3 ArrayDeque

ArrayDeque是Java中基於數組實現的雙端隊列,在Java中Deque的實現有LinkedList和ArrayDeque,正如它兩的名字就標誌了它們的不一樣,LinkedList是基於雙向鏈表實現的,而ArrayDeque是基於數組實現的。

3.3.1 ArrayDeque的繼承關係

可見ArrayDeque是Deque接口的實現,和LinkedList不一樣的是,LinkedList既是List接口也是Deque接口的實現。

3.3.2 ArrayDeque使用

案列

ArrayDeque<String> deque = new ArrayDeque<>();
deque.offer("aaa");
deque.offer("bbb");
deque.offer("ccc");
deque.offer("ddd");
//peek方法只獲取不移除
System.err.println(deque.peekFirst());
System.err.println(deque.peekLast());
複製代碼

案例二:

ArrayDeque<String> deque = new ArrayDeque<>();
deque.offerFirst("aaa");
deque.offerLast("bbb");
deque.offerFirst("ccc");
deque.offerLast("ddd");
String a;
while((a = deque.pollLast())!=null){
    System.err.print(a+"->");
}
複製代碼

上述程序最後獲得隊列中排列結果爲ccc,aaa,bbb,ddd因此循環使用pollLast(),結果ddd,bbb,aaa,ccc,圖示案列二的插入邏輯以下:

3.3.4 ArrayDeque內部組成

//具體存放元素的數組,數組大小必定是2的冪次方
transient Object[] elements; // non-private to 
//隊列頭索引
transient int head;
//隊列尾索引
transient int tail;
//默認的最小初始化容量,即傳入的容量小於8容量爲8,而默認容量是16
private static final int MIN_INITIAL_CAPACITY = 8;
複製代碼

3.3.5 數組elements長度

此處elements數組的長度永遠是2的冪次方,此處的實現方法和hashMap中基本同樣,即保證長度的二進制所有由1組成,而後再加1,則變成了100…,故必定是2的冪次方。

private static int calculateSize(int numElements) {
    int initialCapacity = MIN_INITIAL_CAPACITY;
    // Find the best power of two to hold elements.
    // Tests "<=" because arrays aren't kept full.
    if (numElements >= initialCapacity) {
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>> 1);
        initialCapacity |= (initialCapacity >>> 2);
        initialCapacity |= (initialCapacity >>> 4);
        initialCapacity |= (initialCapacity >>> 8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0) // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    return initialCapacity;
}
複製代碼

3.3.6 ArrayDeque實現機制

以下圖所示:

此處應將數組視做首尾相連的,最初頭部和尾部索引都是0,addLast方向往右,addFirst方向往左,因此數組中間多是空的,當頭指針和尾指針相遇的時候對數組進行擴容,並對元素位置進行調整。 源碼:

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}
複製代碼

注意下邊這行代碼,表示當head-1大於等於0時,head=head-1,不然head=elements.length - 1。

head = (head - 1) & (elements.length - 1)
複製代碼

換一種寫法就是下邊這樣,是否是就是上邊addFirst的指針移動方向?

head = head-1>=0?head-1:elements.length-1
複製代碼

這個就是位運算的神奇操做了,由於任何數與大於它的一個全是二進制1作&運算時等於它自身,如1010&1111 = 1010,此處不贅述。 再看addLast方法:

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
複製代碼

一樣的注意有一串神奇代碼。

(tail = (tail + 1) & (elements.length - 1))
複製代碼

該表達式等於tail = tail+1>element-1?0:tail+1,是否是很神奇的寫法,其原理是一個二進制數所有由1組成和一個大於它的數作&運算結果爲0,如10000&1111 = 0。poll方法和add方法邏輯是相反的,此處就再也不贅述,諸君共求之!

4. Set

若是說List對集合加了有序性的化,那麼Set就是對集合加上了惟一性。

4.1 Set接口

java中的Set接口和Colletion是徹底同樣的定義。

package java.util;
public interface Set<E> extends Collection<E> {
    // Query Operations
    int size();
    boolean isEmpty();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    // Modification Operations
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean retainAll(Collection<?> c);
    boolean removeAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
  //此處和Collection接口由區別
   Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT);
    }
}
複製代碼

4.2 HashSet

Java中的HashSet如其名字所示,其是一種Hash實現的集合,使用的底層結構是HashMap。

4.2.1 HashSet繼承關係

4.2.2 HashSet源碼

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;
    private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
    public int size() {
        return map.size();
    }
    public boolean isEmpty() {
        return map.isEmpty();
    }
    public boolean contains(Object o) {
        return map.containsKey(o);
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

    public void clear() {
        map.clear();
    }
}
複製代碼

能夠看到HashSet內部實際上是一個HashMap。

4.2.3 HashSet是如何保證不重複的呢?

可見HashSet的add方法,插入的值會做爲HashMap的key,因此是HashMap保證了不重複。map的put方法新增一個原來不存在的值會返回null,若是原來存在的話會返回原來存在的值。關於HashMap是如何實現的,見後續!

4.3 LinkedHashSet

LinkedHashSet用的也比較少,其也是基於Set的實現。

4.3.1 LinkedHashSet繼承關係

和HashSet同樣,其也是Set接口的實現類,而且是HashSet的子類。

4.3.2 LinkedHashSet源碼

package java.util;

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {

    private static final long serialVersionUID = -2851667679971038690L;

    public LinkedHashSet(int initialCapacity, float loadFactor) {
      //調用HashSet的構造方法
        super(initialCapacity, loadFactor, true);
    }
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
    public LinkedHashSet() {
        super(16, .75f, true);
    }
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }
    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | 
Spliterator.ORDERED);
    }
}
複製代碼

其操做方法和HashSet徹底同樣,那麼兩者區別是什麼呢?1.首先LinkedHashSet是HashSet的子類.2.LinkedHashSet中用於存儲值的實現LinkedHashMap,而HashSet使用的是HashMap。LinkedHashSet中調用的父類構造器,能夠看到其實列是一個LinkedHashMap。

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
複製代碼

LinkedHashSet的實現很簡單,更深刻的瞭解須要去看LinkedHashMap的實現,對LinkedHashMap的解析將單獨提出。

五、Map

Map是一種鍵值對的結構,就是常說的Key-Value結構,一個Map就是不少這樣K-V鍵值對組成的,一個K-V結構咱們將其稱做Entry,在Java裏,Map是用的很是多的一種數據結構。上圖展現了Map家族最基礎的一個結構(只是指java.util中)。整理了2021年Java面試題

5.1 Map接口

package java.util;

import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.io.Serializable;
public interface Map<K,V> {
    // Query Operations
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    // Modification Operations
    V put(K key, V value);
    V remove(Object key);
    // Bulk Operations
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    interface Entry<K,V> {
        K getKey();
        V getValue();
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }
        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }
    }

    // Comparison and hashing
    boolean equals(Object o);
    int hashCode();
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }
    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }
    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }

            // ise thrown from function is not a cme.
            v = function.apply(k, v);

            try {
                entry.setValue(v);
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
        }
    }
    default V putIfAbsent(K key, V value) {
        V v = get(key);
        if (v == null) {
            v = put(key, value);
        }

        return v;
    }
    default boolean remove(Object key, Object value) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, value) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        remove(key);
        return true;
    }

    default boolean replace(K key, V oldValue, V newValue) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, oldValue) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        put(key, newValue);
        return true;
    }

    default V replace(K key, V value) {
        V curValue;
        if (((curValue = get(key)) != null) || containsKey(key)) {
            curValue = put(key, value);
        }
        return curValue;
    }
    default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        Objects.requireNonNull(mappingFunction);
        V v;
        if ((v = get(key)) == null) {
            V newValue;
            if ((newValue = mappingFunction.apply(key)) != null) {
                put(key, newValue);
                return newValue;
            }
        }

        return v;
    }

    default V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue;
        if ((oldValue = get(key)) != null) {
            V newValue = remappingFunction.apply(key, oldValue);
            if (newValue != null) {
                put(key, newValue);
                return newValue;
            } else {
                remove(key);
                return null;
            }
        } else {
            return null;
        }
    }

    default V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue = get(key);

        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue == null) {
            // delete mapping
            if (oldValue != null || containsKey(key)) {
                // something to remove
                remove(key);
                return null;
            } else {
                // nothing to do. Leave things as they were.
                return null;
            }
        } else {
            // add or replace old mapping
            put(key, newValue);
            return newValue;
        }
    }
    default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        Objects.requireNonNull(value);
        V oldValue = get(key);
        V newValue = (oldValue == null) ? value :
                   remappingFunction.apply(oldValue, value);
        if(newValue == null) {
            remove(key);
        } else {
            put(key, newValue);
        }
        return newValue;
    }
}
複製代碼

Map接口自己就是一個頂層接口,由一堆Map自身接口方法和一個Entry接口組成,Entry接口定義了主要是關於Key-Value自身的一些操做,Map接口定義的是一些屬性和關於屬性查找修改的一些接口方法。

5.2 HashMap

HashMap是Java中最經常使用K-V容器,採用了哈希的方式進行實現,HashMap中存儲的是一個又一個Key-Value的鍵值對,咱們將其稱做Entry,HashMap對Entry進行了擴展(稱做Node),使其成爲鏈表或者樹的結構使其存儲在HashMap的容器裏(是一個數組)。

5.2.1 HashMap繼承關係

5.2.2 HashMap存儲的數據

Map接口中有一個Entry接口,在HashMap中對其進行了實現,Entry的實現是HashMap存放的數據的類型。其中Entry在HashMap的實現是Node,Node是一個單鏈表的結構,TreeNode是其子類,是一個紅黑樹的類型,其繼承結構圖以下:

HashMap存放數據的數據是什麼呢?代碼中存放數據的容器以下:

transient Node<K,V>[] table;
複製代碼

說明了該容器中是一個又一個node組成,而node有三種實現,因此hashMap中存放的node的形式既能夠是Node也能夠是TreeNode。

5.2.3 HashMap的組成

有了上邊的概念以後來看一下HashMap裏有哪些組成吧!

//是hashMap的最小容量16,容量就是數組的大小也就是變量,transient Node<K,V>[] table。
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大數量,該數組最大值爲2^31一次方。
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默認的加載因子,若是構造的時候不傳則爲0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //一個位置裏存放的節點轉化成樹的閾值,也就是8,好比數組裏有一個node,這個 
      // node鏈表的長度達到該值纔會轉化爲紅黑樹。
    static final int TREEIFY_THRESHOLD = 8;
    //當一個反樹化的閾值,當這個node長度減小到該值就會從樹轉化成鏈表
    static final int UNTREEIFY_THRESHOLD = 6;
    //知足節點變成樹的另外一個條件,就是存放node的數組長度要達到64
    static final int MIN_TREEIFY_CAPACITY = 64;
    //具體存放數據的數組
    transient Node<K,V>[] table;
    //entrySet,一個存放k-v緩衝區
    transient Set<Map.Entry<K,V>> entrySet;
    //size是指hashMap中存放了多少個鍵值對
    transient int size;
    //對map的修改次數
    transient int modCount;
    //加載因子
    final float loadFactor;
複製代碼

這兒要說兩個概念,table是指的存放數據的數組,bin是指的table中某一個位置的node,一個node能夠理解成一批/一盒數據。

5.2.4 HashMap中的構造函數*

//只有容量,initialCapacity
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0) // 容量不能爲負數
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //當容量大於2^31就取最大值1<<31; 
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //當前數組table的大小,必定是是2的冪次方
    // tableSizeFor保證了數組必定是是2的冪次方,是大於initialCapacity最結進的值。
    this.threshold = tableSizeFor(initialCapacity);
}
複製代碼

tableSizeFor()方法保證了數組大小必定是是2的冪次方,是如何實現的呢?

static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製代碼

該方法將一個二進制數第一位1後邊的數字所有變成1,而後再加1,這樣這個二進制數就必定是100…這樣的形式。此處實如今ArrayDeque的實現中也用到了相似的方法來保證數組長度必定是2的冪次方。

5.2.5 put方法

開發人員使用的put方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
複製代碼

真正HashMap內部使用的put值的方法:

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;
    //當hash到的位置,該位置爲null的時候,存放一個新node放入 
    // 這兒p賦值成了table該位置的node值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //該位置第一個就是查找到的值,將p賦給e
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //若是是紅黑樹,調用紅黑樹的putTreeVal方法 
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
          //是鏈表,遍歷,注意e = p.next這個一直將下一節點賦值給e,直到尾部,注意開頭是++binCount
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //當鏈表長度大於等於7,插入第8位,樹化
                    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;
}
複製代碼

5.2.6 查找方法

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 &&
        //這一行是找到要查詢的Key在table中的位置,table是存放HashMap中每個Node的數組。
        (first = tab[(n - 1) & hash]) != null) {
        //Node多是一個鏈表或者樹,先判斷根節點是不是要查詢的key,就是根節點,方便後續遍歷Node寫法而且
        //對於只有根節點的Node直接判斷
        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;
            }
            //遍歷鏈表,當鏈表後續爲null則推出循環
            while ((e = e.next) != null);
        }
    }
    return null;
}
複製代碼

5.3 HashTable

和HashMap不一樣,HashTable的實現方式徹底不一樣,這點從兩者的類繼承關係就能夠看出端倪來,HashTable和HashMap雖然都實現了Map接口,可是HashTable繼承了DIctionary抽象類,而HashMap繼承了AbstractMap抽象類。

5.3.1 HashTable的類繼承關係圖

HashTable

HashMap

5.3.2 Dictionary接口

public abstract
class Dictionary<K,V> {
    public Dictionary() {
    }
    public abstract int size();
    public abstract boolean isEmpty();
    public abstract Enumeration<K> keys();
    public abstract Enumeration<V> elements();
    public abstract V get(Object key);
    public abstract V put(K key, V value);

    public abstract V remove(Object key);
}
複製代碼

Dictionary類中有這樣一行註釋,當key爲null時會拋出空指針NullPointerException,這也說明了HashTabel是不容許Key爲null的。

//throws NullPointerException if the {@code key} is {@code null}.</pre>
複製代碼

5.3.3 HashTable組成

/**
 * The hash table data.
 * 真正存放數據的數組
 */
private transient Entry<?,?>[] table;

/**
 * The total number of entries in the hash table.
 */
private transient int count;

/**
 * The table is rehashed when its size exceeds this threshold. (The
 * value of this field is (int)(capacity * loadFactor).)
 * 從新hash的閾值
 * @serial
 */
private int threshold;

/**
 * The load factor for the hashtable.
 * @serial
 */
private float loadFactor;
複製代碼

 HashTable中的元素存在Entry[] table中,是一個Entry數組,Entry是存放的節點,每個Entry是一個鏈表。

5.3.4 HashTable中的Entry

final int hash;
final K key;
V value;
Entry<K,V> next;
複製代碼

知道Entry是一個單鏈表便可,和HashMap中的Node結構相同,可是HashMap中還有Node的子類TreeNode。

5.3.5 put方法

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //在數組中的位置 0x7fffffff 是31位二進制1 
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
      //若是遍歷鏈表找到了則替換舊值並返回
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
複製代碼

本質上就是先hash求索引,遍歷該索引Entry鏈表,若是找到hash值和key都和put的key同樣的時候就替換舊值,不然使用addEntry方法添加新值進入table,由於添加新元素就涉及到修改元素大小,還可能須要擴容等,具體看下邊的addEntry方法可知。

private void addEntry(int hash, K key, V value, int index) {
    Entry<?,?> tab[] = table;
    //若是擴容須要從新計算hash,因此index和table都會被修改
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }
    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //插入新元素
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    modCount++;
}
複製代碼
tab[index] = new Entry<>(hash, key, value, e);
複製代碼

這行代碼是真正插入新元素的方法,採用頭插法,單鏈表通常都用頭插法(快)。

5.3.6 get方法

@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return (V)e.value;
        }
    }
    return null;
}
複製代碼

get方法就簡單不少就是hash,找到索引,遍歷鏈表找到對應的value,沒有則返回null。相比諸君都已經看到,HashTable中方法是用synchronized修飾的,因此其操做是線程安全的,可是效率會受影響。

相關文章
相關標籤/搜索