Map API
Map 是集合中一个重要接口,是双列集合的根接口
其内部存储结构为 key-value 键值对
- 无重复键值对
- key 可为 null(限一个)
- value 可为 null(多个)
方法 | 说明 |
---|---|
void clear () | 清除所有键值对 |
boolean containsKey (Object key) | 如果此映射包含指定键的映射,则返回 true |
boolean containsValue (Object value) | |
Set<Map, Entry<K,V>> entrySet () | |
boolean equals (Object o) | |
default void forEach (BiConsumer action) | |
V get (Object key) | |
int hashCode () | 返回此集合的哈希码值 |
boolean isEmpty () | |
Set<K> keySet () | 返回所有 key 的 set |
V put (K key, V value) | 添加键值对 |
void putAll (Map<? extends K,? extends V> m) | |
V remove (Object key) | 删除键值对 |
default boolean remove (Object key, Object value) | 仅当指定的key映射到指定的值时删除 |
default V replace (K key, V value) | 只有当目标映射到某个值时,才替换 |
boolean replace (K key, V oldValue, V newValue) | |
int size () | |
Collection<V> values () | 返回此 Map 的 Collection |
HashMap API
HashMap 是 Map 的一个实现类,其没有实现线程同步,是非线程安全的集合
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
Integer integer = map.get(1);
Integer remove = map.remove(1);
boolean key = map.containsKey(1);
boolean value = map.containsValue(1);
boolean empty = map.isEmpty();
// 获取所有 key
Set<Integer> integers = map.keySet();
// 获取所有 value
Collection<Integer> values = map.values();
// 获取所有键值对
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
for (Map.Entry<Integer, Integer> entry : entries) {
Integer k = entry.getKey();
Integer v = entry.getValue();
}
map.clear();
}
底层分析
在 JDK 1.8 中,HashMap 底层采用 数组 + 链表 + 红黑树 来实现,JDK 1.7 之前的底层为 数组 + 链表
HashMap 内部维护了一个链表,用来存放键值对
该节点内部类继承了 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;
}
}
用一个 table 链表数组来存放数据
transient Node<K,V>[] table;
为了方便遍历,HashMap 底层维护了一个 EntrySet 集合,该集合存放的元素类型是 Entry
该集合中的 key 指向了 Node 结点中的 key,entrySet 定义的类型是 Map.Entry,实际类型是 HashMap$Node
transient Set<Map.Entry<K,V>> entrySet;
创建
Map<Integer, Integer> map = new HashMap<>();
当使用无参构造器创建 HashMap 时,会对加载因子进行初始化为 0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
创建完成后,此时的 table 是空的(HashMap$Node [] table = null)
在创建的时候,还可以自定义容器的大小,自定义加载因子等
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)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
在传入参数后,底层会对其进行校验,如果校验失败则会抛出异常
情景:传入的加载因子 > 1
当传入的加载因子大小超过1的时候,系统不会抛出异常,因为这是合理的。但在容器想要扩容的时候,扩容的阈值是通过容器现有容量与加载因子的乘积计算得出的,当加载因子超过1的时候,扩容阈值会大于当前的容器容量值,会导致容器一直无法扩容,添加超过容器大小的容器后,会一直添加在节点链表的后面,即 容器的大小定死了
Put
HashMap 底层的存储数据结构为 Node<K,V>[] table,这是一个 哈希表,初始默认为 null
当第一次调用 put 方法进行添加键值对的时候,会先对基本数据类型进行自动装箱,之后进入 put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 先进入 hash 方法计算哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 计算出 hash 值后,把 hash 值,key、value 传入核心方法 putVal
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;
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;
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);
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;
}
- 当第一次添加键值对时,会对集合进行扩容,初始扩容大小为16,临界值为 12(16 * 0.75)
- 以后每次进行扩容时,扩容倍数为2,临界值为容器扩容后的大小 * 加载因子
- 在添加键值对的时候,先根据计算处的 hash 值查看 hash 对应的索引位置是否有元素存在,如果有元素存在,则对该位置的链表进行树的判断;如果其是一颗红黑树,则根据树的方式进行查找
- 如果不是树,则对链表的每个元素都进行遍历查看,如果节点的整条链表中没有存在 put 的键值对,则进行 newNode 并加入链表
- 加入链表后进行树化判断,即当链表的长度超过8,且集合的大小超过64时,会对该链表进行红黑树化
- 如果链表的长度超过了8,但集合大小没超过64,则对集合进行扩容,直到64为止才可以进行树化
初始容量大小
HashMap 的第一次扩容时,初始大小为 16,这个大小的 2的整数次幂
16 = 2^4
16、32、64 …
当使用有参构造创建 HashMap,传入指定容器大小时,会对容器大小进行扩容,返回一个 2 的整数幂的数值
如 输入一个 13,返回一个 16
把容器的大小设置为 2 的整数幂的目的:是为了让哈希表中的数据能够尽可能地均匀分布,并尽可能地减少哈希冲突
遍历
Map 集合不能直接使用迭代器或者 foreach 进行遍历,但是转成 Set 之后就可以使用了
方式一:通过 keySet 函数获取所有键,再使用 for 循环遍历
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
Set<Integer> keySet = map.keySet();
for (Integer key : keySet) {
System.out.println(map.get(key));
}
// 通过迭代器
Iterator<Integer> iterator = keySet.iterator();
while (iterator.hasNext()) {
Integer next = iterator.next();
System.out.println(map.get(next));
}
}
方式二:通过 entrySet 函数获取所有键值对,再使用 for 循环遍历
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>();
map.put(1, 1);
map.put(2, 2);
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
for (Map.Entry<Integer, Integer> entry : entries) {
Integer key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key);
System.out.println(value);
}
// 迭代器
Iterator<Map.Entry<Integer, Integer>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, Integer> next = iterator.next();
Integer key = next.getKey();
Integer value = next.getValue();
System.out.println("key = " + key + ",value = " + value);
}
}
HashTable
特点:
- 键值都不能为 null
- 线程安全
底层有数组 HashTable$Entry[],初始化大小为 11
临界值 8 = 11 * 0.75*
扩容:11 * 2 + 1 = 23
private transient Entry<?,?>[] table;
HashMap 与 HashTable 的区别
- HashTable 继承自 Dictionary,HashMap 继承自 AbstractMap,二者都实现了 Map 接口
- HashTable 不允许 null key 和 null value,HashMap 允许
- HashMap 的使用几乎与 HashTable 相同,不过 HashTable 是 synchronized 同步的;HashMap 是线程不安全