1) Map用于保存具有映射关系的数据:Key-Value
2) Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
3) Map 中的 key 不允许重复4) Map 中的 value 可以重复
5) Map 的key 可以为 null, value 也可以为null ,注意 key 为null,只能有一个value 为null ,可以多个.
6) 常用String类作为Map的 key
底层实现:
1.HashSet 底层是 HashMap
2.添加一个元素时,先得到hash值转成索引值
3.找到存储数据表table,看这个索引位置是否已经存放的有元素如果没有,直接加入如果有,调用 equals 比较,如果相同,就放弃添加,如果不相同,则添加到最后6在Java8中如果一条链表的元素个数超过 TREEIFY THRESHOLD(默认是8).并且table的大小 >=MIN TREEIFY CAPACITY(默认64)就会进行树化(红黑树)
HashMap一些字段信息:
// table的默认长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 默认最大table长度
static final int MAXIMUM_CAPACITY = 1 << 30; // 1073741824
// 加载因子,75%
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// table中一个元素链表达到某个长度转化为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转化回链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中的Node被树化时最小的hash表容量(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN TREEIFYCAPACITY时,此时应执行resize扩容操作这个MIN TREEIFYCAPACITY的值至少是TREEIFY THRESHOLD的4倍。)
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储node的表,hash桶
transient Node<K,V>[] table;
// 记录所有kv数据,遍历使用
transient Set<Map.Entry<K,V>> entrySet;
// HashMap中存储的键值对的数量
transient int size;
// HashMap扩容和结构改变的次数
transient int modCount;
// 扩容的临界值 = 容量 * 填充因子
int threshold;
// 扩容百分比(填充因子)
final float loadFactor;
首先,每一个键值对都是由Node来实现的,如果链表长度达到8,是由TreeNode来实现节点的
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ...省略
}
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;
}
}
// ...省略
}
HashMap的四个构造方法
在jdk1.8中,调用这前三种构造器之后,并没有立刻创建一片空间,而是在首次调用put()方法时,才创建空间。最后一个初构造器是直接传了一个Map作为参数进行初始化,并将内容读取做hash放到table中
调用put()方法插入键值对
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
先判断表是否为空或长度为0,如果是,就扩容
通过(n-1)&hash来获得要插入的位置 //n是tab.length
如果为空,直接插入,否则就会根据节点类型来进行不同的处理
else {
Node<K,V> e; K k;
// 当前第一个节点和要插入的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) {
// 到了尾巴还是没有要找的key,就往这个节点后面创建并插入这个节点
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;
}
}
// 如果有和要插入的key的值相同的,就覆盖
if (e != null) {
V oldValue = e.value;
// 根据参数查看是否要修改存在的值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 看用户实现,空
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
put()大致内容为:
- 确定是否已经初始化table,没有就先初始化一下。
- 定位插槽后确定是否有节点存在,没有就创建一个节点插入。
- 如果有一个节点并且就是我要找的那一个,就将值进行覆盖,如果不是我要找的就根据这个节点判断是树结构还是链表结构,根据数据结构不同进行遍历查找。
- 如果都查不到就新建一个节点插入,如果找到就覆盖,在链表插入后需要判断当前是否到达树化的阈值,判断到达阈值进行树化。
扩容机制 resize()
- 如果当前table没有内容说明是初始化,那确定新table它的容量与扩容阈值后直接返回。
- 如果table有内容,需要先判断是否还能继续扩容(达到最大容量就会直接返回)。没有达到最大容量就直接增加一倍,然后根据旧table的容量是否大于等于16来确定新table的扩容阈值。
- 如果旧table的容量大于等于16,那么新table的扩容阈值就是旧table容量的两倍,否则就是新table容量*0.75。
- 到了这里就需要分三种情况来处理旧table的节点转移了。
- ** 只有一个节点就直接定位新数组位置转移即可。
- ** 如果是树节点就确定是否可以取消树化,转换链表转移
- ** 最后一种就是链表结构了,那就确定每个节点是否要转移,然后进行转移处理即可。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 等于0说明刚进来初始化,不等于零就是某个put方法调用的逻辑
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// table有内容,非初始化
if (oldCap > 0) {
// 最大容量返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 将新table的容量扩容一倍,然后判断不是最大值并且旧table容量大于等于16,
// 就将新数组扩容阈值扩大一倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 初始化传入threshold就会大于0
else if (oldThr > 0)
newCap = oldThr;
// 初始化无参,就会给默认值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 进入这个方法说明上面经过 if (oldCap > 0)处理但没有进入else if中,
// newCap > MAXIMUM_CAPACITY 或者 oldCap < DEFAULT_INITIAL_CAPACITY
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 对扩容阈值与扩容大小都处理好后开始处理节点重定位
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 初始化就直接返回了(初始化oldTable是空的),这里需要重定位
if (oldTab != null) {
// 遍历所有table插槽
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
// 清空这个节点使gc能够及时回收,这个节点已经赋值给变量e
oldTab[j] = null;
// 这个插槽只有只有一个节点,重定位放入新table
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 这个节点已经转换为树结构了,
else if (e instanceof TreeNode)
// 红黑树拆分,拆分后的高低位树过小(节点小于等于6个),则取消树化,将其转为链表结构。
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 链表结构,拆分成新table需要重定位与不需要的
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;
}
Map常见方法
添加、删除、修改操作:
object put(object key,object value): 将指定key-value添加到(或修改)当前map对象中
void putALL(Map m):m中的所有key-value对存放到当前map中
Object remove(Object key): 移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(object key):是否包合指定的key
boolean containsValue(Object valueT是否包合指定的vaLue
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
ColLection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
LinkedHashMap
1. LinkedHashMap 加入顺序和取出元素/数据的顺序一致
2. LinkedHashMap 底层结构 (数组table+双向链表)
3. 添加第一次时,直接将 数组table 扩容到 16 ,存放的结点类型是 LinkedHashMap$Entry
4. 数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry类型
// 头节点
transient LinkedHashMap.Entry<K,V> head;
// 尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序
// true可以按最近访问顺序遍历,最近访问的优先读
final boolean accessOrder;
// 添加LinkedHashMap节点独有的定义(双向指针: before,after)
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}