关闭

集合源码学习(七):HashMap(Java8)

397人阅读 评论(0) 收藏 举报
分类:

Java8中,新加了很多新特性,特别是集合,分割迭代器,Stream,Functional Interface等等,Java8中的HashMap也和以往的实现略有不同。
这些天看了好久的HashMap,理清了HashMap的结构以及实现原理,听我慢慢分析。

HashMap是什么?

/**
 * 基于Map接口实现,允许null值和null键。 
 * HashMap和HashTable很相似,只是HashTable是同步的,以及不能为null的键
 * HashMap有两个重要参数,capacity和load factor 默认的load factor大小为0.75
 * iterator是fail-fast的。
 * 
 */
public class HashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>,
        Cloneable, Serializable {

如上,基本的特性在代码里面注释了,HashMap实现了Map接口,是一个基于散列表的Map类,Map接口的特性就是存储键值对。散列表是一种存储结构,它可以通过散列函数直接访问到目标数据值,所以在定位下标方面可认为为o(1)。

HashMap重要的字段

/**
     * Hash的默认大小
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * HashMap最大存储容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 增长因子,意思就是当table已经用到table.length*0.75时,就需要扩容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 由链表存储转变为由树存储的门限,最少是8
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 由树存储节点转化为树的节点,默认是6,即从8到6时,重新转化为链表存储
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 当由链表转为树时候,此时Hash表的最小容量。 也就是如果没有到64的话,就会进行resize的扩容操作。
     * 这个值最小要是TREEIFY_THRESHOLD的4倍。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

上述代码中解释了HashMap中重要字段的意思,相信大家一看就会有大概理解了。

由于在Java8的实现中,当经过hash函数计算得出的下标地址冲突到一定范围时,就会 把冲突的数据用链表的形式连起来,而当用链表数据大于一定范围时,就会将链表转化为红黑树存储。
关于链表,Java中典型应用就是LinkedList,可以看:LinkedList
而红黑树,Java中典型应用是TreeMap, 可以看:TreeMap

HashMap结构概括

首先HashMap会有一个基准数组table:

/**
     * 存储数据的table集合,长度一定为2的倍数
     */
    transient Node<K, V>[] table;

第一步,table是一个数组,所以会有下标,HashMap首先会根据传入每个节点的(key,value)中的key,算出应该放到哪一个下标的数组中。
第二步,如果此下标数组为null,那么就直接放入,不为null,就走到第三步。
第三步,如果不为null,就说明冲突了,检查key的equals方法,看是否和原节点的key相同,相同就直接替换,否则进入第四步。
第四步,很明显冲突了,而且是不相等的冲突,这是检查是否需要将此下标的存储结构换为红黑树,不需要就是链表直接在末尾插入节点,否则进入第五步。
第五步,原有的链表结构不足以支撑存储了,所以换为红黑树存储了,此时就是往红黑树中插入该节点。
上述步骤省略了链表与红黑树之间转换。
整个存储结构图如下(没有放入红黑树存储结构)(省略了value值)
这里写图片描述

HashMap的存储节点

首先看HashMap的Node节点代码,这就是table数组所使用的结构。如果冲突的是链表存储,则直接是这种结构存储。

    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        V value;
        // 可能要连接下面的链表,所以会有个next
        Node<K, V> next;
        ...
        省略

再看红黑二叉树存储的结构:

    static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
        TreeNode<K, V> parent; // red-black tree links,红黑树,保证是一棵平衡二叉树
        TreeNode<K, V> left;   //左子树
        TreeNode<K, V> right;  //右子树
        TreeNode<K, V> prev; // 指向下一个节点,类似于线索二叉树, needed to unlink next upon
                                // deletion,删除时记得置null
        boolean red;        //红黑特性
    ...
    省略

这是当不用链表表示冲突值时候,用红黑树表示时候的节点。由上可知,TreeNode继承自LinkedHashMap.Entry,
而它的结构如下:

static class Entry<K,V> extends HashMap.Node<K,V>

所以,其实TreeNode是Node的一个子类,所以table中也是可以存放TreeNode的。

hash值的计算方法

这里首先介绍hash值的计算方法,这也是一门有学问有艺术性的东西。
在HashMap中要注意区分hashCode和hash两个方法,他们是不通的!!
这里就不细说hashCode了,看下面hash方法

/**
     * 自己低位和高位异或操作,能够降低冲突 计算冲突,结合高16位与低16位
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

hashCode()是一个native方法,意味着方法的实现和硬件平台有关,默认实现和虚拟机有关,对于有些JVM,hashCode()返回的就是对象的地址,大多时候JVM根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,并返回。
所以hashCode返回的是一个32位的2进制数值,而Java8中这样的实现,保证了对象的hashCode的高16位的变化能反应到低16位中,相比较而言减少了过多的位运算,是一种折中的设计。

table的容量只能是2的倍数

table容量为2的倍数时,有利于下一个缓解的计算table的下标,另一个方面,虽然在HashMap中,提供了一个构造方法:

public HashMap(int initialCapacity, float loadFactor) 

看似提供了初始容量的方法,但是这个方法最后一行代码中调用了另一个方法tableSizeFor来确定table的容量:

/**
     * hashMap大小只能为map的倍数。 最终会返回一个最适合cap的2的倍数
     * capacity.
     */
    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;
    }

所以最终table的length只能是2的倍数。

table下标计算方法

有点基础的读者应该知道,一般索引,都是传入的是一个键(key),而这里的key是一个引用类型,通俗点,就是个类(class),既然是个class,那又怎么获取下标呢?即怎么与下标联系起来呢?看下面代码:

tab[(n - 1) & hash]

没错,就是通过这样的方式,其中hash=hash(key),n=table.length这行端代码就相当有艺术了。
由前面知道,table.length是一个2的倍数,随意化成2进制就是开头一个1,后面n个0。随意当减1后,就会变成一排1,
之后,在与刚刚得到的hash(通过高位和低位计算后得到的hash)值做二进制与操作,因为(n-1)的高位都是0,所以最终只会截
取到hash的后log(n)-1位,会得到一个范围在0~table.length的值,这个值,就是数组的下标。是不是很有艺术。

由于hash是由key的hashCode的高16位与低16位经过异或而得,混合了原始哈希码的高低位,大大的提升了随机性,也让碰撞机率大大降低。

put方法

前面已经讲了put方法的基本过程,下面再细看看putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) ,其中,如果key=null,那么hash(key)=0,所以是能够存放null值的。方法实现代码:

    /**
     * 插入值, onlyIfAbsent,为真的话,就是不替换,无就插,有就不插 Implements Map.put and related
     * methods evict,表示需要调整二叉树结构,LinkedHashMap中需要
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;   //存放table
        Node<K, V> p;      //存放以前存放在table[(n-1)&hash]的节点,如果有
        int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;          //判断是否需要扩容
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 没有数据,就是放一个链表头节点
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K, V> e;
            K k;
            if (p.hash == hash
                    && ((k = p.key) == key || (key != null && key.equals(k))))
                // 一模一样,连key也equals后相等时
                e = p;
            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;
                    //其中,如果key的equals也相等,就直接替换
                    p = e;
                }
            }
            // 替换操作,key一样,旧值换为新值
            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();
        //LinkedHashMap使用
        afterNodeInsertion(evict);
        return null;
    }

具体代码分析已经注释到了代码里面。

get方法

如下代码:

    /**
     * 根据key返回它的值。
     */
    public V get(Object key) {
        Node<K, V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

如上代码所示,可以获取null值。

    /**
     * 根据key返回值。 也就是先算hash,在找到其位置,在看是否有因冲突而产生的链表或者二叉树。
     */
    final Node<K, V> getNode(int hash, Object key) {
        Node<K, V>[] tab;   //指向table,这样如果对table加锁,自己还是能够只读的
        Node<K, V> first, e;
        int n;
        K k;
        if ((tab = table) != null && (n = tab.length) > 0
                && (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // 总是检查是否为头节点。
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    // 二叉树
                    return ((TreeNode<K, V>) first).getTreeNode(hash, key);
                do {
                    // 链表
                    if (e.hash == hash
                            && ((k = e.key) == key || (key != null && key
                                    .equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

基本的由key获取节点的过程大致是这样,代码中已有注释,这里就不多讲。

remove方法

如下代码:

    /**
     * 根据key,删掉这个节点。
     */
    public V remove(Object key) {
        Node<K, V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null
                : e.value;
    }

接下来看具体的removeNode方法:

    /**
     * 删除某一个节点。
     * @param matchValue
     *            如果为真,那么只有当value也想等时,才能删除。
     * @param movable 能否删除

     */
    final Node<K, V> removeNode(int hash, Object key, Object value,
            boolean matchValue, boolean movable) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, index;
        if ((tab = table) != null && (n = tab.length) > 0
                && (p = tab[index = (n - 1) & hash]) != null) {
            //寻找node节点过程
            Node<K, V> node = null, e;
            K k;
            V v;
            if (p.hash == hash
                    && ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash
                                && ((k = e.key) == key || (key != null && key
                                        .equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //node节点就是已经找到的,符合条件的要删除的节点。
            if (node != null
                    && (!matchValue || (v = node.value) == value || (value != null && value
                            .equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

具体代码和前面的get方法相似,先找到节点,然后在判断哪种方法删除,以及删除之后的调整。

containsValue方法

和containsKey方法不同,它可以通过先散列,在判断key是否equals来判断是否含有这个key,而containsValue方法,则是直接暴力枚举所有value,然后得出有这个value,性能较差。

    /**
     * 在map中如果至少有一个value的值为value,就返回true。 ,注意下面有个双重循环,一个是循环数组,一个是循环链表(二叉树)。
     */
    public boolean containsValue(Object value) {
        Node<K, V>[] tab;
        V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K, V> e = tab[i]; e != null; e = e.next) {
                    //在TreeNode中,next属性也够用,因为TreeNode的父类是Node
                    if ((v = e.value) == value
                            || (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }

resize方法

前面讲过,当table的使用量到达length*loadFactor时,机会触发扩容操作,扩容操作的基本流程为:
1、判断是否需要扩容
2、将老数组table的元素,一个一个遍历并插入到新数组newTable中
3、更改相应的字段属性值。

    /**
     * 初始化使用,
     * 或者将hashmap大小调整为2的倍数级使用。
     */
    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) {
            // 如果当前size大于最大容量,则下一次就是int的最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 减少容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY
                    && oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        } else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else { // zero initial threshold signifies using defaults
            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)
                        // 当这个位置没有东西时候,就直接取莫放在这里。,重新计算hash值以便。
                        newTab[e.hash & (newCap - 1)] = e;

                    else if (e instanceof TreeNode)
                        // 是二叉树节点
                        ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                    else { // preserve order
                            // 仅仅是链表节点。
                        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;
    }

HashMap里面的iterator

HashMap里面具有下面几种Iterator:
HashIterator:普通Iterator的父类
KeyIterator:key的Iterator,继承自HashIterator
ValueIterator:Value的Iterator,继承自HashIterator
EntryIterator:key和value的Iterator,继承自HashIterator
同样的,HashMap里面也有Spliterator:
关于何为Spliterator,请看这篇: 集合源码学习:Spliterator
但是就目前Java8的源码来看,HashMap里面的分割列表,它是基于table的元素进行迭代的,啥意思呢?
在就是在trySplit方法里面,仅仅是对table进行横向的分割,类似于对数组的分割。
而在tryAdvance中,只会对table[current]进行以下 的遍历,即遍历链表或二叉树。如果current为null,则向下找一个不为空的table[current],找到后,只遍历一个table[current]找的终点则是本Spliterator的fence。
而在forEachRemaining中,遍历多个,和tryAdvance不同的时,它会遍历本Spliterator所有不为null的table[current]。

学习过程中,从很多文章中学到了知识:
http://www.importnew.com/20121.html
https://www.zhihu.com/question/20733617/answer/111577937
http://www.cnblogs.com/tonyluis/p/5671873.html
http://blog.csdn.net/ghsau/article/details/16843543

1
0
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

java8 hashMap排序的新认识

利用java8 对hashMap 进行排序后的新认识.
  • jianjun4833
  • jianjun4833
  • 2017-05-04 17:28
  • 1497

ConcurrentHashMap源码分析(JDK8版本)

注:本文源码是JDK8的版本,与之前的版本有较大差异 ConcurrentHashMap是conccurrent家族中的一个类,由于它可以高效地支持并发操作,以及被广泛使用,经典的开源框架Sprin...
  • u010723709
  • u010723709
  • 2015-08-26 19:10
  • 19331

JAVA8 hashmap源码阅读笔记(红黑树链表)

一:hashmap的13 个成员变量 static final int DEFAULT_INITIAL_CAPACITY = 1 < 数组默认初始容量:16 static final int MAXI...
  • crpxnmmafq
  • crpxnmmafq
  • 2017-07-18 22:08
  • 4835

Java 8集合框架源码学习——HashMap

心得:相较于JDK 1.7,Java 8中的HashMap有了较大的性能提升。修改了hash和resize方式,增加了红黑树的支持。学习参考资料: (1)疫苗:Java HashMap的死循环;1....
  • Zerohuan
  • Zerohuan
  • 2015-12-18 13:56
  • 1723

【Java8源码分析】集合框架-HashMap

一、HashMap的存储结构总共有两种存储类// 1. 哈希冲突时采用链表法的类,一个哈希桶多于8个元素改为TreeNode static class Node implements Map.Entr...
  • linxdcn
  • linxdcn
  • 2017-05-10 22:53
  • 982

Java8集合源码解析-HashMap

寻找志同道合小伙伴一起交流学习,欢迎加入* 点击链接加入群【java编程技术交流】:https://jq.qq.com/?_wv=1027&k=49y8iMS*1 概述HashMap是基于哈希表实现...
  • qq_33589510
  • qq_33589510
  • 2017-06-03 12:45
  • 347

java HashMap源码分析(JDK8)

这两天在复习JAVA的知识点,想更深层次的了解一下JAVA,所以就看了看JAVA的源码,把自己的分析写在这里,也当做是笔记吧,方便记忆。写的不对的地方也请大家多多指教。 JDK1.6中HashM...
  • samjustin1
  • samjustin1
  • 2016-08-30 16:59
  • 302

Java8 - HashMap源码

java8 - HashMap源码解析,哈希表与红黑树的结合。
  • u013124587
  • u013124587
  • 2016-09-24 18:24
  • 6173

Java 8 中HashMap源码分析

HashMap的系统介绍: HashMap实现了Map接口(注意:map类容器都没有实现Collection接口,只有set,list这类的容器才实现Collection),其对一般的基本操作(pu...
  • do_smile
  • do_smile
  • 2015-07-12 20:51
  • 511

Java8 HashMap源码分析

概述相比较Java7中的链表组合存储,Java8中的HashMap有了大量改进,最为明显的就是Java8中采用数组+链表+红黑树的方式对元素进行存储,这样安全和功能性完备的情况下让其速度更快,同时减少...
  • mq2553299
  • mq2553299
  • 2017-08-07 20:16
  • 163
    个人资料
    • 访问:135006次
    • 积分:3247
    • 等级:
    • 排名:第12111名
    • 原创:199篇
    • 转载:16篇
    • 译文:0篇
    • 评论:25条
    contact

    疑问或者想撩:

    luoan007@gmail.com

    回复会在三天之内~
    博客专栏
    设计模式整个代码及类图
    设计模式相关代码以及类图的整个项目都托管到了github上,包括各个模式的示例代码以及类图,类图则用的visio绘制,整个visio文件也在该项目上,这里贴上地址: https://github.com/anLA7856/design 如果觉得有帮助,star一下呗~我会非常开心的
    最新评论