HashSet/HashMap底层源码解析

HashSet/HashMap底层源码解析

注意:这里仅仅只针对JDK8进行源码分析
HashSet new的过程实际上是创建HashMap过程,首先看一下测试代码
测试代码

 @Test
    @SuppressWarnings("all")
    public void hashSetTest(){
        Set hashSet=new HashSet();
        hashSet.add("李白");
        hashSet.add("杜甫");
        hashSet.add("李白");
    }

源码一:new HashSet()调用构造器的源码

private transient HashMap<E,Object> map;
public HashSet() {
    map = new HashMap<>();
}

源码二:new HashMap()是对loadFactor属性的初始化过程

final float loadFactor;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//这里的DEFAULT_LOAD_FACTOR是一个扩容机制的扩容因子,0.75符合泊松分布
//泊松分布的选择,是hash冲突和空间利用率的折中方案
public HashMap() {
  this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

源码三:new完成以后,调用add方法

private static final Object PRESENT = new Object();
//这里应该就给很多人一个解答了
/*
 *为什么HashMap存储的Key-Value,而HashSet如果想要底层实现HashMap,不得也有一个Value值才行吗?
 *答:这里value值始终固定为Object PRSENT,且从对象创建开始就已经生成不改变
 *即:Value设置了一个固定地址值
*/
public boolean add(E e) {
   return map.put(e, PRESENT)==null;
}
//HashMap中的put方法
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

源码四:调用了put方法后,我们就发现还是在套娃,调用了putVal方法
这里我们先不解析putVal方法,先看五个参数

//hash(key):这里是对字符串做hash处理,生成一个hash码
static final int hash(Object key) {
   int h;
   //这里需要说明一下:如果key为空,则hash就是0,如果不为空,则需要使用key生成一个hash码后异或(^)上(hashCode的值无符号右移16的值)
  
   /*
    * 为什么要使用hashCode无符号右移16
    * 取出hashCode码的高16位
    * 首先我们知道int是4字节--32位bit
    * 其次,这个无符号右移实际上是为了后续转换为数组索引做准备的
    * 这个点会在putVal()方法中进一步解释
   */
   //为什么要无符号右移以后还要异或,为什么使用^而不是&|呢
   //是为了均衡分布,避免hash冲突--看图1
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//key--实际就是我们需要添加的值--"李白"
//value--PRESENT
//false--这个值是否不存在,如果存在,则不改变已存在的值
//true--这个就不重要了,因为使用它的方法并未实现,而是一个空方法
afterNodeInsertion(evict);
void afterNodeInsertion(boolean evict) { }

图1:这里我们可以看出,&、|这些逻辑运算结果都是偏向于true或false的,只有^才能让hash值更加随机散列
图1
重点来了:
源码五:调用putVal()方法

transient Node<K,V>[] table;
static final int TREEIFY_THRESHOLD = 8;
transient int size;
int threshold;
//这个先不要看,直接从putVal开始看,从putVal中看这些东西是做什么的
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;
    }
}
//这个先不要看,直接从putVal开始看,从putVal中看这些东西是做什么的
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
    int n;
    if (root != null && tab != null && (n = tab.length) > 0) {
        int index = (n - 1) & root.hash;
        TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
        if (root != first) {
            Node<K,V> rn;
            tab[index] = root;
            TreeNode<K,V> rp = root.prev;
            if ((rn = root.next) != null)
                ((TreeNode<K,V>)rn).prev = rp;
            if (rp != null)
                rp.next = rn;
            if (first != null)
                first.prev = root;
            root.next = first;
            root.prev = null;
        }
        assert checkInvariants(root);
    }
}
//首先这个参数已经解释过了
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
  //这里的Node是一个静态内部类--作用是创建一个Node节点--也就是上面的static Class Node类
   Node<K,V>[] tab; Node<K,V> p; int n, i;
   //首先,如果是第一次创建,毫无疑问table是null,如果是第二次add,则这个table就不是null了--因为HashSet只new了一次
   //所以这个if语句判断的是,如果这个Node数组是null,并且table表的长度也是0,那就开始初始化呗--后面再解析resize()方法
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //如果table表i这个索引处没有值,那就存放一个Node值呗
    /*
    * 这里就需要解释一下,为什么是无符号右移16位了
    * 这里n是table表的长度,而我们知道table表的长度一般很少在高16位,导致了一个问题
    * table表的长度永远是和低16位的hash值比较
    * 那么如何能让低16位的值更加随机,就只能采用高16位的值先和自身来一次异或,再与n进行与运算,这样取得的索引值就会更随机
    * 随机的目的是:避免一个节点上挂载的链表过长,而数组都还没存满--导致头重脚轻
    */
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//如果i索引处有值
        Node<K,V> e; K k;
        //1.那就把这个"李白"的hash值和p=table[i=(n-1)&hash]处的hash值进行比较
        //相等就继续判断,不相等就拉倒
        //2.接着就会继续判断这个"李白"和p处的字符串是不是同一个地址(当然如果不是引用类型这里就需要其他理解--自行理解)
        //3.判断这个字符串的内容是否与p处的字符串的内容一致(调用了equals方法)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //如果都满足--则将p的引用给e--都满足就覆盖一下呗
            e = p;
        //否则如果上述条件不满足,看看是否p节点已经被转换成了红黑树--没有转换就不执行呗,转换了就直接加在树的节点上
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//上述条件也不满足,也没有转换成红黑树
            for (int binCount = 0; ; ++binCount) //死循环搞一手
            //到这里,我们知道p处有值,且不与"李白"这个值相等
            //但是p是一个Node链表,所以我们仍然需要遍历p这个链表,遍历完我们才能知道p链表是否符合存放"李白"这个值的资格
            /*
            * 1.如果p的下一个节点是空的--代表这个p就一个节点且有值,那就直接添加呗
            * 2.如果循环的参数大于等于链表的最大长度-1,则变成红黑树--这也就是为什么会有判断p是否等于TreeNode的原因
            * 3.链表的长度到底是等于8时转换,还是大于8时转换,还是等于7时就转换呢
            * 	3.1当binCount=0时先++,也就是当binCount=1时就进来执行添加操作--而这时p已经有了一个节点,说明binCount可以代表链表的索引值(添加的是第二个,binCount=1)
            * 	3.2这里的逻辑是添加完成后再判断,所以是添加完第8个以后,此时binCount=7,TREEIFY_THRESHOLD-1=7比较结果相等哇,转换一下
            */
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
              //4.但是如果p的下一个节点不是空的,那就比较一下呗,这里就不重复描述了
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
               //e一直指向p的后继节点--如果后继节点不为空,则继续循环
                p = e;
            }
        }
        /*
        * e的值分析:
        * 如果链表没有满8,则e为空
        * 如果链表满8,转换成红黑树,则e为空,因为putTreeVal()方法返回值为null
        * 如果"李白"已经存在,则e=p e不为空
        * 那么:
        * 	1.如果"李白"已经存在,则按照此逻辑,返回Object()的地址值
        * 	2.为什么要已经存在就要返回一个object地址呢,因为add方法中return map.put(e, PRESENT)==null,如果是null则添加成功,否则添加失败
        */
        if (e != null) { // existing mapping for key
            V oldValue = e.value;//Object()--PERSENT
            if (!onlyIfAbsent || oldValue == null)//true||fasle
                e.value = value;//Object()--PERSENT
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //这里就是一个扩容机制:
    /*
    * 如果添加完数据后,modCount就会++
    * size也会++
    * threshold在第一次扩容的时候就已经赋值了,等会再讲
    * threshold=(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)=16*0.75
    * 所以,如果size>12就会开始扩容
    */
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

后续再详细分析一下红黑树吧,害

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Carl·杰尼龟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值