核心概念理解
散列表(Hash Table 又名哈希表/Hash表):
- 是根据键(key)直接访问在内存存储位置值(value)的数据结构,它是由数组演化而来, 利用了数组支持按照下标进行快速随机访问数据的特性
散列函数:
- 将键(key)映射为数组下标的函数.可表示为hashValue = hash(key)
散列函数的基本要求:
- 散列函数计算得到的散列值必须是大于等于0的正整数,因为hashValue需要作为数组的下标;
- 如果key1 == key2,那么经过hash后得到的哈希值也必须相同,即 hash(key1)== hash(key2);
- 如果key1 != key2,那么经过hash后得到的哈希值也必不相同,即 hash(key1) != hash(key2);
散列冲突(哈希冲突,哈希碰撞):
- 指多个key映射到同一个数组下标位置
散列冲突解决方法:
- 链地址法(拉链法,链表法)
分析源码的方式(如何分析源码)?
可从以下四个方面进行分析(基于JDK11分析):
- 类继承的父类及实现的接口
- 重要的成员变量
- 构造函数
- 关键方法
HashMap源码分析
- 类继承的父类及实现的接口
/**
1.继承自AbstractMap
2.实现了Map, Cloneable, Serializable接口
特别说明(以下知识需要另外学习扩展):
Cloneable: 标记接口, 表明具有Clone的特性;
Serializable: 标记接口, 表明具有序列化及反序列化的特性;
*/
HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
- 重要的成员变量
//默认初始容量-必须是二次方幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量,如果隐式指定了更高的值则使用由具有参数的构造函数中的任意一个执行。
//必须是二次方幂 <= 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;
//构造函数中未指定时使用的负载系数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//链表转换为红黑树的阈值。
static final int TREEIFY_THRESHOLD = 8;
//红黑树退化为链表的阈值。
static final int UNTREEIFY_THRESHOLD = 6;
//可以将存储箱树化的最小表容量。(否则,如果一个bin中的节点太多,则会调整表的大小。)应至少为4*TREEIFY_THRESHOLD,
//以避免调整大小阈值和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
- 构造函数
//构造一个具有默认初始容量(16)和默认负载因子(0.75)的空HashMap。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//构造一个具有指定初始容量和默认负载因子(0.75)的空HashMap。
//参数:initialCapacity–初始容量。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用指定的初始容量和负载因子构造一个空的HashMap。
//参数:initialCapacity–初始容量
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);
}
//使用与指定Map相同的映射构造新的HashMap。创建的HashMap具有默认的加载因子(0.75)和足够在指定Map中保存映射的初始容量。
//参数:m–映射要放置在此映射中的映射
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
- 哈希表中,数组每个下标对应的位置称为桶(bucket)or槽(slot),每个桶回对应一条链表,所有哈希值相同的元素都到相同桶位对应的链表中;
- 插入操作, 通过散列函数计算出对应的散列桶位,将其插入对应的链表中即可,插入的时间复杂度是O(1);
- 查找or删除,通过散列函数计算出对应的散列桶位,然后遍历链表查找or删除;
- 平均情况下基于链表法解决冲突时查询的时间复杂度为O(1);
- 哈希表可能会退化为链表,查询的时间复杂度从O(1)退化为O(n);
- 将链表法中的链表改造为其他高效的动态数据结构, 如红黑树, 查询时间复杂度为O(logN)
面试相关 1.说一下HashMap的实现原理? *****
- 1.底层使用hash表数据结构,即数组和链表or红黑树;
- 2.添加数据时,计算key的值确定元素在数组中的下标;
- a.key相同则替换;
- b.key不同存入链表or红黑树中;
- 3.获取数据时,通过key的hash计算出数组下标获取元素;
- 4.jdk1.8之前采用数组+链表实现;
- 5.jdk1.8之后采用数组+链表+红黑树实现, 链表长度大于8且数组长度大于64时,会从链表转化为红黑树;
2.HashMap的put方法的具体流程? *****
源码分析:
HashMap<String,String> hashMap = new HashMap<>();
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- HashMap是懒加载,在创建对象时并没有初始化数组;
- 在无参构造函数中, 设置了默认的加载因子是0.75;
- 1.判断键值对数组table是否为空or为null,否则执行resize()进行扩容(初始化);
- 2.根据键值key计算hash值得到数组索引;
- 3.判断table[i] == null,条件成立,直接新建节点添加;
- 4.判断table[i] == null,不成立:
- a.判断table[i]的首个元素是否和key一样,若相同直接覆盖value;
- b.判断table[i]是否为红黑树,如果是则在树中插入键值对;
- c.遍历table[i],链表尾部插入数据,然后判断链表长度是否大于8, 大于8这把链表转为红黑树,在红黑树中执行插入操作,遍历中若发现key存在则直接覆盖;
- 5.插入成功后,判断实际存在的键值对数量size是否超过最大容量thresshod(数组长度*0.75),如果超过,则进行扩容;