资讯专栏INFORMATION COLUMN

Java容器类研究8:HashMap

zhichangterry / 516人阅读

摘要:的值是在上述方法中处理过的值,通过与当前容量进行,直接获取到哈希表的位置。策略二,如果已经很大了,扩容已经不可取,那么就采用红黑树结构转化链表。红黑树的创建不再详述。红黑树的根就是中第一个节点。

java.util.Map

Map中的自我引用

需要小心用易变的对象作为Map的key,这会导致Map的行为无法预测。Map也不可以将自己作为key,可以作为value,但是会导致equals和hashCode方法不是well defined.

Map中有些操作涉及递归的遍历,如果Map自我引用,则有可能出现异常。这些方法包括:clone,equals,hashCode和toString。

Map可以将null作为key和value

这是Map接口自己默认实现的一个获取指定key的value,当没有该key时可以获取一个默认值得方法。这就给用户提供了更灵活的选择来处理map中没有存这个key的情况。这里比较有意思的一点是在用v = get(key)判断一次后,为什么又用containsKey(key)再判断一次,因为有的map中是允许存null作为value的,所以有key在Map中,但是value为null的情况。

    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }
HashMap 如何求int型数的最近2次幂

HashMap中,用户可以初始化容量,但是HashMap中容量皆为2的次幂,所以会把用户预设的值先转为最近的2的次幂。tableSizeFor方法求出的结果总是大于或等于cap,且最接近cap的一个2的次幂。当然,对于大于2^30的数,会返回-2147483648,所以这里会判断n为负数的情况,同时,设置了HashMap的最大容量应该为2^30(MAXIMUM_CAPACITY)。

有意思的是经过如下几步就能求得一个2的次幂,下面方法所做的是先求得一个2^n-1,然后+1。获得一个2^n-1的过程也很巧妙,就是不停的拷贝1n |= n >>> x;将前x位的1拷贝到了接着的x位。通过几步位操作就获得了目标值,不得不赞叹开发者的聪明。

    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;
    }
HashMap的table

HashMap的table就是一个Node的数组,大小一致保持2的次幂。Node就是HashMap中存储的元素,它有哈希值、key、value和存储下一个Node的next属性。处理冲突的方法是闭哈希方法,也就是有相同的hash值的Node会用链表串起来。

HashMap中的hash值如何计算
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

将key的hashCode的高位和低位部分通过异或合并,使得低位和高位的值都能在hash时发挥作用。因为哈希表会用一个掩码来取hashCode的后面一段,如不采用上述方法,前面一部分的hashCode就被忽略了。如果一组key恰巧只在前段的hash值有所差异,而后段都是相同的,则会出现大量的冲突,这种情况在实际中是很可能存在的。

如何将Node哈希到table中

公式如:table[node.hash & (capacity - 1)] = node。node的hash值是key在上述hash()方法中处理过的值,通过与当前容量-1进行mask,直接获取到哈希表的位置。

HashMap的table的扩张

如上所述,table大小保持2的次幂,扩张的步骤:

申请一个容量乘以2的新Node数组。

遍历table,将原来的元素依次copy到新数组上,这里原来的链表会进行分裂。因为table的扩大,Node的hash值会被多mask一位,所以Node被Hash到的位置也会变化,Node要么保持原位置,要么就是在相对于原位置有一个旧容量大小的偏移。

这里不会将原来的元素重新进行一次hash的过程,重建原来的hash table,这样代价是比较大的。这里就利用了Node位置变化的规律,直接将原来的链表分裂为两个。

虽然已经进行了优化,但是该过程代价还是比较高的,时间复杂度为原table大小+元素数量

table中链表太长如何处理

当Node发生多次冲突,在一个hash值下建了一个很长的链表,这会导致查询的代价越来越大,这里采用了树结构来减轻这种问题。

具体过程是,当某个hash值下的链表超过了阈值,则会采取策略对其长度进行消解。

如果table还不算大,那么直接对table扩容,链表自然会被分裂到两地。

策略二,如果table已经很大了,扩容table已经不可取,那么就采用红黑树结构转化链表。

红黑树的创建不再详述。需要知道,这里采用的是二叉搜索树,进行比较的是每个hash值,不是key的直接比较。红黑树的根就是table中第一个节点。原始链表会先变成双向链表,以保存前后关系,然后再变成树结构。虽然由链表转化成了树结构,但是每个节点仍然保存了链表的前后关系,所以可以迅速的从树结构退化为链表结构。从TreeNode的属性中可以看出其既有树结构的left、right、parent关系,又有prev、next(继承)的双向链表关系。

        TreeNode parent;  // red-black tree links
        TreeNode left;
        TreeNode right;
        TreeNode prev;    // needed to unlink next upon deletion
HashMap的KeySet、Values和EntrySet

HashMap用KeySet来提供一个key的集合视角,KeySet并没有再申请一个空间来存储这些key,而是将所有的方法建立于HashMap的方法之上。KeySet提供了删除key的操作,该操作会映射到其HashMap之上,最后就是在HashMap中删除了该元素。KeySet没有提供添加元素的方法,因为HashMap需要的是key和value,而KeySet只能添加key,方法不能映射到HashMap上。

同样,HashMap提供了value的集合视角Values,EntrySet提供了Node集合的视角,原理类似。

HashMap的Spliterator

在HashMap中,分为key、value和node三种Spliterator,实现原理都是类似的。HashMap的Spliterator的划分不是针对元素的直接划分,而是对table这个数组的划分,这样更为简单。划分的策略也很简单,采用的二分法。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67894.html

相关文章

  • Java容器研究9:其它Map

    摘要:和的区别和的区别是,在操作的方法上加入关键字,使得线程安全。使用进行比较,或者传入的比较器。基于,它自己的任务主要是维护保持顺序的双向链表。和的区别提供了一个高效的线程安全的访问和更新的方式。在中的过程和类似。 HashTable和HashMap的区别 HashTable和HashMap的区别是,HashTable在操作table的方法上加入synchronized关键字,使得线程安全...

    zilu 评论0 收藏0
  • Java容器研究7:Set

    摘要:继承自,继承接口,接口继承自。的也是使用的的的。中和方法理论上是常数时间的复杂度,前提是元素在中分散比较均匀。并且这些元素在结构中,根据进行了排序,所以用遍历时可以按照递增递减顺序。遍历访问元素的操作比更为高效,时间由元素数量决定。 java.util.Set HashSet继承自AbstractSet,继承接口Set,Set接口继承自Collection。 null是否可以放在Set...

    RdouTyping 评论0 收藏0
  • Java容器研究1:Collection

    摘要:集合类关系是和的父接口。相等必须是对称的,约定只能和其它相等,亦然。和接口在中引入,这个单词是和的合成,用来分割集合以给并行处理提供方便。这些并不立即执行,而是等到最后一个函数,统一执行。 集合类关系: Collection ├List │├LinkedList │├ArrayList │└Vector │ └Stack └Set Map ...

    AprilJ 评论0 收藏0
  • 记一次惨痛的面试经历

    摘要:把内存分成两种,一种叫做栈内存,一种叫做堆内存在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。堆内存用于存放由创建的对象和数组。 一次惨痛的阿里技术面 就在昨天,有幸接到了阿里的面试通知,本来我以为自己的简历应该不会的到面试的机会了,然而机会却这么来了,我却没有做好准备,被面试官大大一通血虐。因此,我想写点东西纪念一下这次的经历,也当一次教训了。其实面试官大大...

    CoorChice 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<