Java容器——HashMap(Java8)源码解析(二)

在前文中介绍了HashMap中的重要元素,现在万事俱备,需要刨根问底看看实现了。HashMap的增删改查,都离不开元素查找。查找分两部分,一是确定元素在table中的下标,二是确定以指定下标元素为首的具体位置。可以抽象理解为二维数组,第一个通过哈希函数得来,第二个下标则是链表或红黑树来得到,下面分别来说。

一 哈希函数

说到HashMap,最值得引起注意的自然是接近常数级别的操作速度,大家也都知道是利用哈希表来实现。这里面就涉及到几个问题:

1 如何计算哈希;

2 如何设置哈希表;

3 哈希冲突后的处理方式。

首先是哈希函数,HashMap中最关键的函数之一。官方给的注释也比较详细。先简单说一下算法的实现。计算哈希是针对HashMap中节点Node<Key,Value>中的Key进行。每个对象都有各自的hashcode方法,计算其哈希值,直接用这个哈希值,再对哈希表长度取余不可以吗,简单高效。答案是可以,但是这样哈希碰撞比较厉害,这样将大大降低HashMap的速度,也浪费空间。

目前使用的方法是,使用算出来的哈希值高16位与低16位相亦或,按照官方注释:这是速度,通用性,bit位均匀分布的折中。这里的位运算和移位运算速度都很快,在不同平台上都能找到直接指令操作,同时保留了高位和地位的特性。


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

哈希函数不能单独来看。上一篇中我们说到,HashMap的初始大小DEFAULT_INITIAL_CAPACITY是为16,计算出来的哈希值肯定是不可能全塞进去的,如何确定一个Key究竟应该放到哪个下标中呢?

    int index = (length - 1) & hash;

方法是表长度减一与哈希值相与。这里又引出了另外一个问题,HashMap的长度设定,选择比初始值大的最小的2的正整数次幂,计算方法如下。

    /**
     * Returns a power of two size for the given target 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;
    }

 这个方法如何计算出2的N次幂呢?2的N次幂是最高bit位为1,其他位为0的正整数,那么2的N次幂减一就是最高位变0,后面的都变成1。为了达成这个目的,这个算法首先计算了2的N次幂减一。一个正整数最高bit位肯定为1,右移一位相或后,最高两位都为1,再右移两位后与原数相与,最高四位都为1,以此类推。因为int最多也就32位,这种方式保证了最后所有位都为1,加一后得到2的N次幂。有一个问题,为什么刚开始的时候要减一呢?是为了防止一开始就是个2的N次幂,这样就对不上了。这是一个很巧妙的算法。

再回到上面这个int index = (length - 1) & hash 计算下标的算法。有了上面的分析,很容易得到,length-1是2的N次幂减一,除最高位外都为1,用这个数与哈希值相与,实际上就是取哈希比特位上的值。注意之前已经说了,哈希算出来的是高16位与低16位相亦,保留了高低位的特性,这样一来就能有效利用元素的哈希进行区分同时加快计算速度。

二 位置查找

通过上面的哈希函数和下标函数,可以准确找到元素所在下标。接下来就是找到具体所在位置。前面说到了,HashMap冲突后的解决方式有两种,冲突数量少时用链表,数量多时用红黑树。下面就分别来看这两种不同方式的查找。

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        // 结点表
        Node<K,V>[] tab;
        // 首结点,临时结点,表长,Key临时变量
        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;
    }

在上面的代码中,基本注释很清楚了,链表查找就是顺序查找,时间复杂度是O(n)。按红黑树查找的方式如下所示。

        /**
         * Calls find for root node.
         */
        final TreeNode<K,V> getTreeNode(int h, Object k) {
            return ((parent != null) ? root() : this).find(h, k, null);
        }        


        /**
         * Finds the node starting at root p with the given hash and key.
         * The kc argument caches comparableClassFor(key) upon first use
         * comparing keys.
         */
        final TreeNode<K,V> find(int h, Object
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值