`

java深入学习Hashmap源码(JDK8)

阅读更多
关于JDK1.6、1.7、1.8三个版本,HaspMap的实现是有区别的,特别是1.8,对hashmap的结构进行了较大的变化。

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





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?如图



一是保证使用 和运算时能够最大的在位桶上均分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
  • 大小: 23.6 KB
  • 大小: 6.4 KB
分享到:
评论

相关推荐

    java_jdk8源码

    Java JDK8源码是Java开发人员深入理解Java平台工作原理的重要资源。对于任何希望提升Java技术水平,特别是对性能优化、并发编程或者想要成为一名更高级的Java开发者的人来说,研究JDK源码是不可或缺的一环。在这里,...

    jdk源码 jdk源码

    Java Development Kit (JDK) 源码是学习和理解Java平台核心机制的关键资源。它包含了许多关键组件的源代码,使开发者能够深入探索Java语言的底层实现,从而提升编程技巧,优化性能,并理解标准库的工作原理。在这个...

    JAVA JDK实例开发宝典源码

    Java JDK实例开发宝典源码是一份非常宝贵的资源,它涵盖了Java编程语言的各个方面,旨在帮助开发者深入理解和熟练运用Java JDK。这份源码集合包含了众多的实际示例,可以帮助初学者和经验丰富的开发者巩固基础,提升...

    jdk1.8 rt.jar 源码

    通过深入研究`rt.jar`源码,开发者可以提升对Java语言的理解,学习如何更有效地使用标准库,避免潜在的问题,并提升代码质量。此外,这也有助于培养良好的编程习惯,更好地遵循Java的设计原则和最佳实践。因此,`rt_...

    java源码之jdk源码

    通过深入学习和分析JDK源码,不仅可以提高编程技巧,还能增强对Java生态系统的理解,为解决复杂问题和开发高效系统奠定坚实基础。同时,了解`javax`、`com`、`org`、`sunw`等包下的类,如`javax.swing`的图形用户...

    javaJDK1.8 源码(.java文件) 新手教程资料

    Java JDK 1.8 源码新手教程资料是一份宝贵的学习资源,它包含了Java开发工具包的关键组件的源代码,让开发者能够深入理解Java语言的底层实现和工作原理。对于初学者而言,掌握这些源码是提升技能、增强问题解决能力...

    jdk源码jdk1.8.0_181

    通过深入学习JDK1.8.0_181的源码,开发者可以: 1. **理解API设计原则**:观察Oracle如何设计和实现标准API,为自己的项目提供设计灵感。 2. **提高性能优化能力**:了解内部实现后,能更准确地定位性能瓶颈,进行...

    深入jdk6.0源码

    《深入JDK6.0源码》是一本旨在帮助开发者深入了解Java Development Kit 6.0的书籍或课程资源。这个主题涵盖了Java语言的基础特性、语法规范以及开发环境的配置和使用,同时也深入到JDK6.0的核心源代码层面,为开发者...

    java jdk 实例宝典源码

    Java JDK实例宝典源码是Java开发者...通过深入研究Java JDK源码,我们可以学习到如何高效地利用Java提供的工具和类库,提高编程效率,同时也能提升对Java平台底层运作的理解,为成为更优秀的Java开发者打下坚实的基础。

    深入Java集合学习系列:HashMap的实现原理

    在JDK 8之后,为了优化性能,当链表长度达到一定阈值(通常为8)时,HashMap会将链表转换为红黑树,以实现更高效的查找、插入和删除操作。 HashMap的容量(capacity)是存储元素数量的上限,初始化时可以设置,默认...

    jdk1.6 源码jdk1.6 源码

    深入理解JDK 1.6的源码对于Java开发者来说至关重要,因为它揭示了语言核心库的工作原理,有助于优化代码、理解和解决潜在问题。 1. **Java虚拟机(JVM)**:JDK 1.6中的JVM是Java程序执行的基础,它的主要职责是...

    Java jdk源码

    Java JDK源码是Java开发人员深入...总的来说,研究Java JDK源码是一次深入学习之旅,它可以帮助开发者从底层理解Java的工作方式,提升编程技艺,解决复杂问题,并且更好地利用Java的特性来设计高效、可靠的软件系统。

    jdk1.8 sun源码

    这个"jdk1.8 sun源码"压缩包很可能包含了这些未公开的Sun Microsystems的源代码,使得开发者有机会深入研究Java平台的内部工作原理,这对于进行底层优化、理解和调试Java程序有着极大的帮助。然而,值得注意的是,...

    java,jdk源码

    Java是世界上最流行的编程语言之一,尤其在企业...对于有志于成为Java专家的开发者来说,深入学习JDK源码是必不可少的一步。无论是为了优化代码性能,还是为了开发自定义的JVM或工具,JDK源码都是不可或缺的参考资料。

    java-jdk源码学习

    Java JDK源码学习是深入理解Java编程语言的关键步骤,它能帮助开发者洞悉语言底层的工作原理,提升编程技能和优化代码的能力。JDK(Java Development Kit)是Java开发的核心工具集,包含了Java运行时环境(JRE)、...

    java 集合源码学习,jdk1.8集合类所有的源码讲解

    总的来说,深入学习Java集合框架的源码有助于理解其工作原理,优化代码性能,并能更好地应对并发、排序、查找等复杂问题。通过阅读源码,我们可以看到设计模式的应用,如工厂模式、装饰器模式等,这些都是成为优秀...

    jdk的部分源码(jdk)

    通过研究这部分源码,开发者不仅可以加深对Java语言的理解,还能学习到高级编程技巧,如性能优化、内存管理以及如何利用JDK提供的工具解决问题。这对于提升Java编程能力,尤其是进行系统级或框架级别的开发大有裨益...

    HashMap 源码分析

    《HashMap 源码解析——JDK11版本》 HashMap是Java中广泛使用的非同步散列表,其设计和实现是高效且灵活的。在JDK1.8之前,HashMap的底层数据...理解HashMap的源码对于深入学习Java集合框架和数据结构具有重要意义。

    JDK1.6源码 JAVA

    JDK1.6版本是Java历史上的一个重要里程碑,它的源码对于我们深入理解Java语言的工作原理以及学习Java虚拟机(JVM)的内部机制具有极大的价值。 1. **Java运行环境** JDK1.6的源码揭示了Java运行环境的实现,包括类...

    面试必考之HashMap源码分析与实现

    在Java编程语言中,HashMap是集合框架中一个重要的类,用于存储键值对的数据结构。面试中,HashMap的源码分析与实现是一个常见的考察点,...深入学习和实践HashMap源码,能够帮助我们更好地理解和优化Java应用程序。

Global site tag (gtag.js) - Google Analytics