Map简介
Java Map是Java编程语言中的一个重要接口,它是Java Collections Framework的一部分。Map提供了以键值对(key-value pairs)的形式存储和访问数据的功能。在Map中,每个key都是唯一的,而一个key只能对应一个value。Map允许使用任何类型的对象作为键或值,例如String、Integer或自定义的对象。
Java Map实现了一种映射关系,使得我们可以通过key来快速地获取到对应的value。因此,Map常被用来存储和检索关联数据集,比如字典、配置信息、电话簿等。
Map接口定义了一系列操作来存储、访问、删除键值对,并检查键或值是否存在于Map中。一些主要的方法包括:
- put(Key key, Value value):往Map中插入一个键值对。如果已经有相同key的键值对,则会更新对应的value。
- get(Object key):通过key检索对应的value。如果不存在,则返回null。
- remove(Object key):通过key移除对应的键值对。如果不存在,什么都不会发生。
- containsKey(Object key):检查是否存在给定key。
- containsValue(Object value):检查是否存在给定value。
- size():返回Map中键值对的个数。
- isEmpty():检查Map是否为空。
- clear():删除Map中所有的键值对。
Java提供了几种实现Map接口的类,主要有:
- HashMap:基于哈希表实现,允许空键和空值。提供了O(1)的基本操作(如put、get、remove等)。此外,它还是非同步的,不保证键值对的插入顺序。
- LinkedHashMap:继承自HashMap,但是它维护了插入顺序。也就是说,在迭代元素时,键值对会按照插入顺序遍历。
- TreeMap:基于红黑树实现,具有自然排序功能。键值对按照key的自然顺序或者自定义比较器的顺序排序。它的基本操作(如put、get、remove等)的时间复杂度是O(logn)。
- Hashtable:基于哈希表实现,与HashMap类似,但不允许空键和空值。是线程安全的,所有的方法都是同步的。由于同步机制带来的性能开销,HashMap通常比Hashtable更受欢迎。
HashMap继承关系
- HashMap 实现了Cloneable接口,可以被克隆。
/**
* 返回的Map是一个新实例,但是key,value都没有被拷贝,所以算浅拷贝
*/
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
- 实现了Serializable,对象可以序列化和反序列化
- 继承AbstractMap并实现了Map接口,具有Map接口所有功能
HashMap总要参数
// 默认初始化容量,容量为数组的大小,即table的数组大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量, 2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负荷系数,容量扩容的一个因子系数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树的阈值, 当链表长度大于等于8时就将链表存储格式转为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表的阈值,当红黑树节点数量小于等于阈值时,数据格式转为链表
// Q:为啥是6呢,不是7,或者其他呢? 留了一个中间值7可以避免,树和链的频繁转换
static final int UNTREEIFY_THRESHOLD = 6;
// 最小转树容量大小,意思就是不是链表长度>=8时就直接转树,还需要容器>=64才行
static final int MIN_TREEIFY_CAPACITY = 64;
// 数据数组
transient Node<K,V>[] table;
// 扩容阈值 threshold = DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY(如果设置了初始容量,
// 是设定的容量 执行tableSizeFor()的大小计算)
// 执行tableSizeFor: 返回给定目标容量的2次幂大小。
int threshold;
HashMap存储结构
HashMap的存储接口在jdk<=1.7的时候时采用数组+链表的形式存储,而在JDK=1.8的时候时采用的数组+链表+红黑树。
Q : 为啥在jdk1.8会加入红黑树的结构呢?
首先jdk<=1.7的时候hashMap采用的时数组+链表结合而成的高级数据结构,即 链表散列; 之所以要使用链表是为了解决哈希冲突指,哈希冲突指的是多个元素映射到同一个位置的情况。因为hashMap在put值的时候,会计算key的hash值然后使用**(n-1) & hash**
计算数组索引,当该索引有存储的值时就判断两个的hash值和key是都相等,相等的化就直接覆盖;不全部相等,就插入链表的尾部(jdk1.8尾插法,jdk<=1.7头插法)。
但是就会存在一个问题,当数据越来越多的时候,链表越来越长,链表的查找效率就会变得很低,所以在jdk1.8的时候就引入了红黑树的接口来提高查询效率。
HashMap主要操作分析
构造方法
HashMap的构造方法有4个,如图
HashMap(int initialCapacity, float loadFactor)
initialCapacity:初始化容量
loadFactor: 负荷系数
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) // 初始容量大于了最大容量,默认设置为最大容量
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 负荷系数小于0或null抛出异常
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
// tableSizeFor : 初始容量设置为2次幂的值, 比如 14(1110), 15(1111) 会转成16(10000)(2**4)
this.threshold = tableSizeFor(initialCapacity);
}
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
public static int numberOfLeadingZeros(int i) {
// HD, Count leading 0's
if (i <= 0)
return i == 0 ? 32 : 0;
int n = 31;
if (i >= 1 << 16) { n -= 16; i >>>= 16; }
if (i >= 1 << 8) { n -= 8; i >>>= 8; }
if (i >= 1 << 4) { n -= 4; i >>>= 4; }
if (i >= 1 << 2) { n -= 2; i >>>= 2; }
return n - (i >>> 1);
}
HashMap(int initialCapacity)
/**
* 使用默认负荷系数,调用HashMap(int initialCapacity, float loadFactor)构造方法
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
HashMap()
/**
* 只将默认负荷系数赋值给使用的负荷系数上
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
HashMap(Map<? extends K, ? extends V> m)
/**
* 构造一个与传入Map具有相同映射的新HashMap, 使用默认的负荷系数
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) { // 传入map值不为空
if (table == null) { // 存储table没有初始化,就初始化容器的扩容阈值
// 获取下一次扩容点的数量大小, +1 防止向下取值
float ft = ((float)s / loadFactor) + 1.0F;
// 判断下一次扩容值是否大于最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 此时threshold为0, 直接获取下次扩容的2次幂的值,赋值给扩容阈值
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold) // table初始化了的,且传入数量大于扩容阈值,进行扩容
resize();
// 循环复制,注意:这里是直接拿到传入map的key值进行复制,并没有进行深拷贝,
// 所以key, value 和原map的索引一样
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
通过上面几个构造方法我们可以观察到几点问题:
- 没有在构造方法中直接初始化
table容器
- 扩容阈值要么为0,要么大于等于初始容量; 并没有像说到 扩容阈值(threshold) = initialCapacity * loadFactor
Q: HashMap是何时初始化**table容器**
的呢?
table容器初始化时在首次Put操作中,判断容器没有初始化后执行了一次扩容(resize)操作才初始化的。
PUT操作
源码分析
/**
* 传入key, vlaue。 获取key之后求得key的hash值然后调用putVal方法
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 这里可以看到,hashMap是可以允许key为空的
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* onlyIfAbsent: true-不替换已存在的值 false-替换为新设置的值
* evict: 驱逐策略,在hashMap中使用不到
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.判断table数组容器是否初始化,或者长度为0, 满足以上条件就进行扩容
// 首次put就会进行初始化,所以直接创建hashMap的时候并不会初始化tavle容器
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.计算出key坐标,在table容器中获取节点数据,
// 2-1如果没有就直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 2-2.如果存在
else {
Node<K,V> e; K k;
// 2-2-1 如果当前节点hash值和key都和插入的一样,就记录一下当前节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 2-2-2 如果节点时红黑树类型,就在树中插入对应节点数据
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 2-2-3 尾插法插入数据到链表中
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);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 3. 节点数据不为空,根据onlyIfAbsent判断是否需要替换纯在的数据
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 4.modCount是用于记录HashMap结构变化(包括插入、删除等操作)的计数器。每当HashMap发生结构变化时,modCount就会自增1。
++modCount;
// 5.判断添加节点后的数据大小是否超过扩容阈值,进行扩容
if (++size > threshold)
resize();
// 6. 该方法会在LinkedHashMap中具体用到,这其实就是一个空模版
afterNodeInsertion(evict);
return null;
}
PUT流程图
resize 扩容
源码分析
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) {
// 当前容器大小大于0,且大于等于最大容器容量,值修改扩容阈值为Integer最大值,不进行扩容,直接返回当前容器
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 当前容器大小大于0,且扩大2被后小于最大容量,当前大小大于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0)
// 当前扩容阈值作为新容器大小
newCap = oldThr;
else {
// 当前扩容阈值为0,就是使用默认参数初始化新容器
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新容器阈值没有被赋值,那就根据新容器长度和负载系数进行计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 新容器长度和计算得到扩容阈值要小于最大容量才能使用计算的阈值。否则使用Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 设置hashmap新的扩容阈值
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 { // 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;
}
扩容流程图
主要说明一下链表重新计算索引排序的过程:
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;
}
- loHead 低位头部节点 loTail 低位尾部节点, 低位是指节点hash值与原容器进行与计算 == 0,那他扩容后的计算出的索引地址和原来保持一直
- hiHead 高位头部节点 hiTail 高位尾部节点 高位是指节点hash值与原容器进行与计算 > 0,那他扩容后的计算出的索引地址=原索引+原来容器长度
以下代码可以简单知道其原因:
System.out.printf("%d-%d%n", 33 & 16, 33 & 15);
System.out.printf("%d-%d%n", 33 & 32, 33 & 31);
System.out.printf("%d-%d%n", 33 & 64, 33 & 63);
System.out.printf("%d-%d%n", 33 & 128, 33 & 127);
0-1
32-1
0-33
0-33
以二进制来看
33 16 | 33 32 |
---|---|
100001 | 100001 |
10000 | 100000 |
0 | 100000 = 32 |
扩容后计算索引就是 | |
100001 = 33 | |
111111 = 63 | |
100001 = 33 |
链转树
源码分析
红黑树节点要求
- 性质1. 结点是红色或黑色。
- 性质2. 根结点是黑色。
- 性质3. 所有叶子都是黑色。(叶子是NIL结点)
- 性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
- 性质5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
/**
* tab 链表
hash 插入数据的hash值
*/
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();
// 找到当前索引位子链表的头节点
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 遍历链表把Node转为为TreeNode 链表,目前该循环并没有转成树结构,
// 只是把节点类型由Node转成了 TreeNode
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);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 遍历链表,构建树结构
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 第一个节点为根节点
if (root == null) {
x.parent = null;
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 根节点hash大于 当前节点,节点在根节点左侧
if ((ph = p.hash) > h)
dir = -1;
// 根节点hash大于 当前节点,节点在根节点右侧
else if (ph < h)
dir = 1;
// 如果hash相等,根据建值类型进行比较
// omparableClassFor() 方法是一个私有方法,用于获取键的 Comparable 类型
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 平衡红黑插入
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root);
}
GET操作
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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 索引的节点的hash值和key一样,直接返回
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);
// 遍历链表查找到 hash值和key一样的节点返回
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 没有初始化容器直接返回null
return null;
}
- 当前索引的节点的hash值和key一样,直接返回
- 节点类型为数,遍历红黑树
- 遍历链表查找到 hash值和key一样的节点返回
remove操作
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* matchValue 删除值匹配的
* movable 删除后是否移动其他节点
*/
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))))
// 当前hash 和 key一直 暂存当前节点
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;
}
Q:为啥在移除的流程没有看钱树转链的判断呢,那树转链的判断在啥时候进行的呢?
在remove操作中不会进行树转链的操作,只有在扩容的时候重新排序修改接口的时候,如果发现树的元素少于6个才会进行树转链表。