中北软院创新实验室源码解析

Map集合了解一下

2018-05-09  本文已影响54人  HikariCP

前言

首先声明本文使用的是jdk1.8

Map映射

Map即双列集合。但我们一般称它为映射,即键值对的形式。

Map映射的特点

Map接口结构

图中叙述的很清楚。

Map接口常见Api解析

由于是顶级Map接口,官方只由注释定义了其实现规则,所以我们做的也只是解读注释而已。

put(K key, V value)

将指定的值与此映射中的指定键关联。如果此映射以前包含一个该键的映射关系,则用指定值替换旧值(当且仅当 m.containsKey(k) 返回 true 时,才能说映射 m 包含键 k 的映射关系)。

putAll(Map<? extends K,? extends V> m)

从指定映射中将所有映射关系复制到此映射中(可选操作)。对于指定映射中的每个键 k 到值 v 的映射关系,此调用等效于对此映射调用一次 put(k, v)。

remove(Object key)

如果存在一个键的映射关系,则将其从此映射中移除。
返回此映射中以前关联该键的值,如果此映射不包含该键的映射关系,则返回 null。

需要注意的是:

如果此映射允许 null 值,则返回 null 值并不一定 表示该映射不包含该键的映射关系;也可能该映射将该键显示地映射到 null。

clear()

从此映射中移除所有映射关系(可选操作)。此调用返回后,该映射将为空。

isEmpty()

如果此映射未包含键-值映射关系,则返回 true。

containsKey(Object key)

如果此映射包含指定键的映射关系,则返回 true。

containsValue(Object value)

如果此映射将一个或多个键映射到指定值,则返回 true。

get(Object key)

返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。

需要注意的是:

如果此映射允许 null 值,则返回 null 值并不一定 表示该映射不包含该键的映射关系;也可能该映射将该键显示地映射到 null。使用 containsKey 操作可区分这两种情况。

entrySet()

返回此映射中包含的映射关系的Set集合。即Entry集合

需要注意的是:

如果对该Set进行迭代的同时修改了映射。则迭代结果是不确定的。迭代结果可能由于并发操作的影响超出我们的预期。

但如果通过迭代器自己的remove操作,或者通过对迭代器返回的映射项Entry执行setValue操作除外。因为这样的修改结果在我们的预知范围之内。

keySet()

返回此映射中包含的键的Set集合。

需要注意的是:

如果对该set进行迭代的同时修改了映射,则迭代结果是不确定的。因为是线程不同步的,所以并发操作其他线程的修改结果我们不会预知,可能会发生异常,可能正常进行但是结果不是我们预期的结果

但通过迭代器自己的remove操作除外。还是那个原因,通过Iterator的迭代方式进行remove的时候是可以保证在remove的时候expectedModCount = modCount的。

所以如果在使用Iterator迭代的时候如果其他线程并发的修改了映射类的话。Iteratore会返回一个并发修改的异常。相比于其他迭代方式对Set集合遇到并发修改的行为的返回结果是确定的。

values()

返回此映射中包含的值的Collection集合。

size()

返回此映射中的键-值映射关系数。如果该映射包含的元素大于Integer.MAX_VALUE,则返回Integer.MAX_VALUE

Map接口常见实现类

红框所标都是Map接口关键的实现类。

其中需要针对HashMap和TreeMap两种需要提前了解一下他们底层的数据结构实现。分别是哈希表(散列表)和红黑树

散列表

针对于Map接口的HashMap实现类以及Set接口的HashSet我们来学习一下散列表。

什么是散列表

散列表其实也很好理解,就是一个链表数组。数组的每一个元素都是一个链表。

散列表最重要的就是它当中的元素之如何存储的。在我们之前所了解的无论是链表还是数组,如果想查找某一元素都是量元素间相互比较,通过比较的方式来迭代遍历元素的,知道找到那个元素。这样的方式无疑当数据量上去之后对我们时间上的开销非常的巨大。所以散列表的存储方式就诞生了

散列表的工作原理

哈希表的存储过程如下:

散列冲突

我们刚才说了,底层容器是一个链表数组。为什么是链表呢,那肯定是数组的一个位置可以存不止一个元素。必然,我们还没有了解过散列表是如何为每个要存入的元素计算其散列值的呢,会不会出现两个元素散列值一样的情况。但既然我们知道散列表底层的数据结构了,肯定就会知道一定是会出现这种情况的。我们一般称这种情况为哈希冲突(hash碰撞),但一般情况下的哈希函数设计都十分合理,这种现象很少出现。即便出现了,也有对应的措施。

此时的处理方式:

哈希冲突解决办法:

负载因子

负载因子(load factor),它用来衡量哈希表的 空/满 程度,一定程度上也可以体现查询的效率

