Map接口
常用实现类的继承关系
HashMap
继承关系
注意:
HashMap
对于添加进去的键值对,它是不保证顺序的,添加顺序是可能和存储顺序是不一致的HashMap
对于元素的添加和查找操作的时间复杂度是常数时间复杂度- 是线程不安全的
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认为16的初始化长度
static final int MAXIMUM_CAPACITY = 1 << 30;//最大为2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认为0.75的装载因子
static final int TREEIFY_THRESHOLD = 8;//单点“挂点”(bin)阈值,超过这个值变为红黑树存储
static final int UNTREEIFY_THRESHOLD = 6;//红黑树中存储的节点数少于这个值就会转换为链表存储
static final int MIN_TREEIFY_CAPACITY = 64;//bins链表转换为红黑树时的最小hashmap的长度(也就是说如果bins中装的节点数超过了上面那个TREEIFY_THRESHOLD,但是hashmap长度还没有超过MIN_TREEIFY_CAPACITY,就要先扩容,而不是转换为红黑树)
transient Set<Map.Entry<K,V>> entrySet;
transient int size;//map 中实际有的键值对的数量
transient int modCount;//每次改变映射和内部结构就会+1
final float loadFactor;//装载因子
int threshold;//下一次需要扩展时候的map容量(一定是一个2的整数次幂);用tableSizeFor(initialCapacity)函数算出不小于initialCapacity的最小二进制数
transient Node<K,V>[] table;//Node数组,存放添加的元素
/*插入hashmap中的实际元素的结构,一个个的node节点
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;//指向同hash到同一位置的节点
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() {//计算添加元素的hash值相同时,需要调用equals判断两者到底想不想等,相等的话就不能添加
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) {//在两个元素的hash值相同时需要
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
,源码太长就不贴了,后面用到会截取分析
初始化
几个名词的解释:
capacity
:HashMap
的容量load factor
:装载因子 = 实际元素个数 / 容量;当实际元素个数 > 容量 * 装载因子,HashMap
就要扩容了,一把来说,这个值设置为 0.75bin
:挂在节点数组上的链表
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; //其他成员变量赋值为默认值
}
public HashMap(int initialCapacity) {//自定初始容量
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {//自定初始容量和装载因子
if (initialCapacity < 0)//容量小于零抛异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)//容量大于max则设置为max
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//装载因子为0或者不是有效输入抛异常
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
添加与修改元素
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
插入过程:
- 根据插入的
(k,v)
的键k
,调用自己类中重写的hashCode()
计算出一个哈希值 - 哈希值根据一些算术运算对应出一个
table
中的位置- 如果这个位置没有元素就直接插入
- 如果这个位置有元素b,c,d,e…了
- 通过循环比较当前元素和这些元素的哈希值,如果哈希值都不同,插入到同一位置的链末尾上(注意在java7之前是使用头插的)
- 如果存在该元素和链上某一个的元素哈希值相同,就调用
equals()
方法,如果不相等也就插入到同一位置的链上 - 如果
equals()
也是相同的,那么表明两个元素是相同的元素,更新该位置上的键值
- 代码如下:
/*hash:插入键的hash值
key:键
value:值
onlyIfAbsent:true的话不改变现有的值
evict:false就是在creation mode
*/
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;//初始为空的时候插入元素,设置为16的默认值
//(length - 1) & hashCode()计算元素的对应防止位置
if ((p = tab[i = (n - 1) & hash]) == null)//根据哈希值计算出来的位置没有元素,直接插入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//要插入的元素和对应位置的元素key值相同
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) // 超过阈值,链表转换为红黑树存储同一位置节点了
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;
}
由上面的插入过程可以看出,为了保证确定待插入元素key值是是否已经存在,需要先调用元素key对应的类中
hashCode()
方法得到对应的插入位置;遇到相同的哈希值的两个元素需要使用equals()
方法判断两个元素是否相等;
所以被添加到HashMap
中的key所对应的类必须重写hashCode()
和equals()
方法
jdk8中哈希到同一位置的元素使用尾插的原因:未完待续
修改元素和插入过程是一样的,同样的元素put就会覆盖原有元素
在插入过程中但实际元素个数超过阈值loadFactor * threshold
时,就会触发Node[]
数组的扩容机制,具体的扩容机制如下:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//创建一个临时Node数组指向现有的Node数组
int oldCap = (oldTab == null) ? 0 : oldTab.length;//获得现有数组的长度;如果是默认初始化的,长度为0,否则为数组长度
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 {//初始阈值就是用的默认初始化0,数组也没有插入过元素
newCap = DEFAULT_INITIAL_CAPACITY;//新数组长度为默认的16长度
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新阈值 = 0.75 * 16
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {//新数组长度确定之后重新哈希移动元素
//省略中间步骤......
}
return newTab;
}
删除元素
public V remove(Object key) {//删除时查找元素要满足哈希值相等,并且equals()判等
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
遍历
我们在使用中都知道能使用HashMap
中的entrySet()
,keySet()
,values()
得到对应的(k,v)
,key
的set或者values
的Collection,然后利用Collection
中的Iterator()
遍历,就不赘述了。但是我还发现了一个问题就是以上的三个结构在put()
中都没有提及,更不用说维护了,那为什么可以调用这些方法得到相应的set呢?我于是翻看了这几个的源码:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;//这里涉及到一个类:EntrySet,我们往下看
}
//EntrySet类中获取iterator的方法如下
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
//继续看类EntryIterator,哇哈哈,终于找到了我们的而目标 HashIterator
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
//在这个类的构造方法中我们找到了答案
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;//这里就是获取了我们一直在更新的 Node数组
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
通过查看其他两种结构的源码都能发现他们都实现了这个HashIterator
抽象类,归到低就是通过entrySet()
,keySet()
,values()
这些结构遍历HashMap
就是获取了我们一直维护的Node[] table
而已,并没有额外的维护这些结构,从一个片面上看我梦也可以理解成这样:键值对(k,v
存到HashMap
中,key
“保存”在一个Set集合中,是不能重复的,而values
“保存”在Collection
当中,可以重复没有顺序。
还有好多想要了解的,以后多补充