Java基礎系列——深入JDK11中的集合(28)

ArrayList的實現原理

概述

一上來,先來看看源碼中的這一段註釋,我們可以從中提取到一些關鍵信息:

Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate t	he size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.) 

從這段註釋中,我們可以得知 ArrayList 是一個動態數組,實現了 List 接口以及 list相關的所有方法,它允許所有元素的插入,包括 null。另外,ArrayList 和Vector 除了線程不同步之外,大致相等。

屬性

//默認容量的大小
private static final int DEFAULT_CAPACITY = 10;
//空數組常量
private static final Object[] EMPTY_ELEMENTDATA = {};
//默認的空數組常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存放元素的數組,從這可以發現 ArrayList 的底層實現就是一個 Object數組
transient Object[] elementData;
//數組中包含的元素個數
private int size;
//數組的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArrayList 的屬性非常少,就只有這些。其中最重要的莫過於 elementData 了,ArrayList所有的方法都是建立在 elementData 之上。

方法

構造方法

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

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

從構造方法中我們可以看見,默認情況下,elementData 是一個大小爲 0 的空數組,當我們指定了初始大小的時候,elementData 的初始大小就變成了我們所指定的初始大小了。

get方法

public E get(int index) {
	Objects.checkIndex(index, size);
	return elementData(index);
}
public static  int checkIndex(int index, int length) {
	return Preconditions.checkIndex(index, length, null);
}
public static <X extends RuntimeException>  int checkIndex(int index, int length,BiFunction<String, List<Integer>, X> oobef) {
	if (index < 0 || index >= length)
		throw outOfBoundsCheckIndex(oobef, index, length);
	return index;
}
private static RuntimeException outOfBoundsCheckIndex(BiFunction<String, List<Integer>, ? extends RuntimeException> oobe,int index, int length) {
	return outOfBounds(oobe, "checkIndex", index, length);
}
private static RuntimeException outOfBounds(BiFunction<String, List<Integer>, ? extends RuntimeException> oobef,String checkKind,Integer... args) {
	List<Integer> largs = List.of(args);
	RuntimeException e = oobef == null ? null : oobef.apply(checkKind, largs);
    return e == null ? new IndexOutOfBoundsException(outOfBoundsMessage(checkKind, largs)) : e;
}

因爲 ArrayList 是採用數組結構來存儲的,所以它的 get 方法非常簡單,先是判斷一下有沒有越界,之後就可以直接通過數組下標來獲取元素了,所以 get 的時間複雜度是 O(1)。

add方法

// 添加方法
public boolean add(E e) {
    // protected transient int modCount = 0;
	modCount++;
    /**
     * 調用add方法
     * e : 要添加的元素
     * elementData:元素數組,如果沒有傳遞參數的時候,那麼就是默認最小的那個長度
     * size : 當前數組中元素的個數
     */
     add(e, elementData, size);
     return true;
}
/**
 * e : 要添加的元素
 * elementData:元素數組
 * s : 數組中的個數
 */
private void add(E e, Object[] elementData, int s) {
    // 如果當前數組中的個數等於數組的長度,那麼就意味着需要擴容了
	if (s == elementData.length)
        // 擴容方法
		elementData = grow();
    // 元素進行賦值
	elementData[s] = e;
    // 數量加一
	size = s + 1;
}
// 擴容方法
private Object[] grow() {
	return grow(size + 1);
}
// 擴容方法,使用數組進行復制操作
private Object[] grow(int minCapacity) {
    // 進行數組複製操作 : 底層依舊使用 System.arrayCopy方法進行操作。
    return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
   	// 獲取 以前的數組長度
	// overflow-conscious code
    int oldCapacity = elementData.length;
    // 進行縮小一倍,然後加上以前的數組長度
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 相減,判斷是否小於等於0
    if (newCapacity - minCapacity <= 0) {
        // 如果小於等於0,那麼判斷 當前數組是否是一個空數組
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // 如果是空數組,那麼就 獲取 默認大小(10),和當前的最小容量 中最大的那個,也就是新容量
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        // 返回 新的最小容量
        return minCapacity;
    }
    // 判斷是否 新容量 是否 小於 最大的容量,如果小於,那麼返回新容量,否則 返回最大的數組容量(MAX_ARRAY_SIZE)
    return (newCapacity - MAX_ARRAY_SIZE <= 0)
        ? newCapacity
        : hugeCapacity(minCapacity);
}
// 獲取最大的數組長度
private static int hugeCapacity(int minCapacity) {
    // 如果最小長度小於0,那麼拋出錯誤
	if (minCapacity < 0) // overflow
		throw new OutOfMemoryError();
    // 判斷是否大於最大的值,如果大於最大的值,那麼返回最大的值,否則返回最大的數組長度
	return (minCapacity > MAX_ARRAY_SIZE)? Integer.MAX_VALUE: MAX_ARRAY_SIZE;
}
// 在指定的位置添加元素
public void add(int index, E element) {
	rangeCheckForAdd(index);
	modCount++;
	final int s;
	Object[] elementData;
    // 是否需要進行擴容
	if ((s = size) == (elementData = this.elementData).length)
		elementData = grow();
    
    //調用一個 native 的複製方法,把 index 位置開始的元素都往後挪一位
    System.arraycopy(elementData, index,elementData, index + 1,s - index);
    // 進行插入操作
    elementData[index] = element;
    size = s + 1;
}

