Java之 HashMap源码分析

java HashMap源码分析

hashMap几个万恶的问题:咱们挨个看源码解答,这玩意除非你看源码,不然谁知道。。。

  1. key是否可以为null? 答:可以,看put方法以及hash方法即可
  2. hashMap什么时候扩容?  答:当size > threshold时进行 resize
  3. hashMap是否是线程安全的? 答:线程不安全,主要方法并没有synchornized关键字修饰
  4. hashMap的容量以及扩容为什么都是2的倍数? 答:详情看 index = (lenght-1) & hash这个计算索引的方式

hashMap成员变量:与自身容量相关

//默认初始容量,2的4次方=16,必须是2的倍数,单看这行你看不出来为啥要是2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方,int有32位,java中最高位是符号位,剩下31位,是2的0-30次方共占用31位
//所以最大就是2的30次方,这最大的还是2的倍数,这是个很神奇的事情
static final int MAXIMUM_CAPACITY = 1 << 30
//默认的容量系数,和容量有关
static final float DEFAULT_LOAD_FACTOR = 0.75f

 hashMap成员变量:与树化相关

//树化的临界值
static final int TREEIFY_THRESHOLD = 8;
//解除树化的临界值
static final int UNTREEIFY_THRESHOLD = 6;
//最小树化的容量
static final int MIN_TREEIFY_CAPACITY = 64;

 hashMap成员变量:与存储相关


//数据节点,这是个链表,看到nextNode没有
static class Node<K,V> implements Map.Entry<K,V>{
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
};
//节点数组,数组中每个节点又是一个链表
transient Node<K,V>[] table;

transient Set<Map.Entry<K,V>> entrySet;

transient int size;

transient int modCount;

int threshold;

final float loadFactor;

主要方法:构造方法 

//构造方法
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        //即时你传递的值大于2的30次方,也没用,最大就是2的30次方
        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);
    }

主要方法:put方法  

//最常用的put方法
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
 }

主要方法:hash方法   

//hash值计算,这里看到Key为Null时,hash值为0
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 主要方法:putVal方法   

  1. 根据hash与数组长度计算index值
  2. index处是否有节点存在,空则直接存入Index处
  3. index处有节点,判断节点是否是树型节点,是则直接在树中追加节点
  4. 非树节点,则根据index节点的.next属性遍历找到末尾节点进行追加,且判断长度
  5. 当长度大于等于7时,则进行树化操作
  6. 当所有node的数量 > threshold时,进行扩容
 //final 修饰的方法不能被重写
 //boolean onlyIfAbsent : true则不改变任何值,false:则进行覆盖,当key相同时有用
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; 
        Node<K,V> p; -> 等于插入成功后的值,或者插入前就占在这个位置上的值
        int n; -> table[].length 
        int i; -> index = (length-1)&hash
        //判断 transient Node<K,V>[] table 这个属性是否需要初始化
        //Node<K,V>[] table = new Node[DEFAULT_INITIAL_CAPACITY]
        if ((tab = table) == null || (n = tab.length) == 0){
            n = (tab = resize()).length;
        }
        //p=tab[index]; index=(table.length-1) & hash,这步是容量为2的倍数的关键!!!
        if ((p = tab[i = (n - 1) & hash]) == null){
            //如果当前index处没有值,创建新的Node
            tab[i] = newNode(hash, key, value, null);
        } else {
            //如果当前index有值,这就是传说中的hash碰撞了
            Node<K,V> e; -> key相同的占位节点,或者占位的树类型节点
            K k; -> 当前占位的这个元素的key
            //这就是你存入了两个相同的key了,hash值一样,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 {
                //非树节点,则以链表的形式追加在末尾,currentNode.next = newNode
                //binCount 等同于当前占位节点后面的链表长度,这条链每个节点的key的hash都相同
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //找末尾节点,末尾节点的next为空,追加在末尾节点
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) {// -1 for 1st
                            //如果这条链的长度>=(8-1=7)了,则将这条链进行树化
                            treeifyBin(tab, hash);
                        }
                        break; //不够树化的数量呢,链表追加完退出循环,完事收工
                    }
                    //这条链上的每个节点,都与要插入的节点判断下key是不是相同,相同的则不做任何处理
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))){
                        break;
                    }
                    p = e;
                }
            }
            //e不管是树还是链表,当e不为空时,意味着要插入的节点与e,他们key的hash值完全相同了
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        } // else 到此结束
        ++modCount;
        //这一步的判断是扩容的关键,需要展开分析
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        //才发现,put方法其实返回的是null
        return null;
    }

树化过程:先记好与树相关的几个成员变量

static final int TREEIFY_THRESHOLD = 8;  -> 用于与hash冲突的链表做比较
//解除树化的临界值
static final int UNTREEIFY_THRESHOLD = 6;  -> 用于与hash冲突的链表做比较
//最小树化的容量
static final int MIN_TREEIFY_CAPACITY = 64; -> 用于与table.length做比较
//节点数组
transient Node<K,V>[] table;

树化相关代码:

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

树化的具体实现:treeifybin 

//参数说明:tab[] hashMap的实例属性,存放所有node的数组
//参数说明:hash 你当前put时传入的key所算出的hash值
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n;
        int 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) {
            /**获取hash值相同的e节点,能够执行到这个方法里的,说明当前这个e节点的长度
            * 已经大于等于 7 了,且e节点是链表第一个节点
            */
            TreeNode<K,V> hd = null; //hd是一个双向链表的头节点,节点是TreeNode
            TreeNode<K,V> tl = null; //t1是temp节点,用于临时保存节点
            do {
                /**
                * 根据第一个节点e的值,创建了一个新的节点p,且节点p的后继节点为空,
                * 根据do->while循环,e节点分别为链表上的,第1,2,3...7节点
                * 且根据这些节点,每次循环都创建一个节点p进行操作
                */
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null){
                    /**
                    * 第一次循环链表的第一个节点时,t1才为null,此时p是根据第一个节点
                    * 创建的TreeNode,hd=p,说明hd是这个链表的头节点
                    */
                    hd = p; 
                } else {
                    //t1是上一个节点,p是当前节点,这两行代码创建了一个双向的链表
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值