/**
* hash表是基于Map接口实现的,这个实现类提供了所有可选的map操作,
* 并且允许空的键和值(除了不支持并发和不允许空键值外,hashmap其他的和hashtable一样),
* 这个类不保证map的顺序,尤其注意,map不保证顺序随时间而变
*
*
* 假设哈希函数能将key值随机均匀的散列在哈希桶中,这个实现类的基础操作(put,get)随时间的变化性能不会发生太大的变化。
* 迭代内部集合所需要的时间与hashmap实例的容量(桶数)加上他的大小(键值对的数量)成正比,因此,如果迭代性能比较重要的话,
* 不能设置太高的容量或者太低的负载
*
*
* 一个hashmap的实例有两个参数影响他的性能:初始容量和负载因子。容量是hash表桶的数量,初始容量就是hash表创建的时候定的容量。
* 负载因子是衡量hash表容量达到多大的时候自动增长的参数。当数量超过当前容量和负载因子后,hash表会进行rehash(内部数据重新排列)
* hash表桶的数量大概增加两倍
*
*
* 一般情况下,默认负载因子(0.75)在时间和空间中是比较合理的。如果过高的话减少了空间成本但是增加了查找成本(主要在put和get操作上)
* 在设置初始容量的时候,要考虑预期条目和负载因子,是rehash的次数最低,如果初始容量比最大条目数除以负载因子,则不会触发rehash
*
*
* 如果许多键值映射保存到了hashmap的实例中,创建实例的时候赋予一个足够大的容量能够保存更多内容将会比频繁的rehash效率更好,
* 特别注意,过多相同的key(hashCode()的值一样)确认会使hash表的性能下降,为了改善这个的影响,当key实现了java.lang.Compareble
* 的时候,可以使比较这些key来帮助打断他们之间的关系(二次比较)
*
*
* 注意这个实现是非同步的,如果多个线程同步操作hashmap,并且至少有一个线程修改了hashmap的结构(增加删除,改和读不算),就必须在外部上锁
* 如果没有这样的对象,就需要用Collections.synchronizedMap来包装,最好在创建的时候就进行这步操作
*
*
* 所有的迭代器返回的视图(set,entrySet等)都具有fail-fast,在iterator进行创建的时候如果结构上发生了任何变化除了iterator的remove
* 都会抛出ConcurrentModificationException,因此在面对并发操作的时候,iterator会快速失败并且清除,而不是放任风险可能会发生在未来
* 不确定的操作上
*
*
* 注意fail-fast行为不能保证所有的东西,通常来说,在非同步并发修改下,不能做出任何有效保证,抛出ConcurrentModificationException
* 的fail-fast机制也是尽最大努力,因此,依赖异常解决正确性是不对的,fail-fast机制仅仅是用来发现bug
*/
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
/*
* map通常作为一个归置东西的hash表,但是当链表太大的时候,他们也会转换为红黑树,每一个结构都类似于java.until.TreeMap。
* 大多数方法尝试使用一般的链表,也会传到红黑树(通过判断节点是不是红黑树的实例),遍历红黑树可以像其他链表一样,但是在
* 过度填充的时候会更快些,然而因为绝大数链表不会过大,在表方法的过程中,使用链表会在前面。
*
*
* bins(比如size为8就是八个bin)通过hashcode排序,但是在这种关系下,如果两个元素都实现了Comparable<C>,他们的compareTo方法
* 会被用来排序。(我们校验类型是通过反射来做的)。当key有完全不同的hash值或者是被排过序的时候,使用树带来的复杂性是值得的
* 因为提供了最坏O(log n)的时间复杂度。因此,当偶然或者恶意使hashCode返回的值分布比较差以及许多key共享了一个hashcode,并且他们也是
* 被排过序的,使用树不会使性能下降过快(就是指链表过长性能比较低)。(如果以上两个都不适合并且不做任何操作,我们会浪费时间或者空间
* 但是唯一已知的情况是来自糟糕的用户编程实践,这些实践已经非常缓慢,几乎没有什么区别)
*
* 因为红黑树是常规节点大小的两倍,我们只会在bins包含了足够的节点数据的时候才会使用他(参考TREEIFY_THRESHOLD)。
* 并且当他们变得很小的时候(由于移除或者resize)会转化为链表,使用良好的良好的hashcode分布,树状结构很少用。
* 在理想情况下,均匀的hashcode,在默认扩容的阈值为0.75的时候,均值为0.5,节点在bins中出现的频率符合泊松分布
* 尽管由于粒度的调整,差距很大, 忽略方差,list的长度 k=(exp(-0.5) * pow(0.5, k) / factorial(k)) 第一个值是:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* 在往上: 少于一千万分之一
*
* 数的根节点通常是他的第一个节点,然后,有时(目前只有在Iterator.remove),根节点可能会在其他地方,但是可以通过
* 父链接(TreeNode.root())来恢复
*
* 所有的内部方法都接受hash值作为一个参数(通常通过公共方法提供),允许他们相互调用不需要重新计算hash值,大多数
* 内部方法也接受“tab”参数,通常是在当前表,但是可能也是一个新的或者旧的当resize或者转化的时候
*
*
* 当bin列表树化,分割,或者链表化的时候,我们保持他们的相对顺序(node.next)来更好的保存位置,为了在调用iterator.remove时
* 稍微简化处理分割和遍历。当在插入的时候用了比较器时,在重新平衡的时候为了保持完整的顺序(或者是尽可能的接近),我们将类和标识符码
* 作为接开关
*/
/**
* 默认初始容量,必须是2的幂次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量,2到2的30次方之间
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 构造函数中没有指定的负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 负载因子
* 使用数代替ist。在至少有这么多个节点的情况下,再添加一个元素进去,bin会被转换成树。
* 这个值必须大于2,并且应该至少是8,转化为链表时也一样
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 在resize操作的时候,小于六个会把树退化为链表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* map的size当没达到64个的时候,优先扩容而不是转化为树,这个值最少是TREEIFY_THRESHOLD 的四倍来防止hash冲突。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
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;
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
}
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)
n = (tab = resize()).length;// put第一个值的时候进行map的第一次扩容
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);//扩容以后,如果根据hash值算的当前槽位的值为空则newNode
else {//已形成链表或者红黑树
Node<K,V> e; K k;
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);//到达了八个,resize或者扩容。
break;
}
// 判断是否有相同的key
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)// onlyIfAbsent 参数为true就代表key已存在不进行替换
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;// 判断map的结构是否发生了变化
if (++size > threshold)// size大于了容量,进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();// 没到64先扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
/**
* 初始化或者扩大两倍,如果是空。根据threshold分配初始大小,否则,由于我们进行了
* 两倍的扩容,所以原来的数据要不待在原位置,要不就要以2为幂的偏移量移动
* @return the table
*/
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; // 两倍扩容
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 初始化第一次
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 { // 高低位定位并且采用了尾插法
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) {
//hash = (h = key.hashCode()) ^ (h >>> 16)
//以八为例,hash & 8 != 0就是hash值要是8的倍数,此时&7的话就是原map中第一位的值放在高位
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;// 如果是八个扩容到16,则第一位的挪到了第九位
}
}
}
}
}
return newTab;
}
hashMap在resize的时候,如果红黑树的节点小于了6为转化为链表,而在remove的时候,则是判断而是通过红黑树根节点及其子节点是否为空来。
迭代
hashmap有KeySet(key值的集合),EntrySet(键值的集合),Values(值的集合,继承的是AbstractCollection)
jdk1.8引入了Spliterator,另外分析。
hashTable
hashTable和hashMap的基本操作类型一样,hashTable不允许key或者value为空,会直接抛出NullPointException,hashTable的线程安全是通过在方法上加synchronized实现的,导致hashTable的在多线程并发的性能不太行。
ConcurrentHashMap
1:key和value都不允许为空
2:集合操作不在抛出ConcurrentModificationException,注释给的解释是,在并发执行中,迭代器反映的是瞬时状态,可以用来监控,但是不能用于程序判断。
3:ConcurrentHashMap的最大值不是INT.MAXVALUE,而是INT.MAXVALUE-8,至于为什么减,我不知道。
未完待续。。。