ArrayList 的 add 方法也很好理解,在插入元素之前,它會先檢查是否需要擴容,然後再把元素添加到數組中最後一個元素的後面。在 newCapacity方法中(jdk1.8是ensureCapacityInternal方法),我們可以看見,如果當 elementData 爲空數組時,它會使用默認的大小去擴容。所以說,通過無參構造方法來創建 ArrayList 時,它的大小其實是爲 0 的,只有在使用到的時候,纔會通過 newCapacity方法去創建一個大小爲 10 的數組。第一個 add 方法的複雜度爲 O(1),雖然有時候會涉及到擴容的操作,但是擴容的次數是非常少的,所以這一部分的時間可以忽略不計。如果使用的是帶指定下標的 add方法,則複雜度爲 O(n),因爲涉及到對數組中元素的移動,這一操作是非常耗時的。

set 方法

public E set(int index, E element) {
    // 檢查索引是否超過了數組大小
	Objects.checkIndex(index, size);
	E oldValue = elementData(index);
	elementData[index] = element;
	return oldValue;
}

set 方法的作用是把下標爲 index 的元素替換成 element,跟 get 非常類似,所以就不在贅述了,時間複雜度度爲 O(1)。

remove方法

public E remove(int index) {
	Objects.checkIndex(index, size);
	final Object[] es = elementData;

	@SuppressWarnings("unchecked") E oldValue = (E) es[index];
	fastRemove(es, index);

	return oldValue;
}
private void fastRemove(Object[] es, int i) {
	modCount++;
	final int newSize;
	if ((newSize = size - 1) > i)
		System.arraycopy(es, i + 1, es, i, newSize - i);
	es[size = newSize] = null;
}

remove 方法與 add 帶指定下標的方法非常類似,也是調用系統的 arraycopy 方法來移動元素,時間複雜度爲 O(n)。

size方法

public int size() {
     return size;
}

size 方法非常簡單,它是直接返回 size 的值,也就是返回數組中元素的個數,時間複雜度爲 O(1)。這裏要注意一下,返回的並不是數組的實際大小。

indexOf 方法和 lastIndexOf

public int indexOf(Object o) {
	return indexOfRange(o, 0, size);
}
int indexOfRange(Object o, int start, int end) {
	Object[] es = elementData;
	if (o == null) {
		for (int i = start; i < end; i++) {
			if (es[i] == null) {
				return i;
			}
		}
	} else {
		for (int i = start; i < end; i++) {
			if (o.equals(es[i])) {
				return i;
			}
		}
	}
	return -1;
}
public int lastIndexOf(Object o) {
	return lastIndexOfRange(o, 0, size);
}
int lastIndexOfRange(Object o, int start, int end) {
	Object[] es = elementData;
	if (o == null) {
		for (int i = end - 1; i >= start; i--) {
			if (es[i] == null) {
				return i;
            }
        }
	} else {
		for (int i = end - 1; i >= start; i--) {
			if (o.equals(es[i])) {
				return i;
			}
		}
	}
	return -1;
}

indexOf 方法的作用是返回第一個等於給定元素的值的下標。它是通過遍歷比較數組中每個元素的值來查找的,所以它的時間複雜度是 O(n)。

lastIndexOf 的原理跟 indexOf 一樣,而它僅僅是從後往前找起罷了。

Vector

Vector很多方法都跟 ArrayList 一樣,只是多加了個 synchronized 來保證線程安全。

Vector 比 ArrayList 多了一個屬性:

protected int capacityIncrement;

這個屬性是在擴容的時候用到的,它表示每次擴容只擴 capacityIncrement 個空間就足夠了。該屬性可以通過構造方法給它賦值。先來看一下構造方法:

public Vector(int initialCapacity, int capacityIncrement) {
	super();
	if (initialCapacity < 0)
		throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
	this.elementData = new Object[initialCapacity];
	this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
	this(initialCapacity, 0);
}

public Vector() {
    this(10);
}

