文章目录
HashMap
和
LinkedHashMap
都实现了
Map
接口,因此它们共有一些常用的方法。下面是一些常用方法及其作用:
HashMap
1. 常用方法
1.插入和更新元素
put(K key, V value)
将指定的键值对插入到Map
中,如果键已存在,则更新值。map.put("key1", "value1");
putIfAbsent(K key, V value)
如果键不存在,则插入键值对;如果键已存在,则不做任何操作。map.putIfAbsent("key2", "value2");
2.获取元素
get(Object key)
获取与指定键关联的值。如果键不存在,返回null
。String value = map.get("key1");
containsKey(Object key)
判断Map
中是否包含指定的键。boolean exists = map.containsKey("key1");
containsValue(Object value)
判断Map
中是否包含指定的值。boolean exists = map.containsValue("value1");
3.删除元素
remove(Object key)
删除与指定键关联的键值对,并返回该值。如果键不存在,则返回null
。map.remove("key1");
remove(Object key, Object value)
只有在指定的键和对应的值都存在时才删除该键值对。map.remove("key1", "value1");
4.获取大小
size()
返回Map
中键值对的数量。int size = map.size();
5.其他常用方法
clear()
清空Map
中所有的键值对。map.clear(); boolean isEmpty = map.isEmpty(); //判断是否为空 Set<K> keys = map.keySet(); // 获取所有键 Collection<V> values = map.values(); // 获取所有值 Set<Map.Entry<K, V>> entrySet = map.entrySet(); // 获取所有键值对 // 迭代 map.forEach((key, value) -> System.out.println(key + "=" + value));
2. 存储原理
1. 存储形式:数组,链表,红黑树
- 使用
Node<K,V>[]
数组保存数据 - JDK8以前新加入的数据如果和原有数据的哈希值相同但键值不同,会以链表的形式组织是数据,新添加的数据作为头节点,存入数组,
Node<K, V> next
指向原有数据 - JDK8以后如果容量大于64,链表长度大于8,这条链表会被转化为红黑树
transient Node<K,V>[] table;
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;
}
// ......
}
// 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K, V> parent; // red-black tree links
TreeNode<K, V> left;
TreeNode<K, V> right;
TreeNode<K, V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K, V> next) {
super(hash, key, val, next);
}
// ......
}
2.关键字段
DEFAULT_INITIAL_CAPACITY
,数组的默认长度16
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
DEFAULT_LOAD_FACTOR
,加载因子,默认为0.75
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
TREEIFY_THRESHOLD
,转化为红黑树的阈值,8
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
UNTREEIFY_THRESHOLD
重新转化为链表的阈值,树中的元素个数小于6时
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
MIN_TREEIFY_CAPACITY
,需要转化为树的最小容量
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
3. 添加过程
- 创建一个加载因子为0.75,table为null的哈希表
public static void main(String[] args) {
HashMap<Integer,String> map = new HashMap<>();
map.put(1,"aaa");
}
- 调用
put
方法,hash(key)
计算key
的哈希值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
putVal()
,参数一: 键的哈希值,参数二:键,参数三:值,参数四:键重复时,是否保留旧值
putVal()
函数的执行过程如下:
- 检查
table
是否初始化
// 1. 检查是否未初始化,如果是,创建一个长为16的数组
// 2. 如果不是检查是否需要扩容,如果需要,数组扩容为原来的两倍,并转移数据,n记录数组长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
- 检查要插入的位置,如果没有元素
// 检查当前插入位置
// p记录当前插入的位置,如果为null,创建一个Node对象,并存入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
- 如果
p
非空,则有三种可能
3.1 与p
的键相等
3.2// hash值相等,键重复,记录该位置 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
p
是一个TreeNode
节点,即红黑树的根节点
3.3// 如果为TreeNode else if (p instanceof TreeNode) // 添加入红黑树 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
p
是链表的头节点// 如果为Node for (int binCount = 0; ; ++binCount) { // 判断p节点的下一个节点是不是null if ((e = p.next) == null) { // 添加节点 p.next = newNode(hash, key, value, null); // 长度是否超过8 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 判断容量是否超过64,是则转化为红黑树 treeifyBin(tab, hash); break; } // e不是null // 判断是否键重复 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 重复则终止循环 break; // 当前节点下移 p = e;
- 此时,
e
记录了有重复键的节点 - 如果
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;
LinkedHashMap
LinkedHashMap
继承自 HashMap
,额外实现一个双向链表保存顺序
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
1. LinkedHashMap
的额外方法
LinkedHashMap
除了继承自 HashMap
的方法外,还提供了与顺序相关的额外功能。
-
访问顺序相关构造函数
通过构造函数设置是否按访问顺序来迭代元素:LinkedHashMap<K, V> map = new LinkedHashMap<>(16, 0.75f, true); // 访问顺序
其中第三个参数
true
表示按访问顺序排列,false
则是按插入顺序排列。 -
removeEldestEntry(Map.Entry<K, V> eldest)
该方法用于指定在LinkedHashMap
超过指定的大小时自动删除最老的元素,通常用于实现缓存。LinkedHashMap<K, V> map = new LinkedHashMap<>(16, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > 100; // 如果超过 100 个元素则删除最旧的 } };
2. 底层原理
存储方式和HashMap
基本一致,只是在添加节点时,额外通过双向链表记录顺序