欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java深入学习Hashmap源码(JDK8)

程序员文章站 2022-07-15 13:59:41
...
关于JDK1.6、1.7、1.8三个版本,HaspMap的实现是有区别的,特别是1.8,对hashmap的结构进行了较大的变化。

1.6整体采用的是位桶+链表的方式,而1.8使用的是位桶+链表+红黑树实现,链表的阈值是8,超过后就由链表变成红黑树,大大增加了查询的效率。


java深入学习Hashmap源码(JDK8)
            
    
    博客分类: java基础 javajdk 


1 涉及到的数据结构:处理hash冲突的链表和红黑树以及位桶

    //Node是单向链表,它实现了Map.Entry接口
    static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;
        //构造函数Hash值 键 值 下一个节点
        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;
        }
        //判断两个node是否相等,若key和value都相等,返回true。可以与自身比较为true
        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 extends LinkedHashMap.Entry {
        TreeNode parent;  // 父节点
        TreeNode left;	//左子树
        TreeNode right;//右子树
        TreeNode prev;    // needed to unlink next upon deletion
        boolean red;	//颜色属性
        TreeNode(int hash, K key, V val, Node next) {
            super(hash, key, val, next);
        }

        //返回当前节点的根节点
        final TreeNode root() {
            for (TreeNode r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }

    transient Node[] table;//存储(位桶)的数组


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


2.构造函数

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


Map中,threshold = initialCapacity * loadFactor;所以在这里面,loadFactor越小,占用的空间就越多,查询检索的效率就越高;反之loadFactor越大,占用空间就小,而查询检索效率就低。由于haspMap的定义就是空间换时间,loadFactor负载因子值设置非常重要。

默认情况下,loadFactor为0.75,threshold 为12;如果loadFactor为1,threshold 就是16,为什么说loadFactor越大,空间占用小?比如一个为15的Map放入,那么在loadFactor在0.75时,是需要扩容的,threshold 就为24,此时初始空间为32;而为1的情况下不需要扩容
,所以占用空间就小。扩容后,位桶table也会增大,为旧的两倍,对比不扩容,位桶上hash冲突会减少很多,检索查询效率自然会高些。


3、扩容机制

构造hash表时,如果不指明初始大小,默认大小为16(即Node数组大小16),如果Node[]数组中的元素达到(填充比*Node.length),就会进行扩容,由于扩容存在数组复制,扩容是比较耗费时间的。代码

 
  final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 旧的容量已经达到极限
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 旧的容量翻倍后没有到极限值,且大于等于默认值
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; //扩容值翻倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // 未设定使用默认值
            newCap = DEFAULT_INITIAL_CAPACITY;
            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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

        //数组转到到新的数组中,分红黑树和链表讨论
        table = newTab;
        
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null) // 空值
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)  // 若为红黑树,直接分离
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // 链表时,需要复制新的
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> 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;
                        }
                    }
                }
            }
        }
        return newTab;
    }


4、put/get时Node[] table位置

由于都是通过hash确定代码,

 
  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length; //table为空的时候,n为table的长度
        if ((p = tab[i = (n - 1) & hash]) == null) // (n - 1) & hash 与1.6中indexFor方法的实现相同,若i位置上的值为空,则新建一个Node,table[i]指向该Node。
            tab[i] = newNode(hash, key, value, null); 
        else {
            // 若i位置上的值不为空,判断当前位置上的Node p 是否与要插入的key的hash和key相同
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//相同则覆盖之
            else if (p instanceof TreeNode)
            // 不同,且当前位置上的的node p已经是TreeNode的实例,则再该树上插入新的node。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            // 在i位置上的链表中找到p.next为null的位置,binCount计算出当前链表的长度,如果继续将冲突的节点插入到该链表中,会使链表的长度大于tree化的阈值,则将链表转换成tree。
                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;
    }

整体过程:
1、判断键值对数组tab[]是否为空或为null,否则resize();
2、根据键值key计算hash值得到插入的数组索引i,如果tab[i]==null,直接新建节点添加,否则转入3
3、判断当前数组中处理hash冲突的方式为链表还是红黑树(check第一个节点类型即可),分别处理。
4、普通中关键代码 (n - 1) & hash; hash先由key值通过hash(key)获取,再通过 hash &(n即为length-1)得到所在数组位置。一般对于哈希表的散列常用的方法有直接定址法,除留余数法等,既要便于计算,又能减少冲突,但是取模中的除法运算效率很低,所以HashMap则通过hash &(length-1)。

为什么要减1?如图

java深入学习Hashmap源码(JDK8)
            
    
    博客分类: java基础 javajdk 

一是保证使用 和运算时能够最大的在位桶上均分hash值,减少hash冲突,上图在未-1情况下,都只有一种结果;数组的length永远是2的N次方,那么length-1最后转成2进制,最后一位都是1,& 操作就可能出现0 或 1,如果不减1,那么2进制最后一位永远都是0,& 的运算结果只有0,明显增加了hash的冲突;
二是保证了结果都在length的长度内,& 操作后,结果永远都是 <= length 或者length -1,但在极端情况下出现 Node[length],就会出现问题,而Node[length -1]不会。

在HashMap实现中还可以看到如下代码取代了以前版本JDK1.6的while循环来保证哈希表的容量一直是2的整数倍数,用移位操作取代了循环移位。


    //这段代码保证HashMap的容量总是2的n次方
    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;
    }



参考:

http://www.2cto.com/kf/201505/401433.html
http://www.cnblogs.com/hfczgo/p/4033283.html?utm_source=tuicool&utm_medium=referral
  • java深入学习Hashmap源码(JDK8)
            
    
    博客分类: java基础 javajdk 
  • 大小: 23.6 KB
  • java深入学习Hashmap源码(JDK8)
            
    
    博客分类: java基础 javajdk 
  • 大小: 6.4 KB
相关标签: java jdk