從構造方法中,我們可以看出 Vector 的默認大小也是 10,而且它在初始化的時候就已經創建了數組了,這點跟 ArrayList 不一樣。再來看一下 grow 方法:

private Object[] grow(int minCapacity) {
	return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}

private Object[] grow() {
	return grow(elementCount + 1);
}

private int newCapacity(int minCapacity) {
	// overflow-conscious code
	int oldCapacity = elementData.length;
	int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
	if (newCapacity - minCapacity <= 0) {
		if (minCapacity < 0) // overflow  
            throw new OutOfMemoryError();
		return minCapacity;
	}
    return (newCapacity - MAX_ARRAY_SIZE <= 0)? newCapacity: hugeCapacity(minCapacity);
}

從 grow 方法中我們可以發現,newCapacity 默認情況下是兩倍的 oldCapacity,而當指定了 capacityIncrement 的值之後,newCapacity 變成了 oldCapacity + capacityIncrement。

總結

  • ArrayList 創建時的大小爲 0;當加入第一個元素時,進行第一次擴容時,默認容量大小爲 10。
  • ArrayList 每次擴容都以當前數組大小的 1.5 倍去擴容。
  • Vector 創建時的默認大小爲 10。
  • Vector 每次擴容都以當前數組大小的 2 倍去擴容。當指定了 capacityIncrement 之後,每次擴容僅在原先基礎上增加 capacityIncrement 個單位空間。
  • ArrayList 和 Vector 的 add、get、size 方法的複雜度都爲 O(1),remove 方法的複雜度爲 O(n)。
  • ArrayList 是非線程安全的,Vector 是線程安全的。

LinkedList 的實現原理

概述

先來看看源碼中的這一段註釋,我們先嚐試從中提取一些信息:

Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).
All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index.
Note that this implementation is not synchronized. If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list

從這段註釋中,我們可以得知 LinkedList 是通過一個雙向鏈表來實現的,它允許插入所有元素,包括 null,同時,它是線程不同步的。

如果對雙向鏈表這個 數據結構很熟悉的話,學習 LinkedList 就沒什麼難度了。下面是雙向鏈表的結構

雙向鏈表每個結點除了數據域之外,還有一個前指針和後指針,分別指向前驅結點和後繼結點(如果有前驅/後繼的話)。另外,雙向鏈表還有一個 first 指針,指向頭節點,和 last 指針,指向尾節點。

屬性

     // 鏈表的節點個數
     transient int size = 0;

    /**
     * Pointer to first node.
     * 指向頭節點的指針
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * 指向尾節點的指針
     */
    transient Node<E> last;

LinkedList 的屬性非常少,就只有這些。通過這三個屬性,其實我們大概也可以猜測出它是怎麼實現的了。

方法

節點結構

Node 是在 LinkedList 裏定義的一個靜態內部類,它表示鏈表每個節點的結構,包括一個數據域 item,一個後置指針 next,一個前置指針 prev。

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 有頭指針和尾指針,所以在表頭或表尾進行插入元素只需要 O(1) 的時間,而在指定位置插入元素則需要先遍歷一下鏈表,所以複雜度爲 O(n)。

在表頭添加元素的過程如下:

當向表頭插入一個節點時,很顯然當前節點的前驅一定爲 null,而後繼結點是 first指針指向的節點,當然還要修改 first 指針指向新的頭節點。除此之外,原來的頭節點變成了第二個節點,所以還要修改原來頭節點的前驅指針,使它指向表頭節點,源碼的實現如下:

private void linkFirst(E e) {
	final Node<E> f = first;
	//當前節點的前驅指向 null,後繼指針原來的頭節點
	final Node<E> newNode = new Node<>(null, e, f);
    //頭指針指向新的頭節點
	first = newNode;
    // 如果原來有頭節點,則更新原來節點的前驅指針,否則更新尾指針
	if (f == null)
		last = newNode;
	else
		f.prev = newNode;
	size++;
	modCount++;
}

在表尾添加元素跟在表頭添加元素大同小異,如圖所示: 當向表尾插入一個節點時,很顯然當前節點的後繼一定爲 null,而前驅結點是 last指針指向的節點,然後還要修改 last 指針指向新的尾節點。此外,還要修改原來尾節點的後繼指針,使它指向新的尾節點,源碼的實現如下:

