HashMap
- 为什么是 大于等于 2的幂次运算
- key生成 数组 是为什么一位运算
- 程序是什么? 程序本质是数据结构和算法组成。
HashMap底层存储原理详解
基本了解
存储》put; 查询》get; 其实存一个
-
底层结构
jdk1.7:数组 + 链表
jdk1.8: 数组 + 链表 + 红黑树 ( 红黑树解决1.7版本什么性能问题)
hash 算法
数组:采用一段连续的存储单元来存储数据的;特点:查询时间复杂度o(1),删除插入时间复杂度o(n), 总结:查询快 增删慢
链表:采用一种物理存储单元上非连续,非顺序的存储结构;特点:插入删除时间复杂度o(1), 查找遍历时间复杂度o(n), 总结 : 增删快 查询慢
ArryList 底层使用数组结构
[图片上传失败...(image-e4ba3c-1639821670796)]
LinkedList底层使用双向链表结构
[图片上传失败...(image-8002d9-1639821670796)]
hash算法
哈希算法(也叫散列),就是把任意长度值(key)通过散列算法变换成固定长度的key(地址)通过这个地址进行访问的数据结构
它通过把关键码映射到表中一个。位置来访问记录,以加快查找的速度。
[图片上传失败...(image-4af96-1639821670796)]
ASCII码 md5
HashCode: 通过字符串算出它额ascii码,进行mod(取模)(%), 算出哈希表中的下标
public static void main(String[] args) {
char c[] = "lies".toCharArray();
for (int i = 0; i < c.length; i ++) {
System.out.println(c[i] + ":" + (int)c[i]);//转换成ascii码
}
}
//打印
l:108
i:105
e:101
s:115
[图片上传失败...(image-6fbe73-1639821670796)]
为什么要进行取模?
为了节省数组空间,所以对429%10 等于 9
;
如果取模后后可能会产生hash冲突(hash碰撞),咋样解决? 使用链表解决
[图片上传失败...(image-105780-1639821670796)]
[图片上传失败...(image-4392a6-1639821670796)]
hashmap存储:
- 将一个
对象的key通过哈希算法算出哈希,然后对hash取模得出一个数组下标 - 然后通过下标去找到存储位置存储
- 如果有相同的下标,则需要把后来的对象当成最新的链表根节点,把next指向下一个节点即可
hashmap取出:
- 通过key查出hash值和下标
- 通过下标去数组查询
- 如果有相同下标,则需要比较hash值,如果hash值不相同,则通过链表向下查找,直到查到hash值相同即可
算法对比
[图片上传失败...(image-64fcbe-1639821670796)]
手写代码实现(数组 + 链表)
定义一个接口
public interface Map {
V put(K k, V v);
V get(K k);
int size();
interface Entry {
K getKey();
V getValue();
}
}
定义一个实现类
package com.kuang.demo12_data_structure.data08_hashmap;
/**
* @description:
* @author: li
*/
public class HashMap implements Map {
private Entry table[] = null;
int size = 0;
public HashMap() {
this.table = new Entry[16]; //java中一般默认为 2^4=16 容量
}
/**
* 1. 通过key hash算法 计算出hash值
* 2. index下标数组 当前下标对象
* 3. 判断当前位置是否有对象, 如果没有 直接存储
* 4. 如果有对象,冲突, 使用next链表
* 5. 返回当前的节点对象
* */
@Override
public V put(K k, V v) {
System.out.println("key:" + k + "::::hash值:" + k.hashCode() + ":::::存储位置:" + (k.hashCode() % 16) );
int hash = k.hashCode();
int index = hash(k);
Entry entry = table[index];
if( null == entry) {
table[index] = new Entry<>(k, v, hash, null);
size ++;
} else {
//hash冲突了
table[index] = new Entry<>(k, v, hash, entry);
}
return table[index].getValue();
}
private int hash(K k) {//简单取模实现hash算法,真实hash算法实现底层移位操作
int index = k.hashCode() % 16;
return index >= 0? index : -index;
}
/**
* 1. 通过key hash算法 计算 数组索引
* 2. 如果索引值和hash值一样 说明就是你找到的对象,如果hash值不相同,则通过链表继续往下查找(递归查找)
* */
@Override
public V get(K k) {
int hash = k.hashCode();
int index = hash(k);
Entry entry = table[index];
V v;
if(entry.getHash() == hash) {
//数组找到了
v = entry.getValue();
} else {
//hash冲突了 向下查找链表
Entry kvEntry = recurFind(entry, hash);
if(null != kvEntry) {
v = kvEntry.getValue();
} else {
v = null;
}
}
return v;
}
/**
* @param entry 当前根节点的链表对象
* @param hash 需要查询hash
* */
private Entry recurFind(Entry entry, int hash) {
//next指向entry
Entry next = entry.getNext();
if(next.getHash() == hash) {
//next的对象的hash值与 需要hash值相同 就是找的对象
return next;
}else {
//不相同继续向下查找
recurFind(next, hash);
}
return null;
}
@Override
public int size() {
return 0;
}
class Entry implements Map.Entry {
K k;
V v;
int hash;
Entry next;
public int getHash() {
return hash;
}
public void setHash(int hash) {
this.hash = hash;
}
public Entry getNext() {
return next;
}
public void setNext(Entry next) {
this.next = next;
}
public Entry(K k, V v, int hash, Entry next) {
this.k = k;
this.v = v;
this.hash = hash;
this.next = next;
}
@Override
public K getKey() {
return this.k;
}
@Override
public V getValue() {
return this.v;
}
}
}
测试打印
public class Test2 {
public static void main(String[] args) {
// Test2 test2 = new Test2();
// test2.put("刘一", "刘一");
Map myMap = new HashMap<>();
myMap.put("刘一", "刘一");
myMap.put("张三", "张三");
myMap.put("李四", "李四");
myMap.put("王五", "王五");
Object ly = myMap.get("刘一");
Object zs = myMap.get("张三");
Object ww = myMap.get("王五");
System.out.println(ly);
System.out.println(zs);
System.out.println(ww);
}
public void put(String key, String value) {
System.out.println("key:" + key + "::::hash值:" + key.hashCode() + ":::::存储位置:" + (key.hashCode() % 10) );
}
}
[图片上传失败...(image-d75ca9-1639821670796)]
1.7版本的实现的hashmap(数组+链表)缺点
当存储数据过时,hash冲突很多,所以链表长度会很长,导致查询数据特别慢,查询性能会很低
所以1.8后为了解决链表过长查询慢,使用红黑树结构;阈值为8才有红黑树;
[图片上传失败...(image-c1ca12-1639821670796)]
为什么链表长度大于8才使用红黑树?
[图片上传失败...(image-9f8ad3-1639821670796)]
[图片上传失败...(image-c9a177-1639821670796)]
红黑树在插入操作为了保证红黑结构,左小右大...,会牺牲插入性能。
并发包
[图片上传失败...(image-baf59d-1639821670796)]
[图片上传失败...(image-cc4ab7-1639821670796)]
对于hashmap面试
1.7 | 1.8 | |
---|---|---|
数据结构 | 数组+链表 | 数组+链表+红黑树 |
链表插入方式 | 头插法 | 尾插法 (因为1.7头插法在并发中会形成循环链表耗尽cpu性能) |
hash算法 | 使用计算机底层位运算 | 简化 |
红黑树阈值 | 当链表长度大于8时使用红黑树 | |
多线程是否安全 | 不安全 | 不安全 |
在初始化数组时,传入一个2的次幂的容量,put时,会初始这个数组,容量是大于等于初始化容量的最近的2次幂,比如初始容量是6,那么它初始化的就是8
-
hash算法:
- 首先调用hashCode方法获取key对应的hashCode值(h)
- 然后进行高位运算:将h右移16位取得高16位,与之前低h 进行异或运算(h ^ (h >>> 16))
- 然后再得到值 与 容量 -1进行与运算,从而得到改对象再数组的下标(hash & (length - 1) = index)
//位运算效率高 String key = "e"; int h = key.hashCode(); h = h ^ (h >>> 16); int index = h & (16 - 1); System.out.println(index); //效率低 String key2 = "e"; int index2 = key2.hashCode() % 16; System.out.println(index2);
key的hashCode 与 容量 - 1 进行 与操作,计算数组的下标(因为在计算机中与操作性能最好)
在存储数据时,有2个问题需要注意:扩容问题和树化问题;
扩容问题:加载因子0.75(3/4),达到容量的0.75就会扩容,两倍扩容(耗性能)
树化问题:数组容量达到64,链表长度大于等于8,后才会进行树化,链表长度小于6就会解除树化(树化耗性能)
hashmap线程不安全
put元素的时候,A线程对一个元素进行hash计算桶的索引坐标,然而正当它准备插入元素的时候,B线程被调度并且被完整执行,如果这个时候A和B线程获得索引坐标是一样的,那么B会插入元素形成新的链表,但是A线程拿到的旧链表信息,所以A线程执行完后会覆盖B线程插入的元素