jdk1.8
文章目录
Map接口特点及常用方法
结构体系
常用实现类
特点
-
Map和Collection并列存在,用于保存具有映射关系的数据 Key-Value
键值对:数据成对出现,且唯一对应,可以理解为钥匙和内容,通过钥匙能找到里面保存的内容,但通过内容找不到钥匙,里面的内容可以重复,但钥匙不能重复
-
Map中数据的Key和Value可以保存任何引用数据类型,数据会封装成 HashMap&Node 对象
-
Key不允许重复(equals判断重复,key重复会替换旧的value)
-
value可以重复
-
key和value对否可以保存null视具体实现类
Map接口常用方法
//所有实现子类均可用
void clear()
从该Map中删除所有的映射(可选操作)。
boolean containsKey(Object key)
如果此映射包含指定键的映射,则返回 true 。
boolean containsValue(Object value)
如果此Map将一个或多个键映射到指定的值,则返回 true 。
Set<Map.Entry<K,V>> entrySet()
返回此Map中包含的映射的Set集合。(源码讲)
V get(Object key)
返回到指定键所映射的值,或 null如果此映射包含该键的映射。
Set<K> keySet()
返回此Map中包含的键的Set集合。
Collection<V> values()
返回此Map中包含的值的Collection集合
V put(K key, V value)
将指定的值与该映射中的指定键相关联(返回空)。
void putAll(Map<? extends K,? extends V> m)
将指定地图的所有映射复制到此映射(可选操作)。
V remove(Object key)
如果存在(从可选的操作),从该Map中删除一个键的映射。
default boolean remove(Object key, Object value)
仅当指定的Key当前映射到指定的值时删除该条目。
int size()
返回此Map中键值映射的数量。
boolean isEmpty()
如果此map不包含键值映射,则返回 true 。
常见实现子类
HashMap
是最常用也是最重要的Map实现子类
特点
- Key不允许重复,可以为null(key重复会替换旧的value)
- value可以重复,可以为null
- 没有实现同步,线程不安全
讲到hash表就要将hash表的两大问题,第一个是使数据分布均匀节省空间,另一个是Hash碰撞问题
什么是数据分布均匀?
就是hash表是通过索引添加查找数据的,但是hash表的索引不是数组按顺序存放数据,而是同过某种计算方式计算出下标位置这就导致有可能数据都聚集在某一处,导致空间的浪费。
什么是冲突处理问题?
正如上面所说,计算出位置,如果计算的位置相同怎么处理?常采用拉链法
这里的索引表就是table数组,在源码里讲
底层实现
Hashmap底层是数组加链表加红黑树,说到树有两个概念,树化和减枝
树化:链表转化为红黑树
减枝:红黑树退化为链表
为什么有这两个概念呢?效率优化,因为索引相同的数据串成一条单链表,当数据很多时单链表按顺序查找效率低,所以就引入红黑树(平衡二叉树的一种,查找效率高)
那么树化和减枝的条件是什么?
先说结论,后面分析源码
当一条单链表上的数据超过8时,就会判断table表的长度,如果长度小于64,就会对table表扩容,每次在原来基础乘2,如果达到了64,就会把这条单链表树化,减枝就是当一棵树的节点减少到6时,就会退化为单链表
源码解读
基于哈希表的Map接口实现。这个实现提供了所有可选的map操作,并允许空值和空键。
HashMap类大致相当于Hashtable,除了它不同步且允许空值。
这个类不保证映射的顺序;特别是,它不能保证顺序在一段时间内保持不变。(table数组扩容时会重新计算索引位置)
这个实现为基本操作(get和put)提供了常量时间性能,前提是哈希函数将元素正确地分散在索引表中。
迭代集合Map所需的时间与HashMap实例的索引表的长度和(键-值映射的数量)成正比。
因此,如果迭代性能很重要,那么不要将初始容量设置得过高(或加载因子设置得过低,默认0.75),这一点非常重要。HashMap实例有两个影响其性能的参数:初始容量和加载因子。
容量是索引表的长度,初始容量只是创建哈希表时的容量。
加载因子是衡量哈希表在自动增加容量之前允许其达到多满的指标(例如现在table长度32,现在有23个索引位置有数据,现在在添加一个数据,且这个数据刚好在一个新的索引位置,那么现在就有24个位置有数据了,24 == 32 * 0.75 当前就会扩容)。
先看源码常量部分
//默认的初始化容量为16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
//最大容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子0.75
//面试题:为什么设置 0.75 这个值呢?
//简单来说就是时间和空间的权衡。
//若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,
//若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树,树化
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的元素个数,减少到6个时,就退化为链表,减枝
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
//这是为了避免,数组扩容和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
//存放所有Node节点的数组,table表,索引表
transient Node<K,V>[] table;
//存放所有的键值对(后面详细讲)
transient Set<Map.Entry<K,V>> entrySet;
//map中的实际键值对个数,即数组中元素个数
transient int size;
//每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
//当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
transient int modCount;
//数组扩容阈值
int threshold;
//加载因子
final float loadFactor;
//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
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;
//左孩子节点
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);
}
}
再看构造函数
//默认无参构造,指定一个默认的加载因子(0.75)
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)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16
//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
//先卖个关子,等到 resize 的时候再说
this.threshold = tableSizeFor(initialCapacity);
}
//可传入一个已有的map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//把传入的map里边的元素都加载到当前map
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m