HashMap
基于哈希表(散列表)的Map接口的实现,允许使用null键和null值,HashMap是非线程安全的,数据元素存取迭代是无序,顺便提一下HashTable,HashTable是线程安全的,除了线程安全和null键值的区别,HashMap和HashTable大致相同。
上图是哈希表的结构图,这种表结构查找效率高,如果我们把0-15的顺序线表的每个地址看成一个"桶",而下标是"桶"的编号,我们通过计算key的hash值“与”上线性表的长度得到得到0-15的值,然后将value放入对应的"桶"内,这样我们查找的时候不用遍历所有的元素,直接去对应的桶里面查找就可以了。
下面我们一起看HashMap的源码,我是用Android studio查看的api25的源码,不同api版本源码会有不同,首先来看看成员变量。
//最少容量
static final int DEFAULT_INITIAL_CAPACITY = 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//扩容因子(当容量到75%的时候就要开始扩容了)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空表
static final HashMapEntry<?,?>[] EMPTY_TABLE = {};
//键值对的数组
transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;
//非空元素的长度
transient int size;
//容量
int threshold;
//扩容因子相关
final float loadFactor = DEFAULT_LOAD_FACTOR
//操作计数器
transient int modCount;
我们再来看看put方法:
public V put(K key, V value) {
//存储key为null的值,如果之前有值就覆盖
if (key == null)
return putForNullKey(value);
//获取key的hash值
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key);
//通过hash&(length-1)得到元素长度内的值,也就是"桶"的下标,(&运算不熟悉的自行百度)
int i = indexFor(hash, table.length);
//通过桶的下标,遍历"桶"里面的元素(需要看一下HashMapEntry的实现),如果hash值相同,key相等,那就覆盖value,返回旧的value
for (HashMapEntry<K,V> 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;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//操作计数
modCount++;
//如果没有相同的key,就添加新元素
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果元素个数大于等于扩容因子容量(目前容量的75%),那就扩容原来的一倍
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? sun.misc.Hashing.singleWordWangJenkinsHash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//添加数据元素(内容简单,代码就不贴了)
createEntry(hash, key, value, bucketIndex);
}
void resize(int newCapacity) {
HashMapEntry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果达到最大了就不扩容了
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
HashMapEntry[] newTable = new HashMapEntry[newCapacity];
//通过遍历,把旧数组中的元素添加到新数组中
transfer(newTable);
table = newTable;
//计算扩容因子容量,loadFactor=0.75f
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(HashMapEntry[] newTable) {
int newCapacity = newTable.length;
//双层循环遍历把元素添加到新数组里面,因为indexFor()方法中的leng变了,
//"桶"的下标就有可能变,所以需要遍历全部元素,通过key的hash值重新计算桶的下标
for (HashMapEntry<K,V> e : table) {
while(null != e) {
HashMapEntry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
上面是put流程的部分源码,还需要自己对照源码看一看。如果能把put看明白,get就很简单了。下面我们看一下remove方法源码:
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.getValue());
}
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
//通过key的hash值获取"桶"的下标
int hash = (key == null) ? 0 : sun.misc.Hashing.singleWordWangJenkinsHash(key);
int i = indexFor(hash, table.length);
HashMapEntry<K,V> prev = table[i];
HashMapEntry<K,V> e = prev;
//遍历这个"桶"内的元素
while (e != null) {
HashMapEntry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
//只有第一个元素就是要删除元素的时候prev才会等于e
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
一些逻辑还需要自己认真屡一下。HashMap核心内容也就这些。下面我们一起看看LinkedHashMap。
LinkedHashMap
LinkedHashMap是HashMap的子类,具有HashMap所有特性,只是额外维护一个双向循环链表来保持迭代顺序,当然,是牺牲了一定的性能。LinkedHashMap支持LRU算法,LruCache就是基于LinkedHashMap来实现的。
下面一起看看源码,看一下仅有的两个成员变量。
//头结点
private transient LinkedHashMapEntry<K,V> header;
//如果是true通过访问排序,如果是false通过插入排序,默认为false
private final boolean accessOrder;
如果你去看一下Lrucache的构造方法,你会发现它传入的就是true。LinkedHashMap里面并没有put的方法,只是重写了addEntry()和createEntry()方法,HashMap的put方法最后会调用addEntry()和createEntry()方法。
void addEntry(int hash, K key, V value, int bucketIndex) {
//双向循环链表头节点(header后的节点)如果是按插入排序就是最先插入的元素,
//如果是按访问排序就是访问最少的元素,把访问最少或最老的元素赋值给eldest,
LinkedHashMapEntry<K,V> eldest = header.after;
if (eldest != header) {
boolean removeEldest;
size++;
try {
//removeEldestEntry()方法交给子类实现,通过判断返回值来删除eldest元素
//默认返回false,所以不删除
removeEldest = removeEldestEntry(eldest);
} finally {
size--;
}
//删除最少或最老的元素
if (removeEldest) {
removeEntryForKey(eldest.key);
}
}
super.addEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
//取出数组中存的节点
HashMapEntry<K,V> old = table[bucketIndex];
//创建新节点
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
//将新节点放在数组中
table[bucketIndex] = e;
//将新元素节点添加到双向循环链表,为什么传header呢,因为要把元素插入在header前面
//header前面是尾节点,header后面是头节点,新添加的元素需要添加到尾节点
e.addBefore(header);
size++;
}
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
//将新元素添加到header前面的尾节点,也就是header和之前的尾节点之间插入新元素节点
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
上面就是LinkedHashMap添加新元素的核心代码。配合结构图能更好的理解。每次新添加的元素都添加到header的前面,也就是尾节点,而header的后面的节点就是头结点,所以在Lru算法中,无论是访问排序还是插入排序,需要删除元素时,都是删除头结点。
public V get(Object key) {
//调用父类的获取元素方法
LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
if (e == null)
return null;
//记录访问
e.recordAccess(this);
return e.value;
}
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//如果是访问排序,就把访问的数据元素放在尾节点
if (lm.accessOrder) {
//记录操作
lm.modCount++;
//删除自己
remove();
//把自己添加到header前面的尾节点
addBefore(lm.header);
}
}
上面试get()的核心代码,如果是插入排序,只需要遍历查找就可以了,如果是访问排序,就需要把访问的元素放在尾节点。
public Map.Entry<K, V> eldest() {
Entry<K, V> eldest = header.after;
return eldest != header ? eldest : null;
}
eldest()方法是返回头节点。以上就是LinkedHashMap比较核心的源码了。