HashMap

本篇文章是【Java集合系列】文章 Map 篇的第二篇,本系列将会逐个分析 Java 中的常用集合的特性及实现,然后对比不同场景下应该选择哪种集合使用。

List 系列

  • ArrayList
  • LinkedList
  • CopyOnWriteArrayList

Queue系列

  • ArrayDeque
  • ConcurrentLinkedDeque
  • LinkedBlockingDeque

Map 系列

  • Hashtable
  • HashMap

HashMap

HashMap 实现了 Map 接口,用于存储键值对,与 Hashtable 不同的是,HashMap 允许 null 元素

HashMap 具有两个性能相关的参数:初始容量(initial capacity)负载因子(load factor), 容量是指 HashMap **桶(buckets)**的容量,初始容量即 HashMap 在创建时桶的默认大小。

负载因子默认是 0.75,默认初始容量是 16,初始容量如果设置过大会导致遍历时间变长,但可以降低 resize 的次数,需要结合场景自己权衡。

在容量达到阈值后会进行扩容,扩容后容量为当前的两倍

另外设置的初始容量并不代表实际数组的初始大小,而是会根据设定值找到一个最近的 2 的次幂当做初始容量。而因为每次扩容都是两倍,所以也保证了数组长度一直是 2 的次幂

HashMap 是非线程安全的,可以通过 Collections 类获取同步的包装对象:

Map m = Collections.synchronizedMap(new HashMap(...));

HashMap 的迭代器被设计为 fail-fast 的,如果迭代器创建完成后 HashMap 的结构被修改,迭代器将会抛出ConcurrentModificationException异常。

HashMap 采用数组+链表/红黑树的数据结构,当链表长度大于等于 8 时链表将转换为红黑树。

源码

HashMap 的源码相比较之前的集合要多很多,主要是因为 1.8 之后引入了红黑树,很多代码都是对红黑树的操作。

构造器

我们先看看初始化:

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

构造器中除了对两个入参做了合法性检测之外就是调用了tableSizeFor方法计算扩容阈值,而这个值也会被当成数组的初始大小。tableSizeFor方法的一系列位运算骚操作即可保证返回值是最接近给定参数的一个 2 的次幂。

put(K, V)

public V put(K key, V value) {
     
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
     
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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)
        // 通过哈希值计算该 key 的数组索引并判断该位置是否已经存在数据,
        // 没有则创建一个新的链表
        tab[i] = newNode(hash, key, value, null);
    else {
     
        // 该索引已存在数据:链表或红黑树
        Node<K,V> e; K k;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // key 已存在,后面会直接更新 value
            e = p;
        else if (p instanceof TreeNode)
            // key 不存在,且当前索引位置的元素为红黑树
            // 则将该键值对插入红黑树中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
     
            // key 不存在,且当前索引位置的元素为链表
            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) {
      // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                // 更新 value
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        // 长度大于阈值后进行 resize 操作
        resize();
    afterNodeInsertion(evict);
    return null;
}

内容主要在putVal中,通过哈希值计算索引的算法也很简单:

// n 为数组的长度
(n - 1) & hash

因为之前保证过数组长度永远是 2 的次幂,所以对n - 1进行 & 运算相当于求余,从而保证索引在数组内。

再将元素添加到链表中后会判断是否需要转换成红黑树,需要的话则会调用treeifyBin转换,最后还会调用resize方法进行扩容。

treeifyBin(Node[], int)

该方法用于将链表转换成红黑树。

final void treeifyBin(Node<K,V>[] tab, int hash) {
     
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
     
        TreeNode<K,V> hd = null, tl = null;
        do {
     
            // 链表节点转换成树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
     
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

遍历链表所有节点,将其转换成树节点,此时只是把链表节点转换成了树节点,还没有称为红黑树,等到节点全部转换完成后调用hd.treeify(tab)方法才会调整为红黑树。

resize()

当数组长度超出阈值后会进行 resize 操作,该操作会创建一个两倍大小的数组,并对数据进行重新分配。

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; // 扩容为原来的两倍
    }
    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);
    }
    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;
}

HashMap_第1张图片

你可能感兴趣的:(Java,数据结构,java,hashmap,Android,算法)