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 过程需要对每个键值对都重新计算哈希值,而比起异或和与操作,取模是一个非常耗时的操作,所以这也是导致效率较低的原因之一。

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