hashMap
先说明hashmap是线程不安全的
参考此链接:http://www.importnew.com/20386.html
hashMap内部的实现比list复杂好多,内部是有数组加链表的形式存储的,而put的键值的hashCode值的低位计算值为其存储在数组的下标,而数组里指向的是一个链表,链表中存的是真正的数据,这里的hashCode低位计算值可能会相等,相等时就会遍历数组指向的这个链表的各个键值,判断键值是不是一样,一样就覆盖,不一样就要添加到此链表中,每次扩为原大小的2倍,这里为什么两倍这么多呢,因为在hashmap中使用了很多的位运算来提高效率,而二进制中升/进一位相当于十进制的乘2.
//put元素的具体实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//判断若数组为空或长度为0时会进行扩容,这里的扩容是初始化长度,在初始化HashMap时不会初始化数组
n = (tab = resize()).length;//保存扩容后的长度
if ((p = tab[i = (n - 1) & hash]) == null)//如果插入数据的hashCode下标对应的数据为空,会对其初始化新的Node,Node是链表中的节点
tab[i] = newNode(hash, key, value, null);//在初始化Node时,将其要插入的值存入其中,然后数组对应位置执行这个链表节点
else {//来到这里的话就说明要插入的数组的对应下标上已经有节点/链表存在了
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//判断指向的节点的键值与要插入的键值是否一样,一样就覆盖其值,这里是先放到了e中
e = p;
else if (p instanceof TreeNode)//这是jdk8引入的红黑树,这里的操作的目的是放入到红黑树中,而这里是获取树中存在的键值对象,如果树中没有这个对象则会创建并返回null,有的话就返回这个对象用于后面覆盖
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);//创建一个节点,内容为要插入的数据,并将最后一个节点的next指向新的节点
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//这里是jdk8的优化,在链表长度大于8是会将链表转换成红黑树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//存在一样的键值,退出遍历,此时的e就是这个存在的键值的引用
break;
p = e;//用p继续遍历
}
}
if (e != null) { // existing mapping for key//这意思是链表中有一样的键值,需要进行覆盖
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;//覆盖原值
afterNodeAccess(e);//hashMap中这个是空实现
return oldValue;//返回被覆盖的值,这里因为是覆盖所以直接返回不用后面的长度判断及增加
}
}
++modCount;
if (++size > threshold)//长度+1,如果加了之后大于当前数组的长度,就进行扩容
resize();//扩容
afterNodeInsertion(evict);//hashMap中这个是空实现
return null;
}
计算存储的下标即扩容
因为默认的长度为16,在2进制中为4位数,所以在一开始会对键值的hashCode进行位运算获取低4位作为数组的下标,在数组为空或者长度为0(前两个会进行初始化)或者在插入后的长度大于数组的长度会进行扩容,扩容会对hashCode的低4位扩成低5位,而在十进制中就是2倍的意思,在扩大后会将原数组添加到新数组中,在添加的过程中会更新下标,因为原下标是4位计算的,要将其改为5位的计算.
注:在jdk7即之前会重新计算hashCode,但在jdk8进行了优化,原来的元素不重新计算hashcode,而是判断其高一位的二进制是1还是0,是1的话就加上该位的值来得到新的下标
//扩容的具体实现
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获取原长度,若为空则默认为0
int oldThr = threshold;//hashCode计算阈值,相当于计算的hashCode上限
int newCap, newThr = 0;
if (oldCap > 0) {//原数组长度大于0,也就是初始化过了
if (oldCap >= MAXIMUM_CAPACITY) {//长度已经是最大值了,不进行扩容
threshold = Integer.MAX_VALUE;//hashCode计算位数为全部,对象的hashCode是通过地址算出来的,正常不会出现相等
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//新的长度小于最大值,原长度大于默认值
newThr = oldThr << 1; // double threshold//提高hashCode计算位数
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;//数组长度小于0,计算阈值大于0的情况,将长度扩到计算阈值大小
else { // zero initial threshold signifies using defaults//初始化
newCap = DEFAULT_INITIAL_CAPACITY;//默认大小16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认计算阈值为长度的0.75倍
}
if (newThr == 0) {//原数组长度大于0且计算阈值等于0或原数组长度等于0且阈值大于0的情况下
float ft = (float)newCap * loadFactor;//计算新的阈值,loadFactor默认为0.75,在初始化时可以设置
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);//新阈值和新数组长度都没有超过上限就是用ft,否者就用int最大值
}
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)//这里是这个下标上只有一个节点,就把这个节点直接放到新的下标上,就像原来低4位计算没有重复的key那现在扩容到低5为计算也不会有重复的key值
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;
//如上边的注所说,jdk8在这里进行了优化,因为每次扩容2倍即2进制进一位,而链表中的键值的hashCode的这一位,如4位扩到5位,就是5位为0的会在一个链表上,为1的会在另一个链表上,这样就生成了两个链表,这两个链表中的各节点的键值的hashcode低5为时相等的,然后将这两个链表放到新数组对应的地方,就完成的链表的新键值的计算与迁移
do {
next = e.next;
if ((e.hash & oldCap) == 0) {//扩容的那位数为0
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {//扩容那位数不为0
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在获取元素的时候以获取的键值的hashCode作为数组下标(这里也会做低位运算处理),找到对应的节点,再看看第一个节点键值是不是要找的,不是就开始遍历这个链表,找不到返回null
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//存储的数组不为空,且长度大于0,且数组对应位置上的节点不为空
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))//如果第一个节点就是,直接返回第一个节点的值
return first;
if ((e = first.next) != null) {//有下一个节点
if (first instanceof TreeNode)//如果是红黑树,对红黑树进行遍历获取
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//对链表进行遍历
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//是这个键值就返回
return e;
} while ((e = e.next) != null);
}
}
return null;
}
IdentityHashMap
与HashMap一样,只是其在相等的判断使用了==,即需要对象地址相等才会相等
LinkedHashMap
参考链接:https://blog.csdn.net/justloveyou_/article/details/71713781
https://blog.csdn.net/ns_code/article/details/37867985
以HashMap为基础,将其中的所有节点用双向链表的形式连在一起
ConcurrentHashMap
参考链接:https://www.cnblogs.com/leesf456/p/5453341.html
线程安全的HashMap,在线程安全的情况下保存的较好的性能.
先看其构造函数,
ConcurrentHashMap()
ConcurrentHashMap(int initialCapacity)
ConcurrentHashMap(Map<? extends K, ? extends V> m)
ConcurrentHashMap(int initialCapacity, float loadFactor)
ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
构造函数可不传参也可传入参数"initialCapacity"来设置其初始大小,默认为16的长度,传入参数"loadFactor"设置其负载因数,此值为定义map的满的程度,默认为0.75
其获取元素的方法与hashMap类似,且没有加锁,那说好的线程安全在哪里呢
再来看看插入元素
插入元素的具体实现,部分实现与Hashmap很像
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {//死循环
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//数组为空
tab = initTable();//初始化数组
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//获取tab数组下标为 (n - 1) & hash的节点,为空时
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))//tab的i下标是否为null,为null就用new Node<K,V>(hash, key, value, null)替换
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
SparseArray
此集合使用了两个数组来存储key和value,其中key限制为int类型.初始默认长度10,每次扩容2倍,使用System.arraycopy()方法复制到新数组上
在插入元素的时候,会使用二分法在key数组中搜索key,若搜索到了会将其值覆盖,若搜索不到会在二分法最后判断的位置(其实也就是key数组进行排序后,新key的位置)插入key值,并在value数组对应的下标位置上插入新value值.
在取值时,会通过key值进行二分法搜索,如搜索成功会返回value数组中对应下标的value.
SparseArray有两个优势:(可能不止,大概说一下)
1.因为使用数组存储,占用的空间会比较少;
2.因为使用二分法存取值,所以存取值的效率会比较高;(这个是相对来说的,毕竟二分法也有最好最坏的情况)
ArrayMap
与SparseArray存储的形式类似,都为两个数组,但其不限制key值的类型,一个数组用于存储key的hashCode(hashcode数组),另一个数组用于存储key和value值(元素数组),存储的格式为一个键一个值,初始默认长度10,每次扩容2倍,使用System.arraycopy()方法复制到新数组上.
在插入元素时会获取键值的hashCode,并使用二分法查找hashcode数组,如查找到有对应的hashcode会去覆盖元素数组的键和值,若找不到则在二分法最后搜索的位置进行插入操作,并在元素数组中插入对应的元素.
在取值的时候会获取使用二分法在hashcode数组中查找对应的位置,有则返回元素数组对应的值.
ArrayMap的优势与SparseArray类似,但其多了一个hashcode数组的开销,所以内存理论上是SparseArray的1.5倍,但正常情况下会不止1.5倍
WeakHashMap
内部逻辑与HashMap基本一样,相对于HashMap,其对键值的引用时弱引用,且在键值被GC时,将其加入ReferenceQueue(此队列记录了被GC的对象),在下次操作(size.get.put等操作)时会进行清除被回收对象,从源码中看出,其操作是在返回值前进行清除的,且清除时进行了加锁保护.
这里需要注意的是,这里键值被GC时不会主动删除这个键值对,而是存在队列中,等待下次操作才进行删除,如果为进行操作,这不会进行删除,此时就像一个HashMap存在,而且在键值存在强引用时,此map就相当于一个普通的HashMap了.
//进行清除时,会调用此方法
private void expungeStaleEntries() {
//在回收的队列中获取被回收的键值
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {//加锁
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC//置空
size--;
break;
}
prev = p;
p = next;
}
}
}
}
小结
java提供了很多的map,而且大部分操作都针对其痛点进行了优化,总的来说,看完这些map中的设计后,还是觉得自己还是太嫩了,虽然有些可能写错了,希望大家指出纠正