Map 类集合
Java Map类集合,与Collections类集合存在很大不同。它是与Collection 类平级的一个接口。
在集合框架中,通过部分视图方法这一根 微弱的线联系起来。
(在之后的分享中,我们会讨论到Collections 框架的内容)
Map类集合中的存储单位是K-V键值对,就是 使用一定的哈希算法形成一组比较均匀的哈希值作为Key,Value值挂在Key上。
Map类 的特点:
-
没有重复的Key,可以具有多个重复的Value
-
Value可以是List/Map/Set对象
-
KV是否允许为null,以实现类的约束为准
Map集合类 | Key | Value | Super | JDK | 说明 |
---|---|---|---|---|---|
Hashtable | 不允许为 null | 不允许为 null | Dictionary | 1.0 | (过时)线程安全类 |
ConcurrentHashMap | 不允许为 null | 不允许为 null | AbstractMap | 1.5 | 锁分段技术或CAS(JDK8 及以上) |
TreeMap | 不允许为 null | 允许为 null | AbstractMap | 1.2 | 线程不安全(有序) |
HashMap | 允许为 null | 允许为 null | AbstractMap | 1.2 | 线程不安全(resize 死链问题) |
从jdk1.0-1.5,这几个重点KV集合类,见证了Java语言成为工业级语言的成长历程。
知识点:
- Map 类 特有的三个方法是
keySet()
、values()
、entrySet()
,其中values()
方法返回的视图的集合实现类是Values extends AbstractCollection<V>
,没有实现add操作,实现了remove/clear等相关操作,调用add方法时会抛出异常。 - 在大多数情况下,直接使用ConcurrentHashMap替代HashMap没有任何问题,性能上面差别不大,且线程安全。
- 任何Map类集合中,都要尽量避免KV设置为null值。
- Hashtable - HashMap - ConcurrentHashMap 之间的关系 大致相当于 Vector - ArrayList - CopyOnWriteArrayList 之间的关系,当然HashMap 和 ConcurrentHashMap之间性能差距更小。
一、hashCode()
哈希算法 哈希值
在Object 类中,hashCode()方法是一个被native修饰的类,JavaDoc中描述的是返回该对象的哈希值。
那么哈希值这个返回值是有什么作用呢?
主要是保证基于散列的集合,如HashSet、HashMap以及HashTable等,在插入元素时保证元素不可重复,同时为了提高元素的插入删除便利效率而设计;主要是为了查找的便捷性而存在。
拿Set进行举例,
众所周知,Set集合是不能重复,如果每次添加数据都拿新元素去和集合内部元素进行逐一地equal()比较,那么插入十万条数据的效率可以说是非常低的。
所以在添加数据的时候就出现了哈希表的应用,哈希算法也称之为散列算法,当添加一个值的时候,先去计算出它的哈希值,根据算出的哈希值将数据插入指定位置。这样的话就避免了一直去使用equal()比较的效率问题。
具体表现在:
- 如果指定位置为空,则直接添加
- 如果指定位置不为空,调用equal() 判断两个元素是否相同,如果相同则不存储
上述第二种情况中,如果两个元素不相同,但是hashCode()相同,那就是发生了我们所谓的哈希碰撞。
哈希碰撞的概率取决于hashCode()计算方式和空间容量的大小。
这种情况下,会在相同的位置,创建一个链表,把key值相同的元素存放到链表中。
在HashMap中就是使用拉链法来解决hashCode冲突。
总结
hashCode是一个对象的标识,Java中对象的hashCode是一个int类型值。通过hashCode来指定数组的索引可以快速定位到要找的对象在数组中的位置,之后再遍历链表找到对应值,理想情况下时间复杂度为O(1),并且不同对象可以拥有相同的hashCode。
HashMap 底层实现
带着问题
- HashMap 的长度为什么默认初始长度是16,并且每次resize()的时候,长度必须是2的幂次方?
- 你熟悉HashMap的扩容机制吗?
- 你熟悉HashMap的死链问题吗?
- Java 7 和 Java 8 HashMap有哪些差别?
- 为什么Java 8之后,HashMap、ConcurrentHashMap要引入红黑树?
0. 简介
- HashMap 基于哈希表的Map接口实现的,是以Key-Value存储形式存在;
- 非线程安全;
- key value都可以为null;
- HashMap中的映射不是有序的;
- 在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构;
- 当一个哈希桶存储的链表长度大于8 会将链表转换成红黑树,小于6时则从红黑树转换成链表;
- 1.8之前和1.8及以后的源码,差别较大
1. 存储结构
在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成,新增了红黑树作为底层数据结构。
通过哈希来确认到数组的位置,如果发生哈希碰撞就以链表的形式存储 ,但是这样如果链表过长来的话,HashMap会把这个链表转换成红黑树来存储,阈值为8。
下面是HashMap的结构图:
2. 重要属性
2.1 table
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
在JDK1.8中我们了解到HashMap是由数组加链表加红黑树来组成的结构其中table就是HashMap中的数组。
2.2 size
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
HashMap中 键值对存储数量。
2.3 loadFactor
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
负载因子。负载因子是权衡资源利用率与分配空间的系数。当元素总量 > 数组长度 * 负载因子
时会进行扩容操作。
2.4 threshold
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
扩容阈值。threshold = 数组长度 * 负载因子
。超过后执行扩容操作。
2.5 TREEIFY_THRESHOLD/UNTREEIFY_THRESHOLD
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
树形化阈值。当一个哈希桶存储的链表长度大于8 会将链表转换成红黑树,小于6时则从红黑树转换成链表。
3. 增加元素
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
3.1 hash()
可以看到实际执行添加元素的是putVal()操作,在执行putVal()之前,先是对key执行了hash()方法,让我们看下里面做了什么
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
key==null
说明,HashMap中是支持key为null的情况的。
同样的方法在Hashstable中是直接用key来获取hashCode,没有key==null
的判断,所以Hashstable是不支持key为null的。
再回来说这个hash()方法。这个方法用专业术语来称呼就叫做扰动函数。
使用hash()也就是扰动函数,是为了防止一些实现比较差的hashCode()方法。换句话来说,就是为了减少哈希碰撞。
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。我们再看下JDK1.7中是怎么做的。
// code in JDK1.7
static int hash(int h)