负载因子 = 总键值对数 / 箱子个数(数组元素)

负载因子越大,意味着哈希表越满,越容易导致冲突,性能也就越低。因此,一般来说,当负载因子大于某个常数(可能是 1,一般为 0.75 等)时,哈希表将自动扩容。

哈希表在自动扩容时,一般会创建两倍于原来个数的箱子,因此即使 key 的哈希值不变,对箱子个数取余的结果也会发生改变,因此所有键值对的存放位置都有可能发生改变,这个过程也称为重哈希(rehash)。

哈希表的扩容并不总是能够有效解决负载因子过大的问题。假设所有 key 的哈希值都一样,那么即使扩容以后他们的位置也不会变化。虽然负载因子会降低,但实际存储在每个箱子中的链表长度并不发生改变,因此也就不能提高哈希表的查询性能。

哈希表的两个问题

我们可以从刚才的分析中得出哈希表的如下两个问题:

这两个问题在Java8中Java官方已经为我们解决了。

红黑树

我们知道JDK1.8之后HashMap底层的哈希表的箱子中元素大于或等于8时会从链表转化成红黑树,并且Map接口的TreeMap实现类和Set接口的TreeSet实现类底层也是由红黑树实现的。我们由此先来学习一下红黑树。

什么是红黑树

红黑树也是平衡二叉树的一种。

他为什么会出生呢,究其原因肯定是在某些方面该数据结构具有更好地效率。

我们知道,由于二叉查找树在特殊情况下会变成链表大大影响查找性能的原因我们有了AVL树,而紧接着由于AVL树高度平衡的性质,频繁的插入和删除,会引起频繁的reblance,导致效率下降的原因我们就有了红黑树这个数据结构。

红黑树不是高度平衡的,算是一种折中,它是局部平衡的。插入最多两次旋转,删除最多三次旋转。

二叉查找树偏向问题:

在二叉查找树上,我们插入节点的过程是这样的:小于节点值往右继续与左子节点比,大于则继续与右子节点比,直到某节点左或右子节点为空,把值插入进去。这样无法避免偏向问题

所以我们就有了平衡树这么一个概念。这样的树结构能够使我们任何的元素在插入与删除的时候依旧能够保持任意节点左右子树高度不相差1。

红黑树原型2-3树

红黑树明显很好地解决了这个问题。但是它的原型其实是2-3树。

image

如图即是一个典型的2-3树。

在2-3树中,共有两种节点。第一种是“2-节点”:

image

该节点性质和二叉查找树种节点性质一致。左孩子小于该节点,右孩子大于该节点

第二种是“3-节点”:

image

该节点和2-节点不同的是多了一条分支。最左边的儿子表示比a小的子树,中间的儿子表示大于a但小于b的子树,右边的儿子表示比b大的子树。

虽然这是2-3树,我们能直观的看到数中是不存在“4-节点”的。但是为了保持树的平衡性,我们将会利用4-节点来在插入和删除过程中保持树的完美平衡。下面是一个4-节点:

image

该节点的分支思路和3-节点一致。

那么2-3树是如何避免二叉查找树中偏向的问题的呢?

很简单,我们来参考一个节点插入步骤图。

image

总的概括起来就是当有新节点插入时

当然我们一个树的平衡,不光是要插入时保持平衡同时在做删除操作操作的时候我们也要保持它的持续平衡。2-3树的删除操作平衡调整比较复杂。我也没有完全理解,有兴趣可以参考:

2-3树与红黑树

2-3到红黑树

我们在了解了2-3树对于解决节点偏移的问题的处理思路之后可以很明显的感觉到,其中需要大量的节点变换。这些变换在实际代码中是很复杂的。所以现在几乎没有2-3树的具体实现。

红黑树也是一种平衡二叉树,但相比于2-3树,红黑树只有一种2-节点。但既然我们要想做和2-3树一样的工作却不给它3-节点,无疑我们把3节点都用合理的方式拆分了。

如何表示3-节点呢?我们尝试一种特殊的边:默认情况下节点的颜色均为黑色。我们将某个节点染为红色,表示它和父亲的的链接是红色的,就像下图:

image

当我们将红链接画平时

image

可以看到,这不就是我们刚才2-3树种的3-节点吗。而事实上,我们完全可以用这样的方式来表示2-3树中的3-节点。

如下即是一颗典型的红黑树:


image

将该树种所有红链接画平,将得到一棵完美平衡的“2-3树”。

红黑树是如何保证平衡

相比于2-3树不断变换节点的缺陷来说。由于红黑树的节点都是2-节点所以其处理方式更简洁。

这两个操作相对于2-3树种的节点变换要简单许多。

具体可以参考:2-3树与红黑树

红黑树如何定义

总结

上一篇下一篇

猜你喜欢

热点阅读