void linkLast(E e) {
    final Node<E> l = last;
    // 當前節點的前驅指向尾節點,後繼指向 null
    final Node<E> newNode = new Node<>(l, e, null);
    //尾指針指向新的尾節點
    last = newNode;
    // 如果原來有尾節點,則更新原來節點的後繼指針,否則更新頭指針
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

最後,在指定節點之前插入,如圖所示: 當向指定節點之前插入一個節點時,當前節點的後繼爲指定節點,而前驅結點爲指定節點的前驅節點。此外,還要修改前驅節點的後繼爲當前節點,以及後繼節點的前驅爲當前節點,源碼的實現如下:

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 指定節點的前驅
    final Node<E> pred = succ.prev;
    // 當前節點的前驅爲指點節點的前驅,後繼爲指定的節點
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 更新指定節點的前驅爲當前節點
    succ.prev = newNode;
    //更新前驅節點的後繼
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

刪除元素

刪除操作與添加操作大同小異,例如刪除指定節點的過程如下圖所示,需要把當前節點的前驅節點的後繼修改爲當前節點的後繼,以及當前節點的後繼結點的前驅修改爲當前節點的前驅: 刪除頭節點和尾節點跟刪除指定節點非常類似,就不一一介紹了,源碼如下

//刪除表頭節點,返回表頭元素的值
private E unlinkFirst(Node<E> f) {
	// assert f == first && f != null;
	final E element = f.item;
	final Node<E> next = f.next;
	f.item = null;
	f.next = null; // help GC
	first = next; // //頭指針指向後一個節點
	if (next == null)
		last = null;
	else
		next.prev = null; // //新頭節點的前驅爲 null
	size--;
	modCount++;
	return element;
}
// 刪除表尾節點,返回表尾元素的值
private E unlinkLast(Node<E> l) {
	// assert l == last && l != null;
	final E element = l.item;
	final Node<E> prev = l.prev;
	l.item = null;
	l.prev = null; // help GC
	last = prev; // 尾指針指向前一個節點
	if (prev == null)
		first = null;
	else
		prev.next = null; // 新尾節點的後繼爲 null
	size--;
	modCount++;
	return element;
}
// 刪除指定節點,返回指定元素的值
E unlink(Node<E> x) {
	// assert x != null;
	final E element = x.item;
	final Node<E> next = x.next; // 當前節點的後繼
	final Node<E> prev = x.prev; // 當前節點的前驅

	if (prev == null) {
		first = next;
	} else {
		prev.next = next;  // 更新前驅節點的後繼爲當前節點的後繼
		x.prev = null;
	}

    if (next == null) {
		last = prev;
	} else { 
		next.prev = prev; // 更新後繼節點的前驅爲當前節點的前驅
		x.next = null;
	}
	x.item = null;
    size--;
    modCount++;
    return element;
}

獲取元素

// 獲取表頭元素 
public E getFirst() {
	final Node<E> f = first;
	if (f == null)
		throw new NoSuchElementException();
	return f.item;
}
// 獲取表尾元素
public E getLast() {
	final Node<E> l = last;
	if (l == null)
		throw new NoSuchElementException();
	return l.item;
}
// 獲取指定下標的元素
Node<E> node(int index) {
	// assert isElementIndex(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 的,LinkedList是在這些基礎的方法進行操作的,下面就來看看我們可以調用的方法有哪些。

// 刪除表頭元素
public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
// 刪除表尾元素
public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
// 插入新的表頭節點
public void addFirst(E e) {
        linkFirst(e);
    }
// 插入新的表尾節點
public void addLast(E e) {
        linkLast(e);
    }
// 鏈表的大小
public int size() {
        return size;
    }
// 添加元素到表尾
public boolean add(E e) {
        linkLast(e);
        return true;
    }
// 刪除指定元素
public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
// 獲取指定下標的元素
public E get(int index) {
        checkElementIndex(index); // 先檢查是否越界
        return node(index).item;
    }
// 替換指定下標的值
public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }
// 在指定位置插入節點
public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
// 刪除指定下標的節點
public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
// 獲取表頭節點的值,表頭爲空返回 null
public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
// 獲取表頭節點的值,表頭爲空拋出異常
public E element() {
        return getFirst();
    }
// 獲取表頭節點的值,並刪除表頭節點,表頭爲空返回 null
public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
// 添加元素到表頭
public void push(E e) {
        addFirst(e);
    }
// 刪除表頭元素
public E pop() {
        return removeFirst();
    }

總結

  • LinkedList 的底層結構是一個帶頭/尾指針的雙向鏈表,可以快速的對頭/尾節點進行操作。
  • 相比數組,鏈表的特點就是在指定位置插入和刪除元素的效率較高,但是查找的效率就不如數組那麼高了

HashMap 的實現原理

概述

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. 
(The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) 
This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

翻譯一下大概就是在說,這個哈希表是基於 Map 接口的實現的,它允許 null 值和null 鍵,它不是線程同步的,同時也不保證有序。

This implementation provides constant-time performance for the basic operations (get and put), assuming the hash function disperses the elements properly among the buckets.
Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). 
Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.

An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. 
The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. 
The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. 
When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.

