Java基础_HashMap源码分析

该篇文章主要从如下几个方面来学习下HashMap

  • HashMap是啥
  • HashMap的代码实操
  • HashMap的get和put方法的源码分析
  • 在1.7 和 1.8的区别
  • 两个小问题

HashMap是啥

  • HashMap是继承至AbstractMap,实现了Map接口的 线程不安全的无序的散列表。
  • 内部根据Key的hashCode存储key-value键值对。
  • 数据结构是:数组+单链表(+红黑树 1.8之后)

HashMap的代码实操

 Map map = new HashMap<>();
        map.put(2, "index2");
        map.put(4, "index4");
        map.put(3, "index3");
        map.put(1, "index1");

        System.out.println("key ==3 ,value == " + map.get(3));
        map.forEach((k, v) -> {
            System.out.println("k == " + k + ",v == " + v);
        });
//运行结果
key ==3 ,value == index3
k == 1,v == index1
k == 2,v == index2
k == 3,v == index3
k == 4,v == index4

HashMap的原理分析

HashMap的构造方法

/**
*创建一个HashMap对象,其中加载因子默认为0.75 ,
*这个加载因子是当用来扩容的时候进行使用的,当前HashMap的容量超过了总容量的75%就
*会进行扩容的操作!
*
*/
public HashMap() {
        
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

/**
 * @param  initialCapacity the initial capacity. 
 * @throws IllegalArgumentException 如果当前传进的initialCapacity为负数,就  
 * 报这个错误
 * 创建一个初始容量为 initialCapacity 加载因子为0.75的一个HashMap对象
 */
public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
 /**
     *         
     * 1. 构造一个自定义的 容量和加载因子的 HashMap对象
     */
public HashMap(int initialCapacity, float loadFactor) {
  // 如果自定义的容量为负数就抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
            //如果自定义的容量大于 1 << 30
        if (initialCapacity > MAXIMUM_CAPACITY)
            //就将 1 << 30 赋值给 initialCapacity
            initialCapacity = MAXIMUM_CAPACITY;
            //如果加载因子小于等于0 或者是非数字 就抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
            // 转换initialCapacity
        this.threshold = tableSizeFor(initialCapacity);
    }
/**
* 构造一个和 传入Map有相同元素的HashMap,
* 初始容量能容下传入的Map,加载因子为0.75
*
**/
 public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

final void putMapEntries(Map m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
tableSizeFor(int cap)
 /**
     * Returns a power of two size for the given target capacity.
     返回给定目标容量两个大小的幂
     */
    static final int tableSizeFor(int cap) {
        // cap-1 是为了防止当前入参已经是2的幂,结果返回的却是入参的2倍
        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;
      // 该方法最总返回的是,离入参最近的2的n次幂,比如入参是3 返回4,入参 5 返回是8 
    }

移位运算

  • 将二进制数据移动位置并补0,比如 11000>>>1 为 01100 ;01100 >>> 2 为00010

或运算

  • 有一个1则为1,全是0位0

HashMap的put()方法

// 将指定的value 与映射中的 key进行映射
// 如果该映射中先前包含该键的映射,那么替换旧的值 并且返回旧值。
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;
            //如果table为null 或者 table的长度为0
            //就调用resize()方法 将返回的 Node[K,V]数组赋给tab 并将该数组的长度赋给 n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            // 如果当前tab的最后一个Node为null 说明tab为空
            // 那么 就调用newNode()方法创建一个新的Node对象,存在在tab数组中
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 如果当前key对应的位置是有数据的,说明发生了hash冲突
            Node e; K k;
            // 当前key和原有的key相同,就将数组中数据赋值给变量 e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          //如果当前 p 是否是 红黑树结构 就调用 putTreeVal
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 当前不是红黑树结构,就是单链表结构
                for (int binCount = 0; ; ++binCount) {
                        //如果链表中Node中的next为null ,说明是单链表中尾部的数据
                     // e= p.next;
                    //  if(e == null)
                    if ((e = p.next) == null) {
                      // 调用newNode方法 创刊一个新的 Node,同时 将创建新的Node赋值给 p.next
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                  //当 新的key与原有的key相同 直接跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
          
          // e!=null 说明存在有相同的key 就把值进行覆盖然后 将原有的值 返回
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
              //这个方法在HashMap中没有做任何事情,只会在存在相同的key才调用 在子类LinkedHashMap中有具体的实现
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        //  该方法会在每次put的时候(没有存在相同key的时候)都会调用,在子类LinkedHashMap中有具体的实现
        afterNodeInsertion(evict);
        return null;
    }
// putTreeVal:循环遍历红黑树的节点,如果节点的key与新的key相同,如果相同返回原有的Node对象,不同就返回null
HashMap的put方法总结
  • 将全局的变量table赋值给局部的变量table,如果tab为null就调用reSize()进行初始化 将新建的Node[] 赋值给tab,将初始化好的数组长度赋值给 n。
  • 拿到key对应位置的数据,如果为null说明该位置之前没有存放过数据,则调用newNode方法创建一个新的数据存放到tab中
  • 拿到的key计算出来的hashCode对应的位置之前存在过,说明发生了hash碰撞,那么判断当前的key和存在的key是否真正的相等,相同则替换对象。
  • 如果key不相等,判断当前碰撞的位置的数据结构是否是黑红树,如果是就调用putTreeVal()方法,如果发现书的节点key与新的key存在相同,那么把旧的node返回赋值给e,否则返回null
  • 以上两者都不是,说明当前位置对应的数据结构就是一个单项链表,那么循环判断当前位置的node对象持有的下一个节点对象是否为null(e = p.next),为null就通过newNode方法创建一个新的节点,指向了p.next,并且判断当前容量是否超出了8(默认值),超出了就将当前的单链表结构转换成红黑树结构,调用方法treeifyBin(),然后跳出循环;如果当前位置的key和新的key相等也跳出循环;否则的话将p.next赋值给p,然后重复循环,知道满足当中的一个条件跳出循环。
  • 跳出循环后,判断当前e时候为null,不为null说明当前e持有的是原有的node对象,就重e中获取之前的旧值替换成新值,然后返回旧值。
  • 判断数组中的数据个数是否大于了阀值,大于了就进行扩容,调用reSize方法;然后调用了afterNodeInsertion(evict);并且返回null
  • get方法要么返回 null 要么返回相同的key对应的旧值。
resize方法
final Node[] resize() {
        //全局的table赋值给oldTab
        Node[] oldTab = table;
        // 保存之前的 Node数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //保存之前扩容的阀值
        int oldThr = threshold;
        //
        int newCap, newThr = 0;
        //如果oldCap大于0,说明扩容之前的 table不为null
        if (oldCap > 0) {
            // 如果之前的老table的长度大于了MAXIMUM_CAPACITY
            if (oldCap >= MAXIMUM_CAPACITY) {
                //就将扩容的阀值设置为Integer.MAX_VALUE;
                threshold = Integer.MAX_VALUE;
                //并且返回老的table,说明不在进行扩容操作了
                return oldTab;
            }
            // 将 老table的容量进行翻倍赋值给newCap,newCap小于MAXIMUM_CAPACITY
            //并且扩容之前的oldCap大于16,那么将原有阀值翻倍赋值给新的阀值存储
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //说明 老的table并没有赋值,oldTab为null
        //那么oldCap为0,这时oldThr大于0 就是阀值大于0 说明 initialCapacity大于0
        //HashMap是通过如下构造方法构建的
        //public HashMap(int initialCapacity)
        //public HashMap(int initialCapacity)
        //public HashMap(Map m)
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {// zero initial threshold signifies using defaults
            //能走到这个分支里面,说明 oldThr =0,oldCap<=0 ,也就是HashMap是通过 new HashMap()创建的
            // 新创建的HashMap的数组容量默认为16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //新的扩容阀值默认是(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 12
            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[] newTab = (Node[])new Node[newCap];
        table = newTab;
        //如果老的oldTab不为null 就将旧数据迁移到新的table中。
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        //如果是一个节点就将数据放到newTab的指定位置上。
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果当前节点的数据结构是红黑树的
                    else if (e instanceof TreeNode)
                        //就进行红黑树的split操作
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //否则的话就是一个单链表
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node 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;
                        }
                    }
                }
            }
        }
  //返回新的扩容后的table
        return newTab;
    }

