Map是以键值对来存储数据元素的。键值对之间存在映射关系,通过key可以查找value。需要注意的是key是不允许重复的,上篇文章我们介绍Set时发现Java中Set的实现大多数最后都是采用Map来存储数据。
map存储的是键值对,键值对之间存在映射关系,map的key是唯一的value可以是不唯一的。
底层使用哈希表实现,需要实现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流程如下:
扩容过程
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里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组。
hash的实现
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap的Hash算法本质上就是三步:取key的hashCode值、高位运算、取模运算
通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。
参考链接:
https://blog.csdn.net/u013132758/article/details/89181005
准备用HashMap存1w条数据,构造时传10000还会触发扩容吗
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。底层是二叉树(红黑树)实现。containsKey、get, put、 remove等操作的时间复杂度为log(n)
很多的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。当不需要线程安全的场合可以用HashMap替换,需要线程安全且高并发的场合可以用ConcurrentHashMap替换
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 提个建议
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);
}
}
如果其中key所引用对象没有被其他对象以强引用所引用那么key就会被回收。如果key所对应的对象被回收那么其表示的键值对会被删除。
因为hashtable同步效率较低所以引入ConcurrentHashMap用于处理并发情况的 HashMap。大概的实现机制是分段,每段有自己的锁,这样就有多把锁可以在一定程度提高同步效率。感兴趣的可以自行去了解具体实现。
https://www.jianshu.com/p/d0b37b927c48
它的内部实现是基于两个数组。
一个int[]数组,用于保存每个item的hashCode.
一个Object[]数组,保存key/value键值对。容量是上一个数组的两倍。
它可以避免在将数据插入Map中时额外的空间消耗(对比HashMap)。
而且它扩容的更合适,扩容时只需要数组拷贝工作,不需要重建哈希表。
和HashMap相比,它不仅有扩容功能,在删除时,如果集合剩余元素少于一定阈值,还有收缩(shrunk)功能。减少空间占用。
至此Java集合相关类基本分析完毕
这里贴一个在网上看到的简单规律总结:
ArrayXxx:底层数据结构是数组,查询快,增删慢
LinkedXxx:底层数据结构是链表,查询慢,增删快
HashXxx:底层数据结构是哈希表。依赖两个方法:hashCode()和equals()
TreeXxx:底层数据结构是二叉树。两种方式排序:自然排序和比较器排序
参考:
面试中的HashMap、ConcurrentHashMap和Hashtable
Android Studio说:使用HashMap不如使用SparseArray?
原文:https://www.cnblogs.com/Robin132929/p/13701528.html