講的是 Map 的這種實現方式爲 get(取)和 put(存)帶來了比較好的性能。但是如果涉及到大量的遍歷操作的話,就儘量不要把 capacity 設置得太高(或 load factor 設置得太低),否則會嚴重降低遍歷的效率。影響 HashMap 性能的兩個重要參數:“initial capacity”(初始化容量)和”load factor“(負載因子)。簡單來說,容量就是哈希表桶的個數,負載因子就是鍵值對個數與哈希表長度的一個比值,當比值超過負載因子之後,HashMap 就會進行 rehash 操作來進行擴容。

HashMap 的大致結構如下圖所示,其中哈希表是一個數組,我們經常把數組中的每一個節點稱爲一個桶,哈希表中的每個節點都用來存儲一個鍵值對。在插入元素時,如果發生衝突(即多個鍵值對映射到同一個桶上)的話,就會通過鏈表的形式來解決衝突。因爲一個桶上可能存在多個鍵值對,所以在查找的時候,會先通過 key 的哈希值先定位到桶,再遍歷桶上的所有鍵值對,找出 key 相等的鍵值對,從而來獲取 value。

屬性

     // 默認的初始容量爲16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
     // 最大的容量上限爲 2^30
    static final int MAXIMUM_CAPACITY = 1 << 30;
     // 默認的加載因子爲 0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 變成樹型結構的臨界值爲 8
    static final int TREEIFY_THRESHOLD = 8;

    // 恢復鏈式結構的臨界值爲 6
    static final int UNTREEIFY_THRESHOLD = 6;

    // 當哈希表的大小超過這個閾值,纔會把鏈式結構轉化成樹型結構,否則僅採取擴容來嘗試減少衝突
    static final int MIN_TREEIFY_CAPACITY = 64;
     // 哈希表
     transient Node<K,V>[] table;
     // 哈希表中鍵值對的個數
     transient int size;

    // 哈希表被修改的次數
    transient int modCount;

    // 它是通過 capacity*load factor 計算出來的,當 size 到達這個值時,就會進行擴容操作
    int threshold;

    // 加載因子
    final float loadFactor;

下面是 Node 類的定義,它是 HashMap 中的一個靜態內部類,哈希表中的每一個節點都是 Node 類型。我們可以看到,Node 類中有 4 個屬性,其中除了 key 和 value 之外,還有 hash 和 next 兩個屬性。hash 是用來存儲 key 的哈希值的,next是在構建鏈表時用來指向後繼節點的。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

方法

get方法

// get 方法主要調用的是 getNode 方法,所以重點要看 getNode 方法的實現
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    	// 如果哈希表不爲空 && key 對應的桶上不爲空
        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) {
                // 如果當前的桶是採用紅黑樹處理衝突,則調用紅黑樹的 get 方法去獲取節點
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 不是紅黑樹的話,那就是傳統的鏈式結構了,通過循環的方法判斷鏈中是否存在該 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. 通過 hash 值獲取該 key 映射到的桶。
  2. 桶上的 key 就是要查找的 key,則直接命中。
  3. 桶上的 key 不是要查找的 key,則查看後續節點:
    1. 如果後續節點是樹節點,通過調用樹的方法查找該 key。
    2. 如果後續節點是鏈式節點,則通過循環遍歷鏈查找該 key。

put 方法

// put 方法的具體實現也是在 putVal 方法中,所以我們重點看下面的 putVal 方法
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
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;
            // 如果桶上節點的 key 與當前 key 重複,那你就是我要找的節點
            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 {
                // 採用循環遍歷的方式,判斷鏈中是否有重複的 key
                for (int binCount = 0; ; ++binCount) {
                    // 到了鏈尾還沒找到重複的 key,則說明 HashMap 沒有包含該鍵
                    if ((e = p.next) == null) {
                        // 創建一個新節點插入到尾部
                        p.next = newNode(hash, key, value, null);
                        // 如果鏈的長度大於 TREEIFY_THRESHOLD 這個臨界值,則把鏈變爲紅黑樹
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 找到了重複的 key
                    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 值計算出 key 映射到哪個桶。
  2. 如果桶上沒有碰撞衝突,則直接插入。
  3. 如果出現碰撞衝突了,則需要處理衝突:
    1. 如果該桶使用紅黑樹處理衝突,則調用紅黑樹的方法插入。
    2. 否則採用傳統的鏈式方法插入。如果鏈的長度到達臨界值,則把鏈轉變爲紅黑樹。
  4. 如果桶中存在重複的鍵,則爲該鍵替換新值。
  5. 如果 size 大於閾值,則進行擴容。

remove 方法

理解了 put 方法之後,remove 已經沒什麼難度了,所以重複的內容就不再做詳細介紹了。

// remove 方法的具體實現在 removeNode 方法中,所以我們重點看下面的 removeNode 方法
public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 如果當前 key 映射到的桶不爲空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            // 如果桶上的節點就是要找的 key,則直接命中
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                // 如果是以紅黑樹處理衝突,則構建一個樹節點
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    // 如果是以鏈式的方式處理衝突,則通過遍歷鏈表來尋找節點
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 比對找到的 key 的 value 跟要刪除的是否匹配
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                // 通過調用紅黑樹的方法來刪除節點
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                // 使用鏈表的操作來刪除節點
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

hash 方法

在get 方法和put方法中都需要先計算 key映射到哪個桶上,然後才進行之後的操作,計算的主要代碼如下:

(n - 1) & hash

上面代碼中的 n 指的是哈希表的大小,hash 指的是 key 的哈希值,hash 是通過下面這個方法計算出來的,採用了二次哈希的方式,其中 key 的 hashCode 方法是一個native 方法:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

這個 hash 方法先通過 key 的 hashCode 方法獲取一個哈希值,再拿這個哈希值與它的高 16 位的哈希值做一個異或操作來得到最後的哈希值,計算過程可以參考下圖。爲啥要這樣做呢?註釋中是這樣解釋的:如果當 n 很小,假設爲 64 的話,那麼 n-1即爲 63(0x111111),這樣的值跟 hashCode()直接做與操作,實際上只使用了哈希值的後 6 位。如果當哈希值的高位變化很大,低位變化很小,這樣就很容易造成衝突了,所以這裏把高低位都利用起來,從而解決了這個問題。 正是因爲與的這個操作,決定了 HashMap 的大小隻能是 2 的冪次方,想一想,如果不是2的冪次方,會發生什麼事情?即使你在創建HashMap的時候指定了初始大小,HashMap 在構建的時候也會調用下面這個方法來調整大小:

static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

這個方法的作用看起來可能不是很直觀,它的實際作用就是把 cap 變成第一個大於等於 2 的冪次方的數。例如,16 還是 16,13 就會調整爲 16,17 就會調整爲 32。

resize方法

HashMap 在進行擴容時,使用的 rehash 方式非常巧妙,因爲每次擴容都是翻倍,與原來計算(n-1)&hash 的結果相比,只是多了一個 bit 位,所以節點要麼就在原來的位置,要麼就被分配到“原位置+舊容量”這個位置。

例如,原來的容量爲 32,那麼應該拿 hash 跟 31(0x11111)做與操作;在擴容擴到了 64 的容量之後,應該拿 hash 跟 63(0x111111)做與操作。新容量跟原來相比只是多了一個 bit 位,假設原來的位置在 23,那麼當新增的那個 bit 位的計算結果爲 0時,那麼該節點還是在 23;相反,計算結果爲 1 時,則該節點會被分配到 23+31 的桶上。

正是因爲這樣巧妙的 rehash 方式,保證了 rehash 之後每個桶上的節點數必定小於等於原來桶上的節點數,即保證了 rehash 之後不會出現更嚴重的衝突。

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);
        }
        // 新的 resize 閾值
        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;
    }

在這裏有一個需要注意的地方,有些文章指出當哈希表的 桶佔用超過閾值時就進行擴容,這是不對的;實際上是當哈希表中的 鍵值對個數超過閾值時,才進行擴容的。

總結

按照原來的拉鍊法來解決衝突,如果一個桶上的衝突很嚴重的話,是會導致哈希表的效率降低至 O(n),而通過紅黑樹的方式,可以把效率改進至 O(logn)。相比鏈式結構的節點,樹型結構的節點會佔用比較多的空間,所以這是一種以空間換時間的改進方式。

LinkedHashMap 的實現原理

概述

Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return true immediately prior to the invocation.)

從註釋中,我們可以先了解到 LinkedHashMap 是通過哈希表和鏈表實現的,它通過維護一個鏈表來保證對哈希表迭代時的有序性,而這個有序是指鍵值對插入的順序。另外,當向哈希表中重複插入某個鍵的時候,不會影響到原來的有序性。也就是說,假設你插入的鍵的順序爲 1、2、3、4,後來再次插入 2,迭代時的順序還是 1、2、3、4,而不會因爲後來插入的 2 變成 1、3、4、2。(但其實我們可以改變它的規則,使它變成 1、3、4、2)

LinkedHashMap 的實現主要分兩部分,一部分是哈希表,另外一部分是鏈表。哈希表部分繼承了 HashMap,擁有了 HashMap 那一套高效的操作,所以我們要看的就是LinkedHashMap 中鏈表的部分,瞭解它是如何來維護有序性的。

