java集合框架:HashMap剖析(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/WYpersist/article/details/79950912

HashMap 介绍

基于哈希表 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的容量(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。

HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。
该类实现了Map接口,根据键的HashCode值存储数据,具有很快的访问速度,最多允许一条记录的键为null,不支持线程同步。

HashMap的类图

 

HashMap源码剖析

所以存储的元素也是键值对映射的结构,并允许使用null值和null键,其内元素是无序的,如果要保证有序,可以使用LinkedHashMapHashMap是线程不安全的

Hashmap 类关系

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

子类LinkedHashMap类关系

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

HashMap中定义的成员变量

/**
 * The default initial capacity - MUST be a power of two.
 */

// 默认初始容量为16,必须为2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

/**
 * Holds cached entrySet(). Note that AbstractMap fields are used
 * for keySet() and values().
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * The number of key-value mappings contained in this map.
 */
transient int size;

/**
 * The next size value at which to resize (capacity * load factor).
 *
 *
@serial
 
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;

int DEFAULT_INITIAL_CAPACITY = 16:默认的初始容量为16 
int MAXIMUM_CAPACITY = 1 << 30:最大的容量为 2 ^ 30 
float DEFAULT_LOAD_FACTOR = 0.75f:默认的加载因子为 0.75f 
Entry< K,V>[] tableEntry类型的数组,HashMap用这个来维护内部的数据结构,它的长度由容量决定 

Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。
int sizeHashMap的大小 
int thresholdHashMap的极限容量,扩容临界点(容量和加载因子的乘积)

HashMap 中三个关于红黑树的关键参数

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

//一个桶的树化阈值

//当桶中元素个数超过这个值时,需要使用红黑树节点替换链表节点

//这个值必须为 8,要不然频繁转换效率也不高

static final int TREEIFY_THRESHOLD = 8;

//一个树的链表还原阈值 //当扩容时,桶中元素个数小于这个值,就会把树形的桶元素 还原(切分)为链表结构

//这个值应该比上面那个小,至少为 6,避免频繁转换

 static final int UNTREEIFY_THRESHOLD = 6;

//哈希表的最小树形化容量

//当哈希表中的容量大于这个值时,表中的桶才能进行树形化

//否则桶内元素太多时会扩容,而不是树形化

 //为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD static final int MIN_TREEIFY_CAPACITY = 64;

然后接着,我们来看hashmap最常用的方法:

     HashMap中我们最长用的就是put(K, V)get(K)。我们都知道,HashMapK值是唯一的,那如何保证唯一性呢?我们首先想到的是用equals比较,没错,这样可以实现,但随着内部元素的增多,putget的效率将越来越低,这里的时间复杂度是O(n),假如有1000个元素,put时最差情况需要比较1000次。实际上,HashMap很少会用到equals方法,因为其内通过一个哈希表管理所有元素,哈希是通过hash单词音译过来的,也可以称为散列表,哈希算法可以快速的存取元素,当我们调用put存值时,HashMap首先会调用KhashCode方法,获取哈希码,通过哈希码快速找到某个存放位置,这个位置可以被称之为bucketIndex,但可能会存在多个元素找到了相同的bucketIndex,有个专业名词叫碰撞,当碰撞发生时,这时会取到bucketIndex位置已存储的元素,最终通过equals来比较,equals方法就是碰撞时才会执行的方法,所以前面说HashMap很少会用到equalsHashMap通过hashCodeequals最终判断出K是否已存在,如果已存在,则使用新V值替换旧V值,并返回旧V值,如果不存在 ,则存放新的键值对<K, V>bucketIndex位置

我们看hashmap类中put的源码:

hashmap类中put的源码

插入节点:

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;
    //如果table为空或长度为0,则初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //(n - 1) & hash:相当于 hash%数组长度,取余找到put位置,如果这个位置还为空,则直接创建Node作为该位置链表的第一位
    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;
            //如果该练表已经转换为了红黑树,那么用红黑树的方式插入该新节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<k,v>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //不是红黑树就是链表,binCount计数,如果大于8就要转换成红黑树
            for (int binCount = 0; ; ++binCount) {
                //p第一次指向表头,以后依次后移
                if ((e = p.next) == null) {
                    //e为空,表示已到表尾也没有找到key值相同节点,则新建节点
                    p.next = newNode(hash, key, value, null);
                    //新增节点后如果节点个数到达阈值,则将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                //查找练表中key和插入key是否一致,一致的话位置找到
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //更新hash值和key值均相同的节点的Value
        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过程


当put方法后,HashMap结构会发生三种情况,接下来我们一起看看

但是我们先来看看数据结构,数据结构是什么样的?

HashMap数据结构

 

HashMap采用位桶+链表+红黑树实现

HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

源码如下:

单向链

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

红黑树

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;
    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

    /**
     * Returns root of tree containing this node.
     */
    
final TreeNode<K,V> root() {
        for (TreeNode<K,V> r = this, p;;) {
            if ((p = r.parent) == null)
                return r;
            r = p;
        }
    }

Java 8 中,如果一个桶中的元素个数超过 TREEIFY_THRESHOLD(默认是 8 ),就使用红黑树来替换链表,从而提高速度。

这个替换的方法叫 treeifyBin() 即树形化。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //如果哈希表中的元素个数超过了 树形化阈值,进行树形化
        // e 是哈希表中指定位置桶里的链表节点,从第一个开始
        TreeNode<K,V> hd = null, tl = null; //红黑树的头、尾节点
        do {
            //新建一个树形节点,内容和当前链表节点 e 一致
            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);
    }
}

transient Node<K,V>[] table;  //存储(位桶)的数组</k,v>

有了以上3个数据结构,只要有一点数据结构基础的人,都可以大致联想到HashMap的实现了。首先有一个每个元素都是链表的数组,当添加一个元素(key-value)时,就首先计算元素keyhash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,但是形成了链表,所以说数组存放的是链表。而当链表长度太长时,链表就转换为红黑树,这样大大提高了查找的效率。

然后接下来,我们知道Hashmap根据键的HashCode值存储数据

Hashcode方法内部实现:

 

我们查看其中的kv在哪,我们发现hashcode方法其实在静态类Node(我们上面说的单链)里面,当然kv也在里面了。

static class Node<K,V> implements Map.Entry<K,V> {

 

 继续看下一篇。。。

 

 

阅读更多

扫码向博主提问

菜鸟级的IT之路

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • Hadoop
  • Spark
  • Java后端
  • HBase
去开通我的Chat快问
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页