Map:Map是一个接口,它定义了一些规则,即get和put操作。Map用于保存具有映射关系的数据,因此Map集合中存的是键值对,并且key不能重复
HashMap:HashMap是Map接口的一个实现类。HashMap提供所有可选的映射操作,并且允许存null键和null值,它不保证映射的顺序,特别是不保证该顺序永远不发生改变。HashMap的迭代所需的时间和HashMap实例的“容量”(桶的数量)以及大小(里面键值对的数量)成比例。
影响HashMap性能的两个参数:初始容量和加载因子。容量就是哈希表中桶的数量,初始容量只是哈希表在创建时的容量,加载因子是哈希表在其容量自动增加之前达到多满的一种程度,比如默认的加载因子是0.75,初始容量是100,那么当哈希表中的条目数量达到0.75 * 100 = 75时,则要对哈希表进行rehash操作(即重建内部结构),从而将哈希表的桶数翻倍。
HashMap是不同步的,所以在多线程访问同一个哈希表的时候,需要在外部进行同步,比如
Map map = Collections.synchronizedMap(new HashMap(...));
HashMap的迭代:由所有此类的"Collection视图方法"所返回的迭代器都是快速失败的:在迭代器创建之后,如果要对HashMap结构进行修改,建议通过迭代器本身的remove()和add()
HashMap的遍历:可以通过获取到HashMap的keySet,然后通过keySet里面的key去取value
HashMap hashMap = new HashMap
HashMap设计思路
HashMap假定哈希函数将元素正确分布在各桶之间,可为get和put提供稳定的性能。迭代视图所需的时间与HashMap实例的“容量”及其大小成比例
HashMap重写的方法:因为HashMap是基于HashCode的,在Object类中有一个HashCode方法,这个方法返回的HashCode对应于当前的地址,也就是说不同的对象,即使它们的内容完全一样所得到的哈希值也会不一样,所以就跟复写equals方法一样,要重新定义hashCode的实现
重写HashCode的原则:1.不唯一原则:不必对每个不同的对象都产生一个唯一的hashcode,只要设计的hashCode方法能get到put进去的内容就可以了;
2.分散原则:hashCode算法生成的值要分散一些,不要很多的hashcode都集中在一个范围,这样有利于提高HashMap的性能
对HashMap的分析
1.HashMap是实现了Map Cloneable Serializable并继承了AbstractMap的类,最重要的是里面有一个实现了Map.Entry的静态内部类HashMapEntry,里面包含了key value next hash四个属性
static class HashMapEntry implements Entry {
final K key;
V value;
final int hash;
HashMapEntry next;
HashMapEntry(K key, V value, int hash, HashMapEntry next) {
this.key = key;
this.value = value;
this.hash = hash;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
@Override public final boolean equals(Object o) {
if (!(o instanceof Entry)) {
return false;
}
Entry, ?> e = (Entry, ?>) o;
return Objects.equal(e.getKey(), key)
&& Objects.equal(e.getValue(), value);
}
@Override public final int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
@Override public final String toString() {
return key + "=" + value;
}
}
put源码如下
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//生成哈希值
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//通过该方法查找hash值对象的元素索引
int i = indexFor(hash, table.length);
for (HashMapEntry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;//保存oldValue用于返回
e.value = value;//赋值新的value
e.recordAccess(this);
return oldValue;
}
}
modCount++;//结构更改的次数
addEntry(hash, key, value, i);//添加新元素
return null;//没有相同的键值返回null
}
private V putForNullKey(V value) {
for (HashMapEntry 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++;
addEntry(0, null, value, 0);
return null;
}
从上面可以看出HashMap其实内部还是用到的table,如果key为null的话,会调用putForNullKey方法存入null键条目,这也就是为什么HashMap允许存null键的原因。因为哈希算法可能导致不同的键值有相同的hash值并有相同的table索引,假设shadow 和 walker这两个key的hash值一样,那么通过indexFor方法得到的table里面的索引肯定一样,这样再new的时候这个Entry的next就指向原来的这个table[i],再有下一个也如此,因此会在table[i]处形成一个链表(PS:因为HashMapEntry里面只有一个next指针指向下一个,所以每次相同hash值的键值对会存放在第1个,然后它的next指向原来的第一个元素)
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
从上面可以看出put的时候如果达到threshold那么直接会将HashMap的桶量翻倍,然后获取索引的时候用到的是hash & (tab.lenth -1);HashMap底层数组长度总是2的n次方,在构造函数中存在capacity=1<<4这样就能保证HashMap的底层数组长度总是2的n次方。当length为2的n次方时,hash & (tab.length -1)就相当于对length取模,而且速度比直接取模快得多,而且这样能够保证table数据均匀分布和充分利用空间
PS:x mod 2^n = x & (2^n - 1) 分析:取模运算,因为2^n二进制是1000...0这种形式,与2^n取模就是取的后面(n-1)位,而与运算遇0则0遇1保持,2^(n-1)全是1,所以 x mod 2^n = x & (2^n - 1)
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
如果传入的初始容量不是2的幂,内部仍然会自行调整为2的幂
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
假设length为16和15,hash为5,6,7
length为15的时候比如6和7结果一样,10和11结果一样,出现很多次hash碰撞,这就导致table多个地方没有存放数据,而且多个地方存的是一个链表,这样就导致查询很慢。所以说当length = 2^n时,hash碰撞的概率较小,查询较快
LinkedHashMap: HashMap是无序的,也就是说迭代HashMap所得到的元素的顺序并不是它们最初放进去的顺序。LinkedHashMap内部维护的是一个双向链表,它可以指定迭代顺序,这个迭代顺序可以是插入顺序也可以是访问顺序,很适合做LRU Cache
LinkedHashMap中,所有put进来的Entry都保存在如下第一个图表示的哈希表中,但由于它又定义了一个以head为头结点的双向链表如图二所示,因此对于每次put进来的Entry,除了将其保存到哈希表中的对应的位置上外,还会将其插入到双向链表的尾部
HashMap和双向链表的配合使用造就了LinkedHashMap,next用于维护HashMap各个桶中的Entry链,before、after用于维护LinkedHashMap中的双向链表,虽然作用的对象都是Entry,但是各自分离,是不同的
使用LinkedHashMap做LRU Cache的使用需要将accessOrder设置为true,设置为true就是制定迭代的顺序为访问顺序,accessOrder为true的时候每次put或者get一个Entry的时候都会将它移到双向链表的尾部,这样最近访问的自然元素自然会在双向链表的尾部。如果要在Cache满的时候移除最老的元素,则可以重写removeEldestEntry方法,该方法默认返回false,只需要重写返回true即可在双向链表满的情况下往里面插入元素时移除最老的元素
void recordAccess(HashMap m) {
LinkedHashMap lm = (LinkedHashMap)m;
if (lm.accessOrder) {//如果要实现LRUCache 则需要将accessOrder设置为true
lm.modCount++;
remove();//移除该元素
addBefore(lm.header);//将该元素插入到双线链表的尾部
}
}
public V get(Object key) {
LinkedHashMapEntry e = (LinkedHashMapEntry)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
//重写了HashMap的createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry old = table[bucketIndex];
LinkedHashMapEntry e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);//每次插入的时候都会将新的Entry放到双链表的尾部,这样就可以按照插入顺序迭代
size++;
}
从如上图所示的get方法还有createEntry方法可以看出,在插入顺序层面,新的Entry插入到双向链表的尾部可以实现按照插入的先后顺序来迭代Entry,而在访问顺序的层面,新put进去的Entry又是最近访问的Entry,所以每次都应该移到双向链表的尾部
/**
* Evicts eldest entry if instructed, creates a new entry and links it in
* as head of linked list. This method should call constructorNewEntry
* (instead of duplicating code) if the performance of your VM permits.
*
* It may seem strange that this method is tasked with adding the entry
* to the hash table (which is properly the province of our superclass).
* The alternative of passing the "next" link in to this method and
* returning the newly created element does not work! If we remove an
* (eldest) entry that happens to be the first entry in the same bucket
* as the newly created entry, the "next" link would become invalid, and
* the resulting hash table corrupt.
*/
@Override void addNewEntry(K key, V value, int hash, int index) {
LinkedEntry header = this.header;
// Remove eldest entry if instructed to do so.
LinkedEntry eldest = header.nxt;
if (eldest != header && removeEldestEntry(eldest)) {
remove(eldest.key);
}
// Create new entry, link it on to list, and put it into table
LinkedEntry oldTail = header.prv;//获取之前的队尾元素
// 创建一个新的元素,并让元素nxt指向header,prv指向之前的队尾元素
LinkedEntry newTail = new LinkedEntry(
key, value, hash, table[index], header, oldTail);
//1.让header的prv指向新的元素
//2.让以前的队尾nxt指向新的元素
//3.将新的元素放进table
table[index] = oldTail.nxt = header.prv = newTail;
}
参考资料:https://www.cnblogs.com/chenssy/p/3521565.html
https://baike.baidu.com/item/Hashmap/1167707?fr=aladdin120