目录
HashMap
关系图
doc
- 基于哈希表的Map接口实现。这个实现提供了所有可选的映射操作,并允许空值和空键。(HashMap类与Hashtable大致相当,只是它是不同步的,并且允许为null)这个类对映射的顺序不做任何保证;特别是,它不保证顺序将随着时间的推移保持不变。
- 这个实现为基本操作(get和put)提供了恒定的时间性能,假设hash函数在bucket中适当地分散了元素。集合视图上的迭代所需的时间与HashMap实例的“容量”(bucket的数量)加上其大小(键值映射的数量)成比例。因此,如果迭代性能很重要,那么不要将初始容量设置得太高(或者负载系数太低),这一点非常重要。
- HashMap的实例有两个影响其性能的参数:初始容量和负载因子。capacity是哈希表中的bucket数,初始容量就是创建哈希表时的容量。加载因子是一个度量哈希表在容量自动增加之前可以达到的完整程度。当哈希表中的存储数超过加载因子与当前容量的乘积(16x0.75)时,哈希表将重新哈希(即重建内部数据结构),使哈希表的存储桶数大约为原来的两倍。
- 一般来说,默认的负载系数(0.75)在时间和空间成本之间提供了很好的折衷。较高的值会减少空间开销,但会增加查找开销(反映在HashMap类的大多数操作中,包括get和put)(意思就是加载因子设置0.75比较合适虽然这值过高会减少空间存储消耗但是这样查找就会花费较多时间是,属于最优解)在设置初始容量时,应考虑地图中的预期条目数及其荷载系数,以尽量减少再灰化操作的次数。如果初始容量大于最大入口数除以负载系数,则不会发生再吹灰操作。
- 如果要在一个HashMap实例中存储许多映射,那么以足够大的容量创建它将使映射的存储效率更高,而不是让它根据需要执行自动重新缓存以增加表。请注意,使用具有相同hashCode()的多个键肯定会降低任何哈希表的性能。为了改善影响,当键是可比较的时,这个类可以使用键之间的比较顺序来帮助打破联系。
- 请注意,此实现不是同步的(线程不安全)。如果多个线程同时访问一个哈希映射,并且至少有一个线程在结构上修改了该映射,则它必须在外部同步。(结构修改是指添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键相关联的值不是结构修改。)这通常是通过对自然封装映射的对象进行同步来完成的。如果不存在这样的对象,则应该使用集合.synchronizedMap方法。最好在创建时执行此操作,以防止意外的不同步访问映射:
- HashMap 会导致除了迭代器本身的 remove 外,外部 remove 方法都可能会导致 fail-fast (快速失败)机制,因此尽量要用迭代器自己的 remove 方法。如果在迭代器创建的过程中修改了 map 的结构,就会抛出 ConcurrentModificationException 异常。
Map m = Collections.synchronizedMap(new HashMap(...));
- 这个类是Java集合框架的一个成员。
属性
/**
* 默认初始容量-必须是2的幂。
* 1向左移位4个,00000001变成00010000,也就是2的4次方为16,
* 使用移位是因为移位是计算机基础运算,效率比加减乘除快。jdk1.7
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量,如果某个具有参数的构造函数隐式指定了更高的值,
* 则使用该值。必须是2的幂<=1<<30。即2的30次方
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 构造函数中未指定时使用的负载因子。常和默认值连用进行扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 使用树(而不是列表)的存储箱计数阈值。将元素添加到至少具有这么多节点的bin时,
* 存储单元将转换为树。该值必须大于2,且至少应为8,
* 以符合树木移除中关于在收缩时转换回普通垃圾箱的假设。
(意思当某个桶(链表)节点数量大于8时,会转换为红黑树。这个得看 hashMap的
数组容量如果容量小于64会先扩容 容量大于6才会转换成红黑树)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*当整个hashMap中数组容量大于64时,且是链表会进行转为红黑树结构。
若小于64则会进行扩容
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
这张表,在第一次使用时初始化,并根据需要调整大小。分配时,长度总是2的幂。
(在某些操作中,我们也允许长度为零,以允许当前不需要的引导机制。)
(存储元素的数组,transient关键字表示该属性不能被序列化 )
*/
transient Node<K,V>[] table;
/**
* 将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 元素数量
*/
transient int size;
/**
记录当前map被修改的次数
*/
transient int modCount;
/**
* 要调整大小的下一个大小值 临界值为(容量*负载系数)。
* 默认0.75X16
* @serial
*/
// (javadoc描述在序列化时为true。
//另外,如果尚未分配表数组,则
//字段保留初始阵列容量,或零表示
//默认初始容量。)
int threshold;
/**
* 哈希表的加载因子。变量记录当前的加载因子
*
* @serial
*/
final float loadFactor;
构造方法
/**
*构造一个具有默认初始容量(16)和默认加载因子(0.75)的空HashMap。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
构造一个具有指定初始容量和默认加载因子(0.75)的空HashMap。
*
* @param 指定初始容量
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 构造具有指定初始容量和负载因子的空HashMap。
*
* @param 指定初始容量
* @param 指定加载因子
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//初始容量不能<0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//初始容量不能大于最大值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。
HashMap是使用默认加载因子(0.75)和足以在指定映射中保存映射的初始容量创建的。
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
//实现Map.putAll and Map constructor
putMapEntries(m, false);
}
HashMap(Map<? extends K, ? extends V> m)
实现Map.putAll and Map constructor
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
/**
* Implements Map.putAll and Map constructor
* 实现Map的putAll 和 Map的constructor
*
* @param m the map
* @param evict false when initially constructing this map, else
* true (relayed to method afterNodeInsertion).
*/
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取集合
int s = m.size();
if (s > 0) {
//当前hashMap里没有元素
if (table == null) { // pre-size
/**求出需要的容量,因为实际使用的长度=容量*0.75得来的,+1是因为小数相除,
基本都不会是整数,容量大小不能为小数的,后面转换为int,多余的小数就要被丢掉,所以+1,
例如,map实际长度22,22/0.75=29.3,所需要的容量肯定为30,
有人会问如果刚刚好除得整数呢,除得整数的话,容量大小多1也没什么影响**/
float ft = ((float)s / loadFactor) + 1.0F;
//限定容量必须<=最大值
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
//当容量>临界值需重新设置临界值
threshold = tableSizeFor(t);
}
else if (s > threshold)
//集合元素数量>临界值 需要扩容 后面put再说该方法
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//把map中的数据遍历转到hashMap中,put时会详解
putVal(hash(key), key, value, false, evict);
}
}
}
总结
由上述我们在通过构造方法构建hashMap时不指定加载因子和初始容量时默认使用16临界值为16*0.75。当使用自定义的加载因子和初始容量,此时hashMap的最大容量不能超出2的30次方。但是我们在后续扩容是可以扩大到Integer的最大容量的
单项链表Node
直接实现Map.Entry 为啥实现该接口 因为Map.Entry里面存储了key和value不需要遍历就能拿到对应key和value
/**
基本哈希bin节点,用于大多数条目。
(TreeNode子类见下文,Entry子类见LinkedHashMap。)
该方法实现Map.Entry<K,V> 重写了对应方法 类似于单向链表连接方式
*/
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 K getKey() { return key; }
public final V getValue() { return 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;
}
}
红黑树TreeNode(重要)
结构图
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 红黑树链接 父节点
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 删除后需要取消链接
boolean red; //红 黑
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* 返回根节点.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
TreeNode.Split()
/**
*将树容器中的节点拆分为较低和较高的树容器,如果现在太小,则取消搜索。
仅从resize调用;请参阅上面关于拆分位和索引的讨论。
*
* @param map map对象
* @param tab 新的存储数据的单项链表的数组
* @param index 当前正在拆分的表的索引
* @param bit 旧存储数据数组length
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
//当前红黑树对象
TreeNode<K,V> b = this;
// 重新链接到lo和hi列表,保持顺序
//低位链表局部变量
TreeNode<K,V> loHead = null, loTail = null;
//高位链表局部变量
TreeNode<K,V> hiHead = null, hiTail = null;
//lc低位计数,hc高位计数 计数是用来判断是否要转成单向链表
int lc = 0, hc = 0;
//遍历红黑树
for (TreeNode<K,V> e = b, next; e != null; e = next) {
//得到当前红黑树下游节点
next = (TreeNode<K,V>)e.next;
//将下游节点设置为null
e.next = null;
//判断是否属于低位链表
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
//低位树链表的第一个树链表节点
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;//低位树元素个数计数
}
else {
//高位节点
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;//计数
}
}
/*
从这我们可以看出tab[index] 的值为一个单项链表,所以hashMap底层使用的是
数组加红黑树或单项链表的方式去存储数据的,
当高低位链表小于6时他就会将红黑树转换成单向链表。
*/
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)//判断低位链表桶节点是否小于6
//将红黑树转成单向链表存储。迁移到新数组中下标不变,还是等于原数组到下标
tab[index] = loHead.untreeify(map);
else {
//大于6就不需要转成单向链表
tab[index] = loHead;
/*
如果存在高位链表说明此时存在2种链表
*/
if (hiHead != null) // (else is already treeified)
//此时的重新构建红黑树
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
//高位链表 newindex= 当前索引+oldlength 其他同低位链表一样
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
/**
* 将红黑树节点转成单向链表
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
//定义单项链表 hd 首节点 tl 中转节点
Node<K,V> hd = null, tl = null;
//获取当前节点进行遍历
for (Node<K,V> q = this; q != null; q = q.next) {
/*
replacementNode用于从TreeNodes到链表的转换的转换
底层就创建了一个Node单向链表节点
*/
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;//设置头节点 第一次遍历时
else //后续遍历
tl.next = p;//用来链接后续节点
tl = p;//链接中转 如果p为首后续遍历始终是给当前节点添加下游
}
return hd;//返回遍历好的单向链表 索引不变
}
TreeNode.spilt()总结(面试必问):
在split()方法这我们可以看出tab[index] 的值为一个单项链表,所以hashMap底层使用的是数组加红黑树或单项链表的方式去存储数据的, 当高低位链表节点小于6时他就会将红黑树转换成单向链表,此时当为低位时此时原oldCap对应数组存储的链表(或红黑树)的索引是不会改变,高位则值的新索引=原index+原容量length。
Put新增
/**
*将指定值与此映射中的指定键相关联。如果映射先前包含键的映射,则替换旧值。
*
* @param 要与指定值关联的键键
* @param 要与指定键关联的值
* @return 与键关联的上一个值,如果没有键的映射,则为null。
* (空返回还可以指示映射先前将null与键关联。)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal()实现Map.put 和相关方法
/**
* 实现Map.put 和相关方法
*
* @param key的hash值
* @param 要put的 key
* @param 要put的 value
* @param onlyIfAbsent 如果为true,则不更改现有值
意思表示如果该key存在值,将现有值覆盖原有,如果为null的话,则插入新的value
* @param evict 如果为false,则表示处于创建模式。
* @return 判断是否添加成功 返回 null 表示添加成功
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
/*
tab链表数组 p中转节点用来链接子节点
n 默认容量 i 获取的小标
*/
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table==null代表首次添加
if ((tab = table) == null || (n = tab.length) == 0)
//此时n等默认容量16
n = (tab = resize()).length;
/*
判断高低位链表详情看后续 e.hash & (newCap - 1)算法推导
*/
if ((p = tab[i = (n - 1) & hash]) == null)
//创建首节点存入tab[]数组中
tab[i] = newNode(hash, key, value, null);
else {
//此时p已经存在值 表示此时已经不是首节点
Node<K,V> e; K k;
// 如果新插入的结点和table中p结点的hash值,key值相同的话
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);
//binCount 子节点数(桶)>8-1时
if (binCount >= TREEIFY_THRESHOLD - 1) // -1代表1
//当桶(链表的)节点传入数组和其对应的hash值
//用来判断该链表结构是扩容还是转换成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 如果新插入的结点和table中p结点的hash值,key值相同的话终止循环
break;
//将链接后的节点重新赋值给局部变量p
p = e;
}
}
//此处判断是否有有冲突的key
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//将当前传过来值覆盖原数据 从事
e.value = value;
//允许LinkedHashMap post操作的回调
afterNodeAccess(e);
//因为返回oldValue,e.value 可能为null 所有null值也是可以添加的
return oldValue;
}
}
++modCount;//当前修改次数
//当元素数量大于临界值
if (++size > threshold)
//扩容
resize();
//插入节点后 回调接口
afterNodeInsertion(evict);
return null;
}
treeifyBin
。
/**
* 替换给定哈希的索引处bin中的所有链接节点,除非表太小,在这种情况下会调整大小
* 意思就是说hashMap的length<64 就属于表太小了 大于64且链表节点>8才会去将链表转换成
* 红黑树
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果当前hashMap的数组长度<64 而且每个数组索引存储的链表节点>8时 会扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//当前hashMap的数组长度>64 而且每个数组索引存储的链表节点>8时 就会将其转换成红黑树
//出于性能考虑
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);
}
}
插入总结(面试常问问题)
当我们使用put时底层会调用putVal()方法将对应 key value onlyIfAbsent(是否更改先有值)等字段,系统会默认初始容量为16的数组存储数据,如果大于临界值就会扩容resize(),存储数据时key,value都可以为null。如果已经存在节点添加时加时会判断是否为红黑树链表如果是向树中添加数据不是遍历单项链表的next节点查询对应值添加数据至末尾,如果单项链表的子节点(桶)Node>TREEIFY_THRESHOLD - 1(8-1)时 此时需要判断当前hashmap的存储数组的length(数组长度小于<64 则进行扩容并不会转换成红黑树,当>64则将链表转换成红黑树)进行存储 索引安排具体看e.hash & (newCap - 1)算法推导。当存在重复数据 onlyIfAbsent为true时 覆盖原始数据。如果此时插入完成后大于临界值则resize()
resize()扩容
/**
*初始化或加倍属性table大小。如果为空,则根据字段阈值中保留的初始容量(16)目标进行分配。
否则,初始化扩容必须是2的倍数(例如16X2),每个bin中的元素必须保持在相同的索引中,
或者在新表中以2的幂次偏移量移动。
*
* @return the table
*/
final Node<K,V>[] resize() {
//获取当前对象的存储元素的 Node数组属性
Node<K,V>[] oldTab = table;
//获取当前数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取当前对象的临界值
int oldThr = threshold;
//重新定义当前的数组长度和临界值
int newCap, newThr = 0;
if (oldCap > 0) {
//如果当前hashMap对象已经存有值 且大于最大
if (oldCap >= MAXIMUM_CAPACITY) {
//设置当前对象临界值为整数的最大值
threshold = Integer.MAX_VALUE;
//扩容完成 返回当前节点
return oldTab;
}
/*
oldCap << 1 表示想左移动一位2的1次方 newCap=oldCap*2
*/
//新数组长度 = (16 <= 新的容量小 < 2^30 )时
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//新临界值是原来临界值的2倍
newThr = oldThr << 1; // double threshold
}
//初始容量处于临界值
else if (oldThr > 0) // initial capacity was placed in threshold
//新数组容量等于当前对象的最大临界值容量
newCap = oldThr;
else { //没有临界值 数组长度使用默认值16
newCap = DEFAULT_INITIAL_CAPACITY;
//临界值为16*0.75
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/*
新临界值为0 表示之前并没有初始化(hashMap扩容使用的是懒加载)
此时这里使用的是自定义的加载因子
*/
if (newThr == 0) {
// 得到新的临界值容量
float ft = (float)newCap * loadFactor;
//临界值= 新的容量ft还是Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将新的临界值更新覆盖到当前对象threshold 属性中
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建新的Node节点数组使使用扩容之后的length
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//将分析得到扩容完成的节点赋值给当前hashMap对象table 属性
table = newTab;
//旧的存储元素数组不为空
if (oldTab != null) {
//遍历为扩容前里面元素 通过高低运算符来确定对应下标
for (int j = 0; j < oldCap; ++j) {
//设置hash节点
Node<K,V> e;
//赋值当前元素!=null时
if ((e = oldTab[j]) != null) {
oldTab[j] = null;//原节点=null
if (e.next == null)
/*
将的值存到新的数组中算出来的索引位置处
若为0 则索引位置 新旧一样
!=0 则索引位置为 oldindex+旧容量length
此时newCap =2倍的oldCap
e.hash & (newCap - 1)算法会区分新数组索引位置
具体看下述
*/
newTab[e.hash & (newCap - 1)] = e;
//判断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 {
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;
}
扩容总结
获取old存储元素数组进拿到对应的length,容量,临界值。临界值大于加载因子当前容量时扩容。
新的数组容量扩容至2倍,临界值重新赋值为new容量加载因子。此时如果为Node节点将原始数据替换到新的数组当中,通过e.hash & (newCap - 1)算法推导 重新将每个元素分配指定数组下标位置。如果为TreeNode节点则调用TreeNode.split方法也是通过高低位链表将红黑树存储到指定索引数组下标位置
Remove删除
/**
* 从该映射中删除指定键的映射(如果存在)。
*
* @param key 要从映射中删除其映射的键
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Implements Map.remove相关方法
*
* @param hash hash值
* @param key 键
* @param value 如果匹配值,则忽略匹配值
* @param matchValue 如果为true,则仅在值相等时移除
* @param movable如果为false,则在移除时不要移动其他节点
* @return 返回删除的node, 如果没有,则为空
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
// 定义节点数组tab用于指向table、节点p、数组长度n、hash所映射的数组下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 节点数组在hash位置处的节点不为空,若为空则直接返回null(不存在可删除元素)
//tab[index = (n - 1) & hash] 表示首索引对应的链表值 首节点!=null
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
/*
定义局部节点变量node存储需要删除的元素、
循环变量e(便于查找对应的删除元素如果非首节点就是要找的元素 后面循环完e =node)、
key、value
*/
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//如果首节点key和hash值都与传过来相同
//p就是要删除的节点吗
node = p;
//如果不相等e=next节点
else if ((e = p.next) != null) {
//判断是否属于红黑是
if (p instanceof TreeNode)
//是直接通过 hash 和key去找到对应节点强转成单向链表
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;//没找到p=oldp.next
} while ((e = e.next) != null);//e取olde的next !=null
}
}
/*
参数设置为value值不相等也可以移除
|| 删除节点的value值 == 传过来的value(可能为null)
||value值不为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;//length-1
afterNodeRemoval(node);
return node;//返回删除的节点
}
}
return null;
}
删除总结
我们在删除hashMap的某一个值时底层remove方法会调用removeNode节点将对应key的hash值与key传入指定好特性定位到对应存储数组位置。通过对应参数和推导算法确定第一个索引存储的链表的首节点,如果首索引的值就是要找的元素直接删除。如果不是取当前首节点的下游链表数据判断是否为红黑树和单项链表 对应去删除对应元素节点并返回该删除的节点 否则返回null。
e.hash & (newCap - 1)算法推导(重要)
HashMap 和 HashTable 的区别
相同点
- HashMap 和 HashTable 都是基于哈希表实现的,其内部每个元素都是 key-value 键值对,HashMap 和 HashTable 都实现了 Map、Cloneable、Serializable 接口。
不同点
- 父类不同:HashMap 继承了 AbstractMap 类,而 HashTable 继承了 Dictionary 类
- 空值不同:HashMap 允许空的 key 和 value 值,HashTable 不允许空的 key 和 value 值。HashMap 会把 Null key 当做普通的 key 对待。不允许 null key 重复。
- 线程安全性:HashMap 不是线程安全的,如果多个外部操作同时修改 HashMap 的数据结构比如 add 或者是 delete,必须进行同步操作,仅仅对 key 或者 value 的修改不是改变数据结构的操作。可以选择构造线程安全的 Map 比如 Collections.synchronizedMap 或者是 ConcurrentHashMap。而 HashTable 本身就是线程安全的容器。
- 性能方面:虽然 HashMap 和 HashTable 都是基于单链表的,但是 HashMap 进行 put 或者 get 操作,可以达到常数时间的性能;而 HashTable 的 put 和 get 操作都是加了 synchronized 锁的,所以效率很差。
常见面试题
位运算符
/**
* 位运算符
*/
public class Test {
public static void main(String[] args) {
/**
* << 左移 3 << 2 = 12 -> 3*2^2 = 12
* >> 右移 4 >> 2 = 2 -> 4/2^1 = 2
* >>> 无符号运算符 3 >>> 1 = 1 -> 3/2^1 = 1
* & 与运算符 6&3 = 2
* 6 = 00110
* 3 = 00011
* ----------
* 00010 = 2^1 = 2
*/
System.out.println(3 << 2);//12
System.out.println(4 >> 1);//2
System.out.println(3 >>> 1);//1
System.out.println(6 & 3);//2
}
}