LinkedHashMap 的大致實現如下圖所示,當然鏈表和哈希表中相同的鍵值對都是指向同一個對象,這裏把它們分開來畫只是爲了呈現出比較清晰的結構。

屬性

在看屬性之前,我們先來看一下 LinkedHashMap 的聲明:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

從上面的聲明中,我們可以看見 LinkedHashMap 是繼承自 HashMap 的,所以它已經從 HashMap 那裏繼承了與哈希表相關的操作了,那麼在 LinkedHashMap 中,它可以專注於鏈表實現的那部分,所以與鏈表實現相關的屬性如下:

//LinkedHashMap 的鏈表節點繼承了 HashMap 的節點,而且每個節點都包含了前指針和後指針,所以這裏可以看出它是一個雙向鏈表
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
	// 頭指針
	transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     * 尾指針
     */
    transient LinkedHashMap.Entry<K,V> tail;

    // 默認爲 false。當爲 true 時,表示鏈表中鍵值對的順序與每個鍵的插入順序一致,也就是說重複插入鍵,也會更新順序
	// 簡單來說,爲 false 時,就是上面所指的 1、2、3、4 的情況;爲 true 時,就是 1、3、4、2 的情況
    final boolean accessOrder;

方法

如果你有仔細看過 HashMap 源碼的話,你會發現 HashMap 中有如下三個方法:

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

如果你沒有注意到註釋的解釋的話,你可能會很奇怪爲什麼會有三個空方法,而且有不少地方還調用過它們。其實這三個方法表示的是在訪問、插入、刪除某個節點之後,進行一些處理,它們在 LinkedHashMap 都有各自的實現。

LinkedHashMap 正是通過重寫這三個方法來保證鏈表的插入、刪除的有序性。

afterNodeAccess方法

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 當 accessOrder 的值爲 true,且 e 不是尾節點
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

這段代碼的意思簡潔明瞭,就是把當前節點 e 移至鏈表的尾部。因爲使用的是雙向鏈表,所以在尾部插入可以以 O(1)的時間複雜度來完成。並且只有當 accessOrder設置爲 true 時,纔會執行這個操作。在 HashMap 的 putVal 方法中,就調用了這個方法。

afterNodeInsertion方法

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

afterNodeInsertion 方法是在哈希表中插入了一個新節點時調用的,它會把鏈表的頭節點刪除掉,刪除的方式是通過調用 HashMap 的 removeNode 方法。想一想,通過afterNodeInsertion 方法和 afterNodeAccess 方法,是不是就可以簡單的實現一個基於最近最少使用(LRU)的淘汰策略了?當然,我們還要重寫 removeEldestEntry 方法,因爲它默認返回的是 false。

afterNodeRemoval方法

void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

這個方法是當 HashMap 刪除一個鍵值對時調用的,它會把在 HashMap 中刪除的那個鍵值對一併從鏈表中刪除,保證了哈希表和鏈表的一致性。

get方法

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

調用的是 HashMap 的getNode 方法來獲取結果的。並且,如果你把 accessOrder 設置爲 true,那麼在獲取到值之後,還會調用 afterNodeAccess 方法。

put 方法和 remove 方法

我在 LinkedHashMap 的源碼中沒有找到 put 方法,這就說明了它並沒有重寫 put 方法,所以我們調用的 put 方法其實是 HashMap 的 put 方法。因爲 HashMap 的 put 方法中調用了 afterNodeAccess 方法和 afterNodeInsertion 方法,已經足夠保證鏈表的有序性了,所以它也就沒有重寫 put 方法了。

	public final boolean remove(Object key) {
        	// 調用了父類的 removeNode方法
            return removeNode(hash(key), key, null, false, true) != null;
        }

Hashtable 的實現原理

概述

Hashtable 可以說已經具有一定的歷史了,現在也很少使用到 Hashtable 了,更多的是使用 HashMap 或 ConcurrentHashMap。HashTable 是一個線程安全的哈希表,它通過使用synchronized 關鍵字來對方法進行加鎖,從而保證了線程安全。但這也導致了在單線程環境中效率低下等問題。Hashtable 與 HashMap 不同,它不允許插入 null 值和 null 鍵。

屬性

Hashtable 並沒有像 HashMap 那樣定義了很多的常量,而是直接寫死在了方法裏,所以它的屬性相比 HashMap 來說,可以獲取的信息還是比較少的。

// 哈希表
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).)
 * 擴容的閾值
 * @serial
 */
private int threshold;

/**
 * The load factor for the hashtable.
 * 負載因子
 * @serial
 */
private float loadFactor;

方法

構造方法

