Java集合-Map
一、简介
Map是以键值对来存储数据元素的。键值对之间存在映射关系,通过key可以查找value。需要注意的是key是不允许重复的,可以认为Map的key组成的集合是一个Set,上篇文章我们介绍Set时也发现Java中Set的实现大多数最后都是采用Map来存储数据。
二、Map子类
HashMap
底层使用哈希表实现,需要实现hashcode和equals方法。面试中涉及最多的还是和HashTable的区别。首先前者是线程不安全的但效率较高key或vale均可为空,后者是线程安全的但线程安全是通过synchronized关键字实现的,key/value不能为空。
需要掌握的内容:
- 底层实现
java1.8版本为数组+链表+红黑树实现,1.7为数组+链表。
//内部定义了一个table数组来存放键值对,
transient Node<K,V>[] table;
//内部类Node表示一个键值对
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
从上面可知HashMap使用内部类Node表示一个键值对,且定义了一个数组来存储键值对,存储过程是根据hash值来确定存储位置的,当发生hash冲突时会把后到的元素链接到上一个Node,当同一index的链表长度超过一定值后会把链表调整为红黑树。
-
存取元素过程
public V put(K key, V value) { //通过hash(key)计算key的hash值 return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //初始化table数组大小 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根据hash值查找在数组中的index,如果找到的index位置上没有元素那么直接放置在该位置 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //找到的index已存在元素即发生了hash碰撞 Node<K,V> e; K k; //如果当前index的元素与要存入元素hash一样且key相等则将p赋值给e if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //如果是树则插入树中 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; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; //插入后需要扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
HashMap存储过程使用put,put流程如下:
- 先通过hash函数定位桶位置,如果桶位置上元素为空则直接插入该位置
- 如果桶位置上元素不为空,判断key是否存在,如果key为空则直接覆盖
- key如果不为空那么判断当前元素是否是TreeNode类型,如果是那么红黑树直接插入
- 如果不是TreeNode类型那么遍历链表,如果key在链表中存在那么直接覆盖value
- 如果key在链表中不存在那么插入链表,插入后判断链表长度如果大于8那么调整链表为红黑树。
- 最后插入完要判断当前size是否大于临界值,如果大于要进行扩容
-
扩容过程
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) //扩容至原来两倍 newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
扩容是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
- resize首先会检查当前容量,如果等于0那么表示初始化
- 如果大于0那么会检查当前容量是否达到最大容量阈值,如果是那么新阈值设置为Integer.MAX_VALUE
- 如果未达到最大容量阈值,那么会扩容为当前容量的两倍。
- 扩容后需要对原数组中的元素进行重hash,Java 1.8版本重hash时不会重新计算hash只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
-
hash的实现
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
HashMap的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算
- 首先会获取key的hashCode值
- 然后hashCode值会跟hashCode值的高16为做异或运算
- 最后拿上两步获取的值对底层数组长度取模,取模操作使用的是h & (table.length -1)
通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
参考链接:
https://blog.csdn.net/u013132758/article/details/89181005
准备用HashMap存1w条数据,构造时传10000还会触发扩容吗
TreeMap
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。底层是二叉树(红黑树)实现。containsKey、get, put、 remove等操作的时间复杂度为log(n)
Hashtable
很多的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段机制(jdk1.7是分段锁 jdk1.8采用CAS+synchronized实现)。当不需要线程安全的场合可以用HashMap替换,需要线程安全且高并发的场合可以用ConcurrentHashMap替换
LinkedHashMap
LinkedHashMap是HashMap的一个子类,双向链表维护键值对次序,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
在内部定义了LinkedHashMapEntry,它继承自HashMap.Node,增加了before, after指针。
static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {
LinkedHashMapEntry<K,V> before, after;
LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap是 LruCache核心。
LinkedHashMap都有哪些小秘密?同时给LruCache 提个建议
IdentityHashMap
HashMap内对key比较时是使用hash和equals,IdentityHashMap则是使用= =实现的
public boolean containsKey(Object key) {
Object k = maskNull(key);
Object[] tab = table;
int len = tab.length;
int i = hash(k, len);
while (true) {
Object item = tab[i]
//k和数组中某个已存在key通过“= =”比较,判断是否存在
if (item == k)
return true;
if (item == null)
return false;
i = nextKeyIndex(i, len);
}
}
WeakHashMap
如果其中key所引用对象没有被其他对象以强引用所引用那么key就会被回收。如果key所对应的对象被回收那么其表示的键值对会被删除。
ConcurrentHashMap
因为hashtable同步效率较低所以引入ConcurrentHashMap用于处理并发情况的 HashMap。大概的实现机制是分段,每段有自己的锁,这样就有多把锁可以在一定程度提高同步效率。感兴趣的可以自行去了解具体实现。
https://www.jianshu.com/p/d0b37b927c48
ArrayMap
它的内部实现是基于两个数组。
一个int[]数组,用于保存每个item的hashCode.
一个Object[]数组,保存key/value键值对。容量是上一个数组的两倍。
它可以避免在将数据插入Map中时额外的空间消耗(对比HashMap)。
而且它扩容的更合适,扩容时只需要数组拷贝工作,不需要重建哈希表。
和HashMap相比,它不仅有扩容功能,在删除时,如果集合剩余元素少于一定阈值,还有收缩(shrunk)功能。减少空间占用。
SparseArray
SparseArray是Android提供的key为int类型的map,底层使用数组实现。其内部有两个数据int[] mKeys和Object[] mValues。
mKeys和mValues通过如下方式对应起来:
- 假设要向SparseArray存入key为 10,value为200的键值对,则先将10存到mKeys中,假设 10 在mKeys中对应的索引值是index ,则将value存入 mValues[index]中
- mKeys中的元素值按照递增的形式存放,每次存放新的键值对时都通过二分查找方法来对mKeys进行排序
SparseArray避免了基本数据类型的拆装箱操作,且在数据量不大的情况下查找效率高(内部使用二分查找),还有就是其移除数据时仅做数据标记并不实际删除,实际删除操作在垃圾回收时才进行。
三、小结
至此Java集合相关类基本分析完毕
这里贴一个在网上看到的简单规律总结:
ArrayXxx:底层数据结构是数组,查询快,增删慢
LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序
参考: