目录
Map
Map的实现类结构
Map:双列数据,存储键值对的数据
-
HashMap:作为Map的主要实现类,线程不安全,效率高;可存储null的key和value
-
LinkedHashMap:保证遍历map元素时可按照添加顺序实现遍历。
原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素
-
-
TreeMap:保证按照添加的key-value对进行排序,实现排序遍历,根据key进行排序。底层使用红黑树。
-
Hashtable:作为Map的古老实现类,线程安全,效率低;不可存储null的key和value
- Properties:常用来处理配置文件。key和value都是String类型
HashMap的底层:
数组+链表(jdk7之前)
数组+链表+红黑树(jdk8之后)
面试题
- HashMap的底层实现原理
- HashMap和Hashtable的异同
- ConcurrentHashMap与Hashtable的异同
Map结构理解
Map中的key:无序、不可重复,使用set存储所有的key—>key所在的类要重写equals()和hashCode()
Map中的value:无序、可重复,使用collection存储所有value—>value所在类要重写equals()
一个键值对:key-value构成了一个Entry对象
Map中的Entry:无序的、不可重复的,使用Set存储所有的Entry
HashMap底层实现原理
以jdk7为例说明HashMap的添加过程:
HashMap map = new HashMap()
实例化之后,底层创建了长度是16的一维数组Entry[] table
map.put(key1,value1)
首先调用key1所在类的hashCode()计算key1哈希值,根据一定算法计算出在Entry数组里的位置:
- 若该位置为空,添加成功
- 若该位置有一个或多个数据(以链表形式存在),比较key1和已经存在的一个或多个数据的哈希值:
- 若都不相等,添加成功
- 若key1的哈希值与某一个数据(key2,value2)的哈希值相同,则继续用equals()比较
- 若不相等,则添加成功;
- 若相等,则使用value1替换value2的值(修改操作)
在不断添加的过程中,涉及到扩容,默认方式:扩容为原来2倍,并将原有数据复制过去
以jdk8相对于jdk7的不同:
-
new HashMap():底层没有创建一个长度为16的数组
-
jdk8底层的数组是:Node[],而非Entry[]
-
首次调用put()方法时,底层调用长度为16的数组
-
jdk8底层结构:数组+链表+红黑树。
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8且当前数组长度 > 64,此时此索引位置上的所有数据改为红黑树存储。
HashMap源码分析
JDK7源码
成员变量
//内部数组的默认初始容量,作为hashmap的初始容量,是2的4次方,2的n次方的作用是减少hash冲突
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
//默认的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子,当容器使用率达到这个75%的时候就扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
*当数组表还没扩容的时候,一个共享的空表对象
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
//内部数组表,用来装entry,大小只能是2的n次方。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//存储的键值对的个数
transient int size;
/**
* 扩容的临界点,如果当前容量达到该值,则需要扩容了。
* 如果当前数组容量为0时(空数组),则该值作为初始化内部数组的初始容量
*/
int threshold;
//由构造函数传入的指定负载因子
final float loadFactor;
//threshold的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//含有所有entry节点的一个set集合
private transient Set<Map.Entry<K,V>> entrySet = null;
put()
方法
public V put(K key, V value) {
//先判断哈希表是否为空,第一次put的话肯定是为空的,
if (table == EMPTY_TABLE) {
// roundUpToPowerOf2方法的作用是将构造器传入的容量初始化大小
//转成最接近2的n字方值,为什么要2的n字方,下面会提到
int capacity = roundUpToPowerOf2(threshold);
//临界值是加载因子*容量大小
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//创建一个Entry数组
table = new Entry[capacity];
//initHashSeedAsNeeded方法的作用:找到与该实例的一个哈希掩码值,使哈希碰撞几率更为小.里
//面会生成一个hashSeed,将会在生成哈希值里面可能会用到。
initHashSeedAsNeeded(capacity);
}
//如果key为null
if (key == null)
//这个方法下面讲解
return putForNullKey(value);
//计算key的哈希值
int hash = hash(key);
//计算该哈希值在哈希表的下标
int i = indexFor(hash, table.length);
//如果刚刚计算出来的下标在哈希表里面为空的话,将不会进入循环
//不为空将遍历table[i]的链表
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判断该链表上是否有相同的哈希值和相同的地址值,或者key相同
//若存在则覆盖旧值,返回旧值
//判断顺序->hash->equals()
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//这个方法下面讲解
addEntry(hash, key, value, i);
return null;
}
为什么容量大小只能是2的n次方
答案在indexFor()
方法中
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
元素在Entry数组中决定位置的方式是使用hash按位与length-1,如果length是15而不是16,则length-1 = 1110,则hash=1110和hash=1111按位与1110的结果都是1110,则会产生冲突。因此都是length都是2的倍数最为合适
对null的处理
HashMap与Hashtable的区别之一就是key是否能处理null
private V putForNullKey(V value) {
//查找哈希表中0索引的位置,是否不为空,如果不为空,则遍历0索引的链表
//查找key==null的键值,覆盖并返回旧值。
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//将null的键放入哈希表0索引的位置
addEntry(0, null, value, 0);
return null;
}
key为null的键值对一定放在table[0]的链表中
添加新结点
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果结点个数大于或等于临界值和该哈希表指定的索引位置不为null
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容会在重点讲解
resize(2 * table.length);
//这一步就是对null的处理,如果key为null,hash值为0,
//也就是会插入到哈希表的表头table[0]的位置
hash = (null != key) ? hash(key) : 0;
//因为扩容了,需要重新计算哈希表的位置
bucketIndex = indexFor(hash, table.length);
}
//创建Entry结点的操作
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//查找出指定索引的结点对象,目的:形成一个链表
Entry<K,V> e = table[bucketIndex];
//第四个哈希表指定索引结点的对象,这样就形成了一个单链链表了。
//为什么要放在链表头,因为好像作者说后面放进去的结点会更大几率使用到。欢迎纠错。
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
主要注意createEntry()
方法中,使用了table[bucketIndex]
作为Entry构造函数的第四个参数,并且将返回值传给table[bucketIndex]
,这句话实现了hashMap添加新节点的头插法:可以看到Entry构造函数中第四个参数n被作为新产生的Entry的下一个元素,因此原来的链表的头被接在了当前Entry的next上。
扩容
void resize(int newCapacity) {
//引用扩容前的Entry数组
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果扩容前的数组大小如果已经达到最大(2^30)了
//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建一个新的哈希表
Entry[] newTable = new Entry[newCapacity];
//将当前所有的哈希表数据复制到新的哈希表
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
//计算临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//将当前所有的哈希表数据复制到新的哈希表
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历旧的哈希表
for (Entry<K,V> e : table) {
while(null != e) {
//保存旧的哈希表对应的链表头的下一个结点
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//因为哈希表的长度变了,需要重新计算索引
int i = indexFor(e.hash, newCapacity);
//第一次循环的newTable[i]为空,赋值给当前结点的下一个元素,
//下面有图会讲解这句代码的含义
e.next = newTable[i];
//将结点赋值到新的哈希表
newTable[i] = e;
e = next;
}
}
}
hashMap的转移也使用了头插法,因此得到的顺序应该是与原来相反的。
JDK8源码
Entry改名为Node
put()
方法
public V put(K key, V value) {
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;
// 第一次执行put的时候分配数组内存
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果当前key在数组中对应的位置没有元素,插入成功
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 对应位置已经存在了链表或者红黑树
else {
Node<K,V> e; K k;
// 如果该位置第一个元素的key就和插入的key相同
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) {
// 走到null了,进行尾插
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度过长了,转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了某个位置的key与插入key相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 进行key对应的value的修改
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;
}