Java源码-hashmap

API要点汇总

  • 允许空值与空键
  • 与Hashtable大致相同,但是不同步
  • 不保证映射顺序,特别是不能保证order(不知道翻译成什么)在其中不随时间变化
  • HashMap实例有两个影响其性能的参数:初始容量和负载因子(load factor)。容量是哈希表中的桶数,初始容量就是创建哈希表时的容量。负载因子是衡量在哈希表的容量被自动增加之前,哈希表被允许获得多少满的度量。当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即重新构建内部数据结构),这样哈希表的桶数大约扩展两倍。

不理解的要点:

  • 在hashmap需要存储许多映射时,需要创建更大容量的Hashmap(比它自己扩展效率高),但使用具有相同hashCode()的多个键肯定会降低任何散列表的性能。它建议当键是可比较的时,该类可以使用键之间的比较顺序实现(?),但这个是不同步的,于是又有Map m = Collections.synchronizedMap(new HashMap(…));(?)。
  • 这个类的所有“collection view methods”返回的迭代器都是快速失败的,如果在创建迭代器之后的任何时候对映射进行结构上的修改,除了通过迭代器自己的remove方法之外,迭代器将抛出ConcurrentModificationException。(不是很了解啊)

关系图

在这里插入图片描述
在这里插入图片描述

函数概要

功能描述
构造函数能够用容量、 负载因子、其它hashmap构造
删除删除所有、依据某个键值、键对值删除
克隆返回浅克隆(键对值不被克隆)
set转换为set)
获取获取值
添加以键对值、map等数据形式添加
替换替换键值对、替换某一键对应的值

有些函数没有功能没有列出,例如compute,这个是java 8新增,不熟悉,等以后用到再更新吧。

源码解析

其函数挺多的,我其实只是挑部分感兴趣的代码看,说实话我也不可能每个函数都看一遍,对于没有实践体会的看起来实在是枯燥,也就不看了,等以后遇到使用问题再扒出来看吧。

hashmap

在这里插入图片描述
如图,它的实现方式是一种数组(API指的是桶)加链表的组合,HashMap 则使用了拉链式的散列算法,并在 JDK 1.8 中引入了红黑树优化过长的链表,也就是将链表部分换成红黑树,优化查询等操作效率,在进行增删查等操作时,首先要定位到元素的所在桶的位置,之后再从链表中定位该元素。

构造方法

提供如下四种构造函数:
在这里插入图片描述在这里,我只列出两种,另外两种都十分简单:

HashMap(int initialCapacity, float loadFactor):

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

构造函数前面的部分是十分容易理解,关键是最后一个tableSizeFor函数,它设置了阈值,我们接着看:

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

以上是tableSizeFor的具体实现,位右移运算,而且,是不是很懵逼?试着在纸上画画,你会发现,在经历过这些操作之后,原始数据所在位数的低位数将全部变为1,最后n + 1显然得到的是2的整数幂,比如你输入一个5,最后会得到一个8。是不是很神奇!
下图是我在网上找的分析图,供大家理解(实在懒,不想画):
在这里插入图片描述
HashMap(Map<? extends K, ? extends V> m)

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

由图上的源码可以看出,这种构造函数将负载因子设置为默认值(0.75),随后调用了另一个函数,我们来看这个函数:
putMapEntries:

final void putMapEntries(Map<? extends K, ? extends V> 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<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

有关evict参数,其注释这样解释:evict false when initially constructing this map, else true (relayed to method afterNodeInsertion).
在进行数据的填充时,它使用到了putVal函数,实际上,Hashmap的put函数就是直接调用这个putVal函数,接下来看看这个函数:

插入

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //如果当前 哈希表内容为空,新建,n 指向最后一个桶的位置,tab 为哈希表另一个引用,将插入动作延期进行
        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;
            //如果第一个就是想要找的数据时,就将e指向此节点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
             //如果是TreeNode类型,就调用红黑树的插入方法
            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;
                }
            }
            //最终决定是否更新数据,首先判断要插入的键值对是否存在 HashMap 中
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //统计修改次数,方便fail-fast的判断
        ++modCount;
        //判断是否超过阈值,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

首先,我们可以明确的是这个函数的final属性,传入的参数hash(key)是对于key进行hash计算:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);接下来我们看到了HashMap 的底层数据结构之一的链表数组:Node<K,V>[] table;它被声明为transient类型(Java中transient关键字的作用,简单地说,就是让某些被修饰的成员属性变量不被序列化,能够节省存储空间)

查找

查找函数的核心算法封装在如下函数内:

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; 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 && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
                //否则查询下一节点
            if ((e = first.next) != null) {
            //若后续为 TreeNode,按照红黑树类型进行查找
                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;
    }

查询的整体思路并不复杂,但在第一个条件语句中的tab[(n - 1) & hash]使我困惑,虽然大致知道是找桶所在的位置,但不清楚实现原理,查找网上相关解析,得到如下解释:

这里通过(n - 1)& hash即可算出桶的在桶数组中的位置,可能有的朋友不太明白这里为什么这么做,这里简单解释一下。HashMap 中桶数组的大小 length 总是2的幂,此时,(n - 1) & hash 等价于对 length 取余。但取余的计算效率没有位运算高,所以(n - 1) & hash也是一个小的优化。举个例子说明一下吧,假设 hash = 185,n = 16。计算过程示意图如下:
在这里插入图片描述

删除

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<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) {
            //判断是否属于TreeNode,是就采用红黑树的函数得到节点
                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);
                }
            }
           // 删除节点,并修复链表或红黑树
            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;
    }

总的来看,基本思路跟查询操作类似。

替换

public V replace(K key, V value) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) != null) {
            V oldValue = e.value;
            e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
        return null;
    }

上面一些代码很简单,就是查询所在节点是否存在,然后进行替换擦作就行,所以就不再赘述。

总结

首先对这篇文章也不是很满意,但总归比上篇好些,有一下几点:

  • 读起来思维比较混乱,虽然我也是从构造函数看起,遇见陌生函数就直接穿插其代码分析,显得抓不住重点
  • 每个代码进行分析之后,没有相关的函数思路总结
  • 有些重点没有阐述清楚,比如阈值与容量和负载因子的关系

抛开这些之外,阅读源码真是让我收获良多,特别印象深刻的有关位运算的应用,我们都知道位运算能极大提高效率,但实际应用上却远远不如,其中有关利用位运算实现求一个数的最接近的幂、实现求桶所在的位置等都让我惊呼神仙操作,还有许多地方的思维逻辑十分严谨,总之这是一个大工程,许多东西我还没有详细去看,比如有关红黑树的操作,有关扩容机制的详细内容,自己还有很长一段路要走,看源码是一件很枯燥的事,但与此同时,看大佬的代码真的让人受益匪浅!
共勉!下次的文章自己还会改进有关文章表达方面的问题的。

推荐几篇文章,我认为写的非常详细,至少比我写的不知道好到哪去了,有一些有关位运算的思路我都是从中才明白的:

HashMap源码详细解析(JDK1.8) (推荐!)
Java 集合深入理解(16):HashMap 主要特点和关键方法源码解读

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值