HashMap源码阅读(一)

HashMap源码阅读

相关注意点

  1. HashMap允许null键和null值, 它与HashTable的区别是, HashTable不允许null和非线程安全。 HashMap无法保证元素的位置信息,本质上就是说迭代的顺序可能和我们添加数据的顺序不一致。
  2. HashMap提供只需常量时间的操作函数 put, get。 在散列分布均匀的情况下,迭代访问操作的时间复杂度和散列桶的个数加上键值对的个数的和成正相关。因此在访问迭代操作为主的情况下,不能把初始化容量设置太高(也不能把装载因子设置太小)。
  3. 一个HashMap有两个影响效果的参数。第一个是初始化容量,第二个是装载因子。 初始化容量表示HashMap创建的时候散列桶的个数。装载因子表示在自动扩充容量之前,哈希表中最多允许的装载程度。如果元素的个数超过初始容量和装载因子的乘积,那么它会自动的把容量扩展为原来的两倍。
  4. 实际上装载因子0.75是时间和空间之间的一个平衡。值越大,空间利用率越高,但查找元素所需时间高。值越小,空间利用率越低, 查找时间越小。
  5. HashMap 非线程安全类,如果需要使用多线程可以使用Synchronize块锁定相应的map集合。或者利用Collections.synchronizedMap包装。 结构化修改:指的是添加或者删除一个或者多个元素,改变一个已经存在键的值不叫结构化修改。
  6. 快速失败, 在迭代器被创建之后,如果map被结构化修改了,那么迭代器会抛出ConcurrentModificationException.

HashMap数据结构包含数组+链表或者数组+红黑树,树节点中还维护着双向链表指针next和prev。链表转换为红黑色主要是为了查找数据更快。俩表转换为红黑树必须满足散列桶数组容量大于阈值且相应散列桶中元素大于阈值。

相应字段介绍

// 默认初始化容量,必须为2的次幂(原因主要和扩容rehash的过程相关)

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 最大允许的容量

static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认装载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表树形化时相应桶中元素个数阈值

static final int TREEIFY_THRESHOLD = 8;

//由树形变为链表的阈值

static final int UNTREEIFY_THRESHOLD = 6;

// 链表树形化必须满足散列表的容量阈值

static final int MIN_TREEIFY_CAPACITY = 64;

// 散列桶数组

transient Node<K,V>[] table;

// map中的键值对个数

transient int size;

// hashmap被结构化修改的次数

transient int modCount;

// 阈值超过这个值就需要扩容, 值为初始容量*装载因子, 如果散列桶没有分配内存,则threshold表示初始容量,或者是零(表示采取默认容量)

int threshold;

// hash表的装载因子

final float loadFactor;

关键方法讲解

构造函数, 散列桶不会分配内存,采取懒加载模式,只有使用的时候才会为散列桶数组分配内存

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;
    // 在散列通数组未分配内存的情况下, threshold表示初始化的容量 
    this.threshold = tableSizeFor(initialCapacity);
}

// 利用高低16位异或操作获取最终的散列值, 充分利用hash的每一位元素,从而散列更加均匀

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

// 把容量转换位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;
}

// 通过此方法我们知道为什么重写equals方法,一定要重写HashCode方法, 因为equals相等, hashcode一定相同。此方法首先查看桶中第一个元素,如果就是目标节点直接返回,否则判断判断第一个元素是树节点还是普通节点,如果是树节点调用getTreeNode查找,否则,利用next指针循环查找。

/**
 * @param hash 键的hash值
 * @param key 键
 * @return 查找到的节点, 如果时null表示没有找到
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // (n-1)& hash映射到相应的散列桶中。
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) { // 相应的桶中存在元素
        // 检查对应桶中的第一个点, 先判断hash值,再判断equals或是否是同一个引用
        if (first.hash == hash && 
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) { //从桶中下一个元素开始查找
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e; 
            } while ((e = e.next) != null);
        }
    }
    return null;
}

// 添加元素, 此方法体现散列桶数组采用懒加载。 第一步首先看散列桶是否初始化,没有则需要分配内存。第二步看对应桶中第一个节点类型, 如果是普通类型则利用next域查找插入元素的位置,如果是树节点则调用putTreeVal在树结构中插入元素。对于普通类型节点的插入,如果插入之后的桶中元素个数超过阈值,调用treeifyBin进行树形化操作

/**
 *
 * @param hash 对应键的hash值
 * @param key 键
 * @param value 要添加的值
 * @param onlyIfAbsent true表示不会更改已经存在的键对应的值,false会更改
 * @param evict false表示散列桶数组是创建模式
 * @return 返回null, 表示不存在相应的key。如果存在相应的key返回原来的value值
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 第一次使用,没有初始化,需要调用resize方法初始化, 
    // 这里可以看到resize不仅能够扩容,而且包含散列桶数组初始化操作
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 下面是添加元素
    // 如果对用桶中第一个节点为null, 直接设置就可以
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else { // 从对应的同种查找, 并添加
          // 存在两种情况, 第一种:找到相同key的记录,那就是看是否替换为新值问题
          // 第二种:添加数据到桶列表最后。 当然需要考虑是否扩容rehash
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; 
        else if (p instanceof TreeNode) // 树节点由自己的操作设置
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { //桶中一个一个元素查找
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 树形化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 找到
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // map中存在对用key的记录, 看是否替换为新值
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); // 钩子方法
            return oldValue;
        }
    }
    ++modCount; // 结构化修改次数+1
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

//resize方法能够实现扩容rehash和懒加载为散列桶数组分配内存。

/**
 * 初始化或者两倍扩容操作。 如果原来hash散列桶数组为null,
 * 则把threshold作为初始化容量来初始化散列桶数组,0表示采用默认容量。扩容rehash过程中,由于内存是   *2的n次幂形式,每一个元素要么留在原来位置的桶中,要么移动原来长度的位置的新桶中
 * @return the table
 */
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) // threshold 中已经设置过初始化容量, 初始化散列桶数组
        newCap = oldThr;
    else {               //threshold为0表示使用默认容量, 初始化散列桶数组
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { // threshold 中已经设置过初始化容量, 更新新数组中的newThr
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 散列桶初始化之后, threshold表示容量和装载因子的乘积,即需要扩容的阈值。
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {// rehash扩容
        // 遍历原始的每一个桶
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 如果第j个桶中只有一个元素, 直接hash到新散列桶中
                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 { //遍历第j个桶中的所有元素
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {// 保存下一个元素
                        next = e.next;
                        // 元素保留在当前散列桶不变, 
                        //主要和容量是2的幂次方及扩容为原来两倍有关, 
                        //所以可以直接判断扩容后的节点位置只有两种可能。
                        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;
}
posted @ 2020-03-24 11:04  张秀杰  阅读(178)  评论(0编辑  收藏  举报