一、HashMap相关知识简单介绍
在HashMap的底层采用的数据存储结构为哈希表,通过hash算法值去确定数据存放的位置,并且还会存在哈希冲突(也叫左hash碰撞等问题);那么在学习HashMap之前,我们需要对HashMap存储结构的数据结构做一个简单的的相关理论知识做个了解。
1、什么式哈希表(hash table)?
哈希表(hash table 也叫散列表)是一种根据关键码值(key value)进行数据访问的数据结构,通过关键码值映射(函数)到连续数据存储区的某个位置来进行快速定位查找、存储数据,这种映射定位函数称为hash函数(也叫散列函数),而通过这种形式来存储或查询数据的连续存储数据区就叫做哈希表(hash table)。
2、什么是hash函数?
简单的说就是根据数字计算,返回在某个特定区域内值的关系映射函数 特定范围值=f(关键字码)
3、什么是hash冲突?
hash冲突指在进行数据插入时候,出现在通过hash函数计算出来的数据存储位置已经存在数据的情况。那么两个数据存放在同一个位置是不可行的,这就是hash冲突(例如:放中药的时候本来打算放在某个格子内,可是这个格子已经放了其他中药把这个格子占了,我们就不能放在这个格子了,这两种只能存放一种,不能一起放)
4、解决hash冲突的方式主要有哪些?
A:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址)
B::再散列函数法(发生冲突,再次使用hash函数再次计算)
C:链地址法(以链表的形式存储 是HashMap的底层实现(数组+单向链表))
5、链表的种类
单向链表、单向循环链表、双端链表、双端循环链表
6、存储结构图:
二、HashMap源码分析之静态内部类Entry
1、 HashMap的内部存储结构是数组和单向链表的结合。当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。 每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。 Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。 Entry是HashMap中的一个静态内部类。代码如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
2、Entry<K,V>静态内部类重要方法
//获取key值
public final K getKey() {
return key;
}
//获取value值
public final V getValue() {
return value;
}
//设置value值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//判断两个结点对象是否相等
public final boolean equals(Object o) {
//判断类型
if (!(o instanceof Map.Entry))
return false;
//类型相同,将Object对象结点装换为Entry对象
Map.Entry e = (Map.Entry)o;
//获取自身对象key和比较对象key
Object k1 = getKey();
Object k2 = e.getKey();
//equals为Object对象的判断方法 是通过== 比较两个对象的,比较的是地址值,如果对象重写了Object对象的 // Object类的equals方法,则按照重写的比较方式算
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
//key相等 ,在以相同的方式比较value值
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
//key的hashCode值^value值的hashCode值
public final int hashCode() {
//Object对象的hashCode函数 底层不知道怎么实现,没有提供源码
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
//重写Object类对象的toString方法
public final String toString() {
return getKey() + “=” + getValue();
}
三、HashMap源码分析
1、重要属性
//默认初始化化容量,即16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,即2的30次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认负载因子值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};
//空的存储实体
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
//实际存储的key-value键值对的个数
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold
int threshold;
//负载因子,代表了table的填充度有多少,默认是0.75
final float loadFactor;
//用于快速失败,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException
transient int modCount;
//默认的threshold值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//计算Hash值时的用到
transient int hashSeed = 0;
2、构造方法
//通过初始容量和加载因子构造HashMap
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;
threshold = initialCapacity;
init();
}
//通过容量构造HashMap,加载因子取默认值,即0.75f
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//通过默认容量和默认加载因子构造HashMap 16和0.75f
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//
通过其他Map来初始化HashMap,容量通过其他Map的size来计算,装载因子取0.75
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
3、V put(k key,V value) 添加元素
public V put(K key, V value) {
//如果table数组为空数组{},进行数组填充(为table分配实际内存空间)
//入参为threshold,此时threshold为initialCapacity 默认是1<<4(=16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);//分配数组空间 16
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
//通过key值调用hash函数得到hashcode值
int hash = hash(key);
//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);
//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);//调用value的回调函数,其实这个函数也为空实现
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);
//新增一个entry
return null;
}
2、HashMap中的hash算法(注意jdk1.7和1.8有区别)
//用了很多的异或,移位等运算, 这里我们可以不用深究原理
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
3、确定存储位置index
static int indexFor(int h, int length) {
// key值的hash函数值和数组长度-1做与运算
return h & (length-1);
}
4、集合元素个数(Entry个数)
public int size() {
return size;
}
5、获取集合元素数据
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
6、当元素的key值为null时候获取方式
private V getForNullKey() {
if (size == 0) {
return null;
}
//因为hash算法实现null存在0的位置
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}