欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JDK8源码阅读(四):HashMap

程序员文章站 2022-06-17 10:27:41
...

1.是什么

是通过K-V键值对来保存数据的数据结构,K和V都可以为null
继承关系如下图所示:
JDK8源码阅读(四):HashMap
数据结构如下图所示:
JDK8源码阅读(四):HashMap

2.构造函数和主要属性

  • 加载因子loadFactor非常重要,这个决定了什么时候进行数组的扩容操作
  • 每次扩容时,都扩容为原来数组长度的2倍
// 存放数据的数组默认初始大小为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 默认的加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表变形为红黑树时,链表的长度
static final int TREEIFY_THRESHOLD = 8;

// 红黑树变形为链表时,红黑树节点个数
static final int UNTREEIFY_THRESHOLD = 6;

// 当hashmap中元素达到64时,才会让链表变形为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

// 存储元素的数组
transient Node<K,V>[] table;

// hashmap中元素的个数
transient int size;

// 加载因子
final float loadFactor;

/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
int threshold;

static final int MAXIMUM_CAPACITY = 1 << 30;

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(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);
}

2.1 这里着重讲一下带有初始容量时的代码

当在创建HashMap时,传入了初始容量后,会处理这个初始容量。

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

但是这个时候还没有初始化存放数据的数组,而是在第一次 put 时,才会去初始化存放数据的数组。
如下图所示:

  • 第一次执行resize()方法,oldTab 是 null,所以 oldCap = 0
  • 然后就会执行图中红框的方法,初始化新的数组容量为 oldThr,也就是threhold
    JDK8源码阅读(四):HashMap

下面这段代码是在JDK1.7以后的实现

    /**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }
  • 第一行代码int n = cap - 1;是因为当cap的值为2x时,如果cap不减1的话n的值会为2x+1,所以我们需要先将cap的值减一。
  • 然后再算出一个2的次方的值,这个值为最接近并大于cap的值。例如5最近的2的次方的值为23(8),29的最近的2的次方的值为25(32)。
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;

为什么上面算法能实现找到大于n的最小的2的次方的值呢?

因为当我们把n无符号右移1位后做或操作,那么一个1就会变为两个1,再右移2位,两个1就变为4个,依次类推,到最后右移16位就会将n的二进制的最高位的1的后面位置全部变为1。

然后最后return 返回的时候,再对n进行加1操作,使得返回值是一个大于n的最小的2的次方的一个值。也就是把n的二进制表示的值的最高位的1前面变为1,然后新的最高位后面的全部变为0。

例如:
17 ==> 0001 0001
n |= n >>> 1:0001 0001 | 0000 1000 ==> 0001 1001
n |= n >>> 2:0001 1001 | 0000 0110 ==> 0001 1111
因为最高位1后面已经全部是1了,所以后面的无符号右移4位、8位和16位后的结果也为0001 1111。
然后末尾+1就等于0010 0000,也就是大于17的最小的2的次方的值32

3.新增元素

在调用put()方法时,主要有2种情况:

  • 该key不在hashmap中存在,则新增一个元素,并判断hashmap是否需要扩容,最后返回null
  • 该key已经存在于hashmap中,则替换掉原来的value为新值,最后返回key对应的旧value值
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

// 当key为null时,hash值为0,否则hash值为key的hashcode与hashcode高16位进行异或,减少hash冲突。
// 在《码出高效:Java开发手册》一书中对这个算法进行了说明:“将高位与低位进行异或 有助于泊松分布。”
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    // 1.当数组为null或者数组长度为0时,进行扩容操作
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    // 2.当所在位置为null时,直接在该位置放入元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;

		// 判断key是已经存在
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            
        // 判断所在位置的元素为红黑树
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

		// 说明所在位置的元素为链表
        else {
        	// binCount是为了统计链表的长度
            for (int binCount = 0; ; ++binCount) {
            	// 找到了链表的末尾,说明key不存在
                if ((e = p.next) == null) {
                	// 将新增的元素添加到链表的末尾
                    p.next = newNode(hash, key, value, null);
                    // 判断是否需要将该链表转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	// 在这个方法里,不会直接将链表转为红黑树,而是还会判断table数组的长度,
                    	// 如果大于64,才会将链表转为红黑树,否则会进行resize()扩容操作。
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果是在链表中间找到了,说明key已经存在
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
		
		// e不为空,说明是修改key所映射的value值,而不是新增的key-value键值对
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // put方法默认onlyIfAbsent为false,即如果旧值不为null,就返回旧值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 是LinkedHashMap实现的方法,在HashMap中为一个空方法
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
	// 元素新增成功后,判断元素个数(包括刚刚新增的元素)是否大于 数组长度 * 加载因子,大于则进行扩容操作
    if (++size > threshold)
        resize();
    // 是LinkedHashMap实现的方法,在HashMap中为一个空方法
    afterNodeInsertion(evict);
    return null;
}

4.移除元素

若key不存在,则返回null
若key存在,则移除key对应的元素,并返回key所对应的value值

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