public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
    	throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
	if (loadFactor <= 0 || Float.isNaN(loadFactor))
	    throw new IllegalArgumentException("Illegal Load: "+loadFactor);

	if (initialCapacity==0)
	    initialCapacity = 1;
	this.loadFactor = loadFactor;
	table = new Entry<?,?>[initialCapacity];
	threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
public Hashtable(int initialCapacity) {
	this(initialCapacity, 0.75f);
}
public Hashtable() {
	this(11, 0.75f);
}

從構造函數中,我們可以獲取到這些信息:Hashtable 默認的初始化容量爲 11 (與 HashMap 不同),負載因子默認爲 0.75 (與 HashMap相同)。

而正因爲默認初始化容量的不同,同時也沒有對容量做調整的策略,所以可以先推斷出,Hashtable 使用的哈希函數跟 HashMap 是不一樣的(事實也確實如此)。

get方法

public synchronized V get(Object key) {
	Entry<?,?> tab[] = table;
	int hash = key.hashCode();
    // 通過哈希函數,計算出 key 對應的桶的位置
	int index = (hash & 0x7FFFFFFF) % tab.length;
    // 遍歷該桶的所有元素,尋找該 key
	for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
		    if ((e.hash == hash) && e.key.equals(key)) {
				return (V)e.value;
		    }
	}
	return null;
}

跟 HashMap 相比,Hashtable 的 get 方法非常簡單。我們首先可以看見 get 方法使用了synchronized 來修飾,所以它能保證線程安全。並且它是通過鏈表的方式來處理衝突的。

另外,我們還可以看見 HashTable 並沒有像 HashMap 那樣封裝一個哈希函數,而是直接把哈希函數寫在了方法中。而哈希函數也是比較簡單的,它僅對哈希表的長度進行了取模。

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();
    // 計算桶的位置
	int index = (hash & 0x7FFFFFFF) % tab.length;
	@SuppressWarnings("unchecked")
	Entry<K,V> entry = (Entry<K,V>)tab[index];
    // 遍歷桶中的元素,判斷是否存在相同的 key
	for(; entry != null ; entry = entry.next) {
		if ((entry.hash == hash) && entry.key.equals(key)) {
			V old = entry.value;
			entry.value = value;
			return old;
		}
	}
	// 不存在相同的 key,則把該 key 插入到桶中
	addEntry(hash, key, value, index);
	return null;
}
private void addEntry(int hash, K key, V value, int index) {
	Entry<?,?> tab[] = 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++;
}

put 方法一開始就表明了不能有 null 值,否則就會向你拋出一個空指針異常。Hashtable的 put 方法也是使用 synchronized 來修飾。你可以發現,在 Hashtable 中,幾乎所有的方法都使用了 synchronized 來保證線程安全。

remove方法

public synchronized V remove(Object key) {
	Entry<?,?> tab[] = table;
	int hash = key.hashCode();
	int index = (hash & 0x7FFFFFFF) % tab.length;
	@SuppressWarnings("unchecked")
	Entry<K,V> e = (Entry<K,V>)tab[index];
	for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
	    if ((e.hash == hash) && e.key.equals(key)) {
			if (prev != null) {
			    prev.next = e.next;
			} else {
			    tab[index] = e.next;
			}
			modCount++;
			count--;
			V oldValue = e.value;
			e.value = null;
			return oldValue;
		}
	}
	return null;
}

remove 方法跟 get 和 put 的原理差不多。

rehash方法

protected void rehash() {
	int oldCapacity = table.length;
	Entry<?,?>[] oldMap = table;

	// overflow-conscious code
    // 擴容擴爲原來的兩倍+1
	int newCapacity = (oldCapacity << 1) + 1;
    // 判斷是否超過最大容量
	if (newCapacity - MAX_ARRAY_SIZE > 0) {
		if (oldCapacity == MAX_ARRAY_SIZE)
			// Keep running with MAX_ARRAY_SIZE buckets
			return;
		newCapacity = MAX_ARRAY_SIZE;
	}
	Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

	modCount++;
    // 計算下一次 rehash 的閾值
	threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
	table = newMap;
	// 把舊哈希表的鍵值對重新哈希到新哈希表中去
	for (int i = oldCapacity ; i-- > 0 ;) {
		for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
			Entry<K,V> e = old;
			old = old.next;

			int index = (e.hash & 0x7FFFFFFF) % newCapacity;
			e.next = (Entry<K,V>)newMap[index];
			newMap[index] = e;
		}
	}
}

Hashtable 的 rehash 方法相當於 HashMap 的 resize 方法。跟 HashMap 那種巧妙的 rehash方式相比,Hashtable 的 rehash 過程需要對每個鍵值對都重新計算哈希值,而比起異或和與操作,取模是一個非常耗時的操作,所以這也是導致效率較低的原因之一。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章