Java1.8中HashMap原理(桶和桶内链表红黑树)

目录

一、HashMap介绍

二、HashMap原理图

三、桶的个数必定为2的n次幂

四、HashMap中的put方法

五、链表转红黑树


一、HashMap介绍

HashMap采用键值对的形式存储数据,在提取数据时可以通过对key的比较得到对应的Value的值.

二、HashMap原理图

Java1.8中HashMap原理(桶和桶内链表红黑树)_第1张图片

HashMap中存储数据是通过存入key的hash值进行存储的,我们先来看一下HashMap的构造方法.

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

接下来再看一下HashMap中的部分域(属性).

//桶数量的默认值(16),而不是桶内可以装的元素的数量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

//必须为2的n次方,所以最大为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
/*
    默认负载系数为0.75,当表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行
    再散列(rehashed),可以通过构造函数初始化
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//桶内数据量超过这个阈值就会将桶内数据结构从链表转为红黑树
static final int TREEIFY_THRESHOLD = 8;

//当小于这个数值时将红黑树转为链表
static final int UNTREEIFY_THRESHOLD = 6;

//在第一次使用时初始化Node数组,桶对象数组
transient Node[] table;

//该HashMap中共有多少个键值对
transient int size;

//桶的数量,默认为DEFAULT_INITIAL_CAPACITY,即16
int threshold;

从以上代码中我们可以看出,threshold属性就是桶的个数,而table就是这个桶的数组.我们可以暂时理解为第一次添加一个新元素的时候,会有一个table = new Node[size]() 的操作.

那么这个Node又是个什么鬼- -.

这其实涉及到内部静态类的概念.也就是Node定义在HashMap里,而不是和HashMap放在同一级别(.java文件中写两个类).以下是Node类型的定义:

/*
* 内部static类,对外部不可见,并且保证每个HashMap对象的内部类相同  
*/
static class Node implements Map.Entry {
    //存入元素的hash值
    final int hash;
    //存入元素的key值
    final K key;
    //key对应的value值
    V value;
    //桶内采用链表形式,指向下一个Node对象
    Node next;

    Node(int hash, K key, V value, Node 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;
    }
}

在Node类里,我们发现有next属性,不用多说,这肯定是个链表.而在HashMap的table数组中,存储的只是第一个Node元素,也就是链表中的头结点.

好了以上HashMap的基本存储就说清楚了,接下来介绍一下桶的个数的相关问题.

三、桶的个数必定为2的n次幂

桶的个数必定为2的n次幂,这是Java设计者定义的约定.在之前的提到的构造方法中有这样一段代码:

    //如果输入的容量设定小于0,则抛出异常 
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果设定长度大于最大值,则设定为最大值,即2^30
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //装载因子小于等于0或者不是一个float类型的数,则抛出一个异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);

tableSizeFor方法:

//获得比cap要大且确保是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;
}

在该方法中,我们先不谈cap - 1,先来看下面的 n | n >>> 1.在Java中,我们知道>>>是右移高位补0,此时我们将原数与移位后得到的数进行或运算,此时我们就可以确保在最高位的后面那一位它的值也是1.下一次n >>> 2的时候就能够确保最高位后面跟上三个1...在所有的移位、或的操作后,我们得到的就是最高位后面全是1的一个int类型数据.此时n+1必定是一个2幂次的数.

详解如下:

 eg:

先来分析有关n位操作部分:先来假设n的二进制为01xxx...xxx。接着

对n右移1位:001xx...xxx,再位或:011xx...xxx

对n右移2为:00011...xxx,再位或:01111...xxx

此时前面已经有四个1了,再右移4位且位或可得8个1

同理,有8个1,右移8位肯定会让后八位也为1。

综上可得,该算法让最高位的1后面的位全变为1。

最后再让结果n+1,即得到了2的整数次幂的值了。

四、HashMap中的put方法

我们先来说一下在HashMap中的加入机制吧,说完机制我们再来看代码.

首页呢,假设桶的数量为n,待插入的元素的key值的哈希值为hash.然后,我们通过求余操作计算 hash % n,假设结果为 t.此时我们就可以往 table[t] 中插入一条新的数据.也就是说通过hash值和桶的数量求余的操作得到要存入的链表的位置.

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[] tab; Node 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 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)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) { // 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;
    }

 在上面代码中,有下面这一段:

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);

i = (n - 1) & hash,这是将n - 1和hash进行二进制上的与操作,n在上一句通过 n = table.length赋值为桶的长度.比如说长度设为之前提到过的默认长度16,16-1=15,15 & hash实际上就是取余的操作.从第三章我们知道,一共有2^m个桶,所以代码中的n-1得到的是从第m位到第0位(从右至左)都是1,此时将hash与n - 1进行与操作得到的结果就跟hash % n得到的结果是一模一样的.这就是2的次幂的重要性.取余操作变成了位与,这大大优化了HashMap的性能.

当然了,你在putVal中采用了(n - 1) & hash的操作的话,桶的设计自然就不能是2的次幂以外的数了,比如说你设定桶的数量是9.hash值比方说传入了2和3,2 & 8 = 0,3 & 8 = 0.这样就冲突了.

五、链表转红黑树

我们已经清楚了HashMap的存储结构以及构造方法,不过在构造方法中还有一个变量没有提到过,那就是loadFactor.它是一个用来判断何时改变桶内存储结构的变量.

当桶内数据越来越多时,链表的查询效率显然就弱了,所以在超过一定存储数量之后,桶内的存储结构就从链表替换为了红黑树.这个加载因子有个默认值是0.75(一般来讲这个默认值足够了).

具体的可以看这篇文章,红黑树原理以及HashMap中链表到红黑树的转换和判断条件等,将的十分详细.

https://www.cnblogs.com/finite/p/8251587.html

 

你可能感兴趣的:(Java)