标签:同步 成本 容量 安全 nes 创建 tle 哈希算法 get
public class TestHashMap {
public static void main(String[] args) {
HashMap<String, String> hashMap = new HashMap<String, String>();
hashMap.put("fu", "辅助");
hashMap.put("ad", "输出");
hashMap.put("sd", "上单");
System.out.println(hashMap);//toString重写了,所以可直接打出
System.out.println("fu:" + hashMap.get("fu"));//拿出key为fu的键值
System.out.println(hashMap.containsKey("fu"));//判断是否存在fu的键
System.out.println(hashMap.keySet());//返回一个key集合。
System.out.println("判空:"+hashMap.isEmpty());//判空
hashMap.remove("fu");
System.out.println(hashMap.containsKey("fu"));//判断是否存在fu的键
Iterator it = hashMap.keySet().iterator();//遍历输出值。前提先拿到一个装载了key的Set集合
while(it.hasNext()) {
String key = (String)it.next();
System.out.println("key:" + key);
System.out.println("value:" + hashMap.get(key));
}
}
}
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16,必须是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;
//!!Java 8 HashMap的分离链表。 在没有降低哈希冲突的度的情况下,使用红黑书代替链表。
/*
使用链表还是树,与一个哈希桶中的元素数目有关。下面两个参数中展示了Java 8的HashMap在使用树和使用链表之间切换的阈值。当冲突的元素数增加到8时,链表变为树;当减少至6时,树切换为链表。中间有2个缓冲值的原因是避免频繁的切换浪费计算机资源。
*/
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
//HashMap的一个内部类,实现了Map接口的内部接口Entry。Map接口的一系列方法
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;//哈希值
final K key;
V value;
Node<K,V> next;//对下一个节点的引用(看到链表的内容,结合定义的Entry数组,哈希表的链地址法!!!实现)
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 K getKey() { return key; }//获取Key
public final V getValue() { return value; }//获取Value
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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;
}
}
// 存储数据的Node数组,长度是2的幂。
// HashMap采用链表法解决冲突,每一个Entry本质上是一个单向链表
transient Node<K,V>[] table;
//缓存我们装载的Node,每个结点。这也是跟keySet一样,可用于遍历HashMap。遍历使用这个比keySet是快多的喔,一会介绍并给例子。
transient Set<Map.Entry<K,V>> entrySet;
// HashMap的底层数组中已用槽的数量
transient int size;
// HashMap被改变的次数
transient int modCount;
// HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子)
int threshold;
// 加载因子实际大小
final float loadFactor;
/*
构造器
*/
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量不能 > 最大容量值,HashMap的最大容量值为2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//负载因子不能 < 0
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//当在实例化HashMap实例时,如果给定了initialCapacity,由于HashMap的capacity都是2的幂,因此这个方法用于找到大于等于initialCapacity的最小的2的幂(initialCapacity如果就是2的幂,则返回的还是这个数)。
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;
}
//本质还是上面的构造器,只不过不选择加载因子而已
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//这里更是两个都不选,都选取默认的大小。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//这个大概了解就是,可以用Map来构造HashMap咯
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//那堆获取size,判空方法就不列了。
// 获取key对应的value
public V get(Object key) {
Node<K,V> e;
// 获取key的hash值
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n;
K k;
// 在“该hash值对应的链表”上查找“键值等于key”的元素。也就是取出这个链表上,索引对应的值。
//这里一边判断一边赋值了,1.把要查的那一行table数组给到临时数组tab(那一行的table数组是通过hash计算得出的);2.把那一行的数组的第一个给到暂存结点first。(所谓第一个其实是单链表中头部,链表的头插法)
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 直接命中
if (first.hash == hash && // 每次都是校验第一个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;
}
//红黑树查找法
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
//根节点
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* 从根节点p开始查找指定hash值和关键字key的结点
* 当第一次使用比较器比较关键字时,参数kc储存了关键字key的 比较器类别
* 非递归式的树查询写法。。。有点复杂。
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;//一开始是根节点,然后遍历下去,表示当前结点
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h) //如果给定哈希值小于当前节点的哈希值,进入左节点
p = pl;
else if (ph < h)//如果大于当前节点,进入右结点
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk))) //如果哈希值相等,且关键字相等,则返回当前节点,终止查找。
return p;
else if (pl == null) //如果左节点为空,则进入右结点
p = pr;
else if (pr == null)//如果右结点为空,则进入左节点
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0) //如果不按哈希值排序,而是按照比较器排序,则通过比较器返回值决定进入左右结点
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null)//如果在右结点中找到该关键字,直接返回
return q;
else
p = pl; //进入左节点
} while (p != null);
return null;
}
//同理推出是否包含某key的方法
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
/*
插入方法!!为了好理解,我们先去看下文讲的jdk1.7的put吧,这个jdk1.8的红黑树、链式的转换太复杂了,一会回来再看。
*/
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;
//判断table是否为空
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//创建一个新的table数组,用resize确定大小,并且获取该数组的长度
//根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果对应的节点存在
Node<K,V> e; K k;
//判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 该链为链表,就用链地址法
else {
//遍历table[i],判断链表长度是否大于TREEIFY_THRESHOLD(默认值为8),大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
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);
break;
}
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)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
/*
树形化总过程:遍历桶中的元素,创建相同个数的树形节点,复制内容,建立起联系
然后让桶第一个元素指向新建的树头结点,替换桶的链表内容为树形内容
但是我们发现,之前的操作并没有设置红黑树的颜色值,现在得到的只能算是个二叉树。在 最后调用树形节点 hd.treeify(tab) 方法进行塑造红黑树。红黑树的构造就看我github不久后的复习笔记吧。
*/
//如果冲突达到8位,就转树形结构,这就是转型的代码:
//将桶内所有的 链表节点 替换成 红黑树节点
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前哈希表为空,或者哈希表中元素的个数小于 进行树形化的阈值(默认为 64),就去新建/扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//如果哈希表中的元素个数超过了 树形化阈值,进行树形化
// e 是哈希表中指定位置桶里的链表节点,从第一个开始
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;//红黑树的头、尾节点
do {
//新建一个树形节点,内容和当前链表节点 e 一致
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);
}
}
/*
扩容机制,也是确定容量的方法
扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
*/
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;
}
// 没超过最大值,就扩充为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的resize上限
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) {
// 把每个bucket都移动到新的buckets中
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 { // preserve order
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) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { // 原索引+oldCap
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {// 原索引放到bucket里
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) { // 原索引+oldCap放到bucket里
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/*
删除:
1.计算哈希值找到对应的桶
2.在桶中寻找,当然查之前要判断桶类型是红黑树还是链表
3.找到只会就删除嘛,当然也要对应桶类型
*/
public V remove(Object key) {
Node<K,V> e;
//查找到则返回该结点,没有则返回null
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 直接命中
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)// 如果是红黑树在红黑树中查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {// 在链表中查找
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//终于找到了??那就去删除啊
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 在红黑树中删除节点
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)// 链表首节点删除
tab[index] = node.next;
else // 多节点单链表删除
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
/*
清空:遍历所有桶的所有元素并至null
*/
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
}
/*
可以看到很简单的步骤:1.计算哈希,找到对应的Entry,;2.插入单链表到头部
*/
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);//注意,HashMap可以存放null的key!!当然只能存一个!下面我们来看看,怎么放null的
int hash = hash(key);
int i = indexFor(hash, table.length);//计算哈希
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//计算的哈希值锁定对应的Entry并且遍历单链表
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++;
addEntry(hash, key, value, i);
return null;
}
private V putForNullKey(V value) {
//nullkey的话,就锁定第一个Entry了,遍历单链表找到插入位置先
//如果找到了e.key==null,就保存null值对应的原值oldValue,然后覆盖原值,并返回oldValue
//如果在table[0]Entry链表中没有找到就调用addEntry方法添加一个key为null的Entry
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++;
addEntry(0, null, value, 0);//添加
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//判断是否要扩容.hashmap每次扩容的大小为2倍原容量,默认容量为16,hashmap的capacity会一直是2的整数幂。
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
//扩容
void resize(int newCapacity) {//传入新的容量
Entry[] oldTable = table;//引用扩容前的Entry数组
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {//扩容前的数组大小如果已经达到最大(2^30)了
threshold = Integer.MAX_VALUE; //这样的话就修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
return;
}
Entry[] newTable = new Entry[newCapacity];//初始化一个新的Entry数组
transfer(newTable, initHashSeedAsNeeded(newCapacity));//将数据转移到新的Entry数组里,并判断是否需要更新哈希值
table = newTable;//HashMap的table属性引用新的Entry数组
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//修改阈值
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//根据put中锁定的Entry去遍历
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {//要不要重新分配hash
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//重新计算每个元素在数组中的位置,找到存放在新数组中的位置,再放置
e.next = newTable[i];//标记
newTable[i] = e; //将元素放在数组上
e = next;//访问下一个Entry链上的元素
}
}
}
public class TestHashMap {
public static void main(String[] args) {
HashMap<String, String> hashMap = new HashMap<String, String>();
for (int i = 0; i < 100000; i++) {
hashMap.put(String.valueOf(i), "fuzhu");
}
Iterator it = hashMap.keySet().iterator();//遍历输出值。前提先拿到一个装载了key的Set集合
long start = System.nanoTime();
while(it.hasNext()) {
String key = (String)it.next();
//System.out.println("key:" + key);
//System.out.println("value:" + hashMap.get(key));
}
long time = System.nanoTime() - start;
long start2 = System.nanoTime();
Iterator it2 = hashMap.entrySet().iterator();
while(it2.hasNext()) {
Map.Entry key = (java.util.Map.Entry)it2.next();
//System.out.println("key:" + key);
// System.out.println("value:" + hashMap.get(key));
}
long time2 = System.nanoTime() - start2;
System.out.println("测试耗时1!!!!"+time);
System.out.println("---------------------------------");
System.out.println("测试耗时2!!!!"+time2);
}
}
/*
最终结果:
如果注释部分不执行,keySet方式是比entrySet快得多(对应下图1),但是注释部分执行时,也就是真正的遍历取值时,entrySet比keySet快得多(对应下图2)
*/
//原因:keySet方式拿到的是装载String的set(需要再次转换拿到key),而entrySet方式拿到的是装载Map.Entry类型的set,无须再次转换,直接getvalue
深入Java基础(四)--哈希表(1)HashMap应用及源码详解
标签:同步 成本 容量 安全 nes 创建 tle 哈希算法 get
原文地址:http://blog.csdn.net/jack__frost/article/details/69388422