目录
HashMap简介
HashMap是我们最常用到的集合之一,是java非常典型的数据结构。学习它的源码是非常只有必要的,我们所要了解的并不仅仅是“HashMap不是线程安全的,HashTable是线程安全的,通过synchronized实现的。HashMap取值非常快”等等。
了解hashmap必须要先对hashmap的存储结构有个了解
它是属于数组及链表相结合的存储结构。如上图 x轴为数组,y轴为链表。
数组存储方式在内存地址是连续大小固定,一旦分配无法被其他引用占用,查询迅速,时间复杂度O(1),插入删除比较慢,时间复杂度为O(n)。
而链表存储方式则与数组相反,属于非连续性,大小非固定,插入及删除块,查询速度慢。
所以HashMap相对中庸。
HashMap的一些常见问题
HashMap的数据结构是啥?数据结构上存储的数据对象结构是啥?
HashMap是一个存储数据对象<封装了K,V属性的对象>的集合,而这个集合是数组+链表类型的数据结构。
根据源码来分析hashMap内部的精髓 hash算法如何保证散列均匀冲突的解决方式?
谈到hash
通常我们jdk的equals在比较的时候就会使用hash算法,此算法会定位到对象的存储位置
具体hash的原理是:
hash函数:找到存储过程
被重写的hashCode(key)
index=h=Hash(int hashCode)
(key.hashCode)&&length -1
length 2^n
通过h就可以找到数组下标的位置
例子如下:
2^4=16
length-1 =15 二进制为 01111
h返回的是 10101
数组上存储的位置为: 00101 【上下都是1才是1】
好处:
1 )散列的范围被低位限制—》散列位置一定在我们的索引范围(即length-1)之内。
2 )低位的0如果越多 代表我们散列的结果越固定。【想象一个若是非length-1就会发生
10000 低位0较多,导致散列结果几乎就是一致】,导致冲突越多,导致数组位置的利用率不高。
HashMap并发闭环问题?
扩容方法会新建一个数组,复制原数组到新的数组,由于下标挂着链表,扩容之后会导致环形链表的出现,JDK1.8已经解决了这个问题了。
手撕HashMap
首先定义最基础的map接口
package com.hikvision.rabbitmq.map;
/**
* @ClassName Map
* @Description TODO
* @Autuor lulinfeng
* @Date 2020/8/18
* @Version 1.0
*/
public interface Map<K, V> {
V get(K k);
V put(K k, V v);
}
然后定义散列实体
package com.hikvision.rabbitmq.map;
/**
* @ClassName Entry
* @Description TODO
* @Autuor lulinfeng
* @Date 2020/8/18
* @Version 1.0
*/
public class Entry<K, V> {
K k;
V v;
Entry<K, V> next;
public Entry(K k, V v, Entry<K, V> next) {
this.k = k;
this.v = v;
this.next = next;
}
public K getK() {
return k;
}
public void setK(K k) {
this.k = k;
}
public V getV() {
return v;
}
public void setV(V v) {
this.v = v;
}
}
然后开始书写我们自定义的HashMap
首先我们定义最基础的四个元素
// 定义数组大小 16
// 结合着下面的扩容因子来解释一波:假如数组用了 4 usesize/defaulLenth =4/16=0.25 即使用率<0.75,不会扩容
private static int defaulLenth = 1 << 4;
// 扩容标准 所使用的useSize / 数组长度 >0.75
// defaulAddSizeFactor 过大 造成扩容概率变低 存储小 但是就是存与取的效率降低
// 0.9 有限的数组长度空间位置内会形成链表 在存与取值中都必须进行大量的遍历和判断(逻辑)
// 过小 内存使用比较多,使用率不高,造成浪费
private static double defaulAddSizeFactor = 0.75;
// 使用数组位置的总数
private int useSize;
// 定义Map 骨架 只要 数组之一 数组
private Entry<K, V>[] table = null;
然后用门面模式,这样可以传参来控制散列大小和扩容因子
// Spring 门面模式运用
public HashMap() {
this(defaulLenth, defaulAddSizeFactor);
}
public HashMap(int length, double AddSizeFactor) {
if (length < 0) {
throw new IllegalArgumentException("参数不能为负数" + length);
}
if (defaulAddSizeFactor <= 0 || Double.isNaN(defaulAddSizeFactor)) {
throw new IllegalArgumentException("扩容标准必须是大于0的数字" + defaulAddSizeFactor);
}
defaulLenth = length;
defaulAddSizeFactor = AddSizeFactor;
table = new Entry[defaulLenth];
}
PUT方法
然后我们开始书写put方法
put的最简单的逻辑即:
1)判断是否需要扩容
2)判断是数组对应index是否存在链表,若无塞值,若有挂载链表上
@Override
public V put(K k, V v) {
// 存储是判断是否需要扩容
if (useSize > defaulAddSizeFactor * defaulLenth) {
up2Size();
}
// 获取数组下标
int index = getIndex(k, table.length);
Entry<K, V> entry = table[index];
// 判断这个entry是否为空,为空意味着未被散列到
if (entry == null) {
table[index] = new Entry(k, v, null);
useSize++;
} else if (entry != null) {
// 形成了链表结构
table[index] = new Entry(k, v, entry);
}
return table[index].getV();
}
获取数组下标【即为啥必须要是2的n次方】
这边我们首先看下获取数组下标的方法
/**
* 寻找数组的下标
**/
private int getIndex(K k, int length) {
int m = length - 1;
int index = hash(k.hashCode()) & m;
return index;
}
采取数组大小-1 01111111 & k的hash 这样比较稳定
看下自定义hash算法
/**
* 自定义hash算法
**/
private int hash(int hashCode) {
hashCode = hashCode ^ (hashCode >>> 20) ^ (hashCode >>> 12);
return hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4);
}
扩容方法
说到扩容,无非就是两点:
1)将新建一个2倍大小的数组
2)将旧的散列赋值到新的散列中
/**
* 扩容
**/
private void up2Size() {
// 如何扩容,无非就是新建一个2倍空间的数组
Entry<K, V>[] newTable = new Entry[2 * defaulLenth];
// 老数组的内容拿到新数组中
againHash(newTable);
}
具体复制
/**
* 复制旧散列到新散列
**/
private void againHash(Entry<K,V>[] newTable) {
List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>();
// for循环 即老数组内容被全部遍历到了entryList中
for (int i = 0; i < table.length; i++) {
if (table[i] == null) {
continue;
}
// 继续找存到数组上的entry对象
foundEntryByNext(table[i], entryList);
}
// 设置entryList
if (entryList.size() > 0) {
useSize = 0;
defaulLenth = 2 * defaulLenth;
for (Entry<K, V> entry : entryList) {
if (entry.next != null) {
entry.next = null;
}
put(entry.getK(), entry.getV());
}
}
}
这边要注意数组上还可能挂着链表
/**
* 查询链表
**/
private void foundEntryByNext(Entry<K,V> entry, List<Entry<K,V>> entryList) {
// 形成了链表结构
if (entry != null && entry.next != null) {
entryList.add(entry);
// 递归,不断地一层层取存entry
foundEntryByNext(entry.next, entryList);
} else {
// 没有链表的情况
entryList.add(entry);
}
}
GET方法
get相对简单
public V get(K k) {
int index = getIndex(k, table.length);
if (table[index] == null) {
throw new NullPointerException();
}
return findByValueByEqualKey(k, table[index]);
}
采取拉链法来获取数据即k相等,且equals也相等
/**
* 拉链法查询
**/
private V findByValueByEqualKey(K k, Entry<K,V> entry) {
if (k == entry.getK() || k.equals(entry.getK())) {
return entry.getV();
} else if (entry.next != null) {
return findByValueByEqualKey(k, entry.next);
}
return null;
}