resize方法的总结

  • resize主要是返回一个新的table和并进行扩容。
  • 如果oldTab不为null,就判断oldTable.length与MAXIMUM_CAPACITY做比较,如果大于等于 就将阀值设为值整形最大值,并且停止扩容;如果小于并且oldTable.length的2倍也小于MAXIMUM_CAPACITY并且oldTable.length大于等于默认的阀值就将阀值扩大为当前的2倍,数组容量扩大为2倍。
  • 在JDK1.7时 是头插法,到了1.8是尾差法。

HashMap的get()方法

public V get(Object key) {
        Node e;
        //根据key的hashCode去查找位置,返回对应的 node 从而拿到了value的值。
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

 final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
            //table不为null,并且根据key的hashCode找到存放的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
          // 如果该位置对应的第一个就是就返回链表头。
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
          //第一个不是
            if ((e = first.next) != null) {
              下一个是红黑树,就通过getTreeNode获取
                if (first instanceof TreeNode)
                    return ((TreeNode)first).getTreeNode(hash, key);
              //不是红黑树 是链表结构 就不但循环取 直到找到key相同的node,返回该node
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
get方法的总结
  • 根据key的hashCode值找到存放的位置对应的数据
  • 当链表的第一个数据就是就返回这个链表头
  • 如果不是的话,就判断当前节点的数据结构是否是红黑树,是的话就调用getTreeNode方法获取,不是的话就循环调用,比对key直到找到相同的key,返回对应的node;
  • 如果对应的key找不到相应的位置,就返回null

HashMap 1.7和1.8的差别

差异类别 1.7 1.8
数据结构 数组+链表(Entry[]) 数组+链表(Node[])+红黑树:
当链表的长度>8的时候转换成红黑树
数据存放的方式 key的hashCoded没有发生碰撞的时候,就采用数组存储,
发生碰撞了就采用单链表的数据结构
key的hashCoded没有发生碰撞的时候,就采用数组存储,
发生碰撞了就采用单链表的数据结构,当链表的长度>8的时候采用红黑树
数据插入的方式 头插法(原有数据向后移) 尾插法
扩容后,老数据的移动放肆 头插法(原数据会出现逆序,出现环形链表,死循环) 尾插法(不会出现逆序,环形列表,死循环等问题)
扩容时机 先判断大于阀值的时候在插入 数据先插入然后判断大于阀值,在扩容

两个问题

hash碰撞是怎回事以及怎么解决

当调用put方法的时候,会先进行key的hash计算,通过hash计算找到要存放的位置,如果此时这个存在数据,说明不同的key有相同的hashCode值,就发生了hash碰撞。

1.8之前采用数组+单链表的数据结构解决的hash碰撞的问题

1.8的时候采用数组+单链表+红黑树结构解决了这个问题。

我们知道1.8之前的单链表是采用头插法,在扩容的时候,容易出现逆序,死循环的问题,所以在1.8的时候加入红黑树 优化了1.7出现的问题。

为什么HashMap中的String,Interger适合作为key

因为包装类 String,Integer是fianl修饰的,保证了计算key时 hash的不可更改和准确性

有效减少了hash碰撞的几率。内部重写了equals()和hashCode()方法,不容易出现hash值计算的错误。

你可能感兴趣的:(Java基础_HashMap源码分析)