仅当做的记录
HashMap:
首先hashmap的底层是基于数组结构和链表结构和红黑树组合成的哈希表结构
数组结构的特点:
1.动态数组在我们的arrayList中查询是非常快的,因为它是连续内存块存储的,查询是随机查询的
2.动态数组的插入和删除,若是指定索引位置就会比较慢,因为指定索引位置的插入或者删除的时候会使数组整体移位时间复杂度O(n),当然如果是正常add ,那么就是默认尾插法,那时间复杂度就是O(1)
3.ArrayList的初始长度是10,若是扩容的话1.5倍的扩容,也就是老数组+老数组的0.5倍
链表结构的特点:
1.linkedList的底层结构是有链表组成,使得我们查询较慢,新增或者删除较快
2.链表结构底层是由一个Node对象组成,内部由当前数据item,下一个数据next,上一个数据prev组成
3.链表结构查询和修改慢,增删快因为链表是由一个node对象组成,内部有next表示下一个节点,prev表示上一个节点,item表示当前数据,当查询的时候它会通过头节点或者尾节点开始查询,一个一个遍历得到,所以很影响效率,而增删快的原因是因为node节点内的关联字段,next和prev,将这两个内置字段更改关联当前存储的数据即可;
而哈希表结构就是结合了数组结构和链表结构的优点组合而成的,达到查询修改和增删快的效果
put方法主要源码解析:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果数组的长度等于空或者长度等于空则初始化长度 resize()方法中既有扩容也有初始化的逻辑
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//通过hash找到下标,判断数组下标是否为空,如果为空就在当前下建立一个新的node
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果此数组下标不为空,那么此处k表示数组中的第一个节点中的node
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == || (key != null && key.equals(k))))
//那么就表示这个数组下标并没有链表
e = p;
//表示这个数组下存在链表,判断是否是数组织,如果是,那么就遍历数组织,有就覆盖,没有就添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果是链表结构,遍历链表,如果存在这个key就覆盖,如果不存在就尾部添加
for (int binCount = 0; ; ++binCount) {
//遍历的这个是不是最后一个,next等于null表示最后一个,那么就新增一个node
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//如果这个链表长度>=8那么就进入此方法判断数组是否>=64就开始做一系列的扩容处理
treeifyBin(tab, hash);
break;
}
//如果新添加的hash值等于链表中的hash,key等于链表中的key,那么就覆盖它
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果node值不为空且value为null,那么就将它移动到尾部去
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//将当前节点移动至尾部
return oldValue;
}
}
++modCount;
//如果长度大于了阈值=初始值*加载因子0.75
if (++size > threshold)
resize(); //做扩容
afterNodeInsertion(evict);
return null;
}
put方法总结:
jdk1.7中是没有引入红黑树的结构的,是以数组和链表组成,但是存在隐患就是如果哈希冲突太严重,导致链表过长,则查询效率会非常的低,1.7内部存储的节点用的是Entry这个对象去存储的,内部封装的是hash,key,value,next;
而jdk1.8引入了红黑树的概念,一开始put的时候传入一个key和value,它会拿着这个key去获取它的hashCode然后去根据数组的长度做一个取模的运算,计算出了下标的范围,它会去判断这个数组的下标上是否存在了node这个对象(node封装的是key,value,hash,next),如果不存在,则新增一个node到此节点中,若是存在,则需要判断它是否存在树组织,如果存在树组织就表示已经是红黑树了,通过key,hash,next去查找树组织上是否已经存在了相同的key的值,如果存在则覆盖它的value,如果不存在则根据数组织的左小,中,右大去存入此node,如果不存在树组织即表明是链表,遍历此链表,如果存在相同key则覆盖,如果不存在则尾部添加,还要判断链表长度是否大于等于8,如果大于等于了,则判断数组长度是否等于64,如果是则需要将它变更成红黑树,否则不用;然后就modCount++,然后判断如果size大于扩容的阈值,则进行扩容;
get方法主要内容解析:
final HashMap.Node<K,V> getNode(int hash, Object key) {
HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
//如果数组中不为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//如果数组中第一个值得hash和key相等,那么就返回对应对应得node
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果第一个node得指针不为null
if ((e = first.next) != null) {
//是否是树组织
if (first instanceof HashMap.TreeNode)
return ((HashMap.TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历链表如果相等则返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
//遍历到最后一个next为真则返回null
} while ((e = e.next) != null);
}
}
return null;
}
get方法总结:
hashMap.get(key,value)的原理:入参key之后通过key去计算它得hash值,然后获取它的数组下标值,然后判断这个数组下标是否为空,如果为空那么就返回null,否则就判断数组第一个node中的hash和key是否相等,如果不相等则判断第一个node中是否存在next,如果存在那么就判断这个node是否为树组织,如果是那么就去遍历红黑树,如果存在key则返回对应value,如果没有则返回null,其次如果不是树组织,那么就遍历所有链表,如果链表没有就返回null,如果存在就返回value;
什么是hash值:
实则就是通过一定的哈希算法(MD5,SHA-1),将一段较长的数据加密成一段较短的数据,这个较短的数据就是较长数据的哈希值,他的特点是唯一的,但凡大数据中有一点细微变化,那么他的哈希值都会跟着变化;
hash冲突:
则是说明此数组下标中已存在了node对象,使用的是链式存储法解决;
为什么引入红黑树:
因为1.7内会导致链表太长,性能就会受到一定的影响,其次红黑树遵循左中右的结构,查询的速度比较链表提高了一倍不止,为什么红黑树一定要是8,因为如果红黑树设置为7,临界值的情况,那么会导致频繁的链表和红黑树频繁切换
简单基于数组链表实现hashMap:
定义接口:
package com.kirn.map;
/**
* @author
* @date 2022/3/27
*/
public interface Map<K, V> {
V put(K k, V v);
V get(K k);
int size();
interface Entry<K, V> {
K getKey();
V getValue();
}
}
重写方法:
package com.kirn.map;
import org.springframework.util.ObjectUtils;
/**
* @author
* @date 2022/3/27
*/
public class HashMap <K,V> implements Map<K,V>{
//定义一个数组
private Entry<K,V>table[];
int size=0;
public HashMap(){
table=new Entry[16];
}
/**
* 1.通过key去获取它的hash值,取余获取下标
* 2.判断下标中是否存在数据,如果不存在那么直接放入
* 3.如果下标中存在next那么就表示链表,遍历链表做hash和key对比,如果相等就覆盖
* 4.如果不相等就放入
* @param k
* @param v
* @return
*/
@Override
public V put(K k, V v) {
//计算hash得到数组
int index=hash(k);
//获取数组中下标的位置
Entry<K,V>entry=table[index];
//判断数组中是否存在node
if (ObjectUtils.isEmpty(entry)){
//如果数组中不存在
table[index]=new Entry<>(k,v,index,null);
size++;
}else {
//如果是链表,那么就头插法进入
table[index] = new Entry<> (k,v,index,entry);
size++;
}
return table[index].getValue();
}
private Integer hash(K k) {
int code = k.hashCode()%16;
return code>=0?code:-code;
}
/**
* 1.根据key获取hash值
* 2.判断数组的下标是否为空,空的话返回null
* 3.如果不为空,对比,不相等就判断next是否为空
* 4.如果不为空,遍历找出对比,如果没有返回null
* 5.如果为空,直接返回
* @param k
* @return
*/
@Override
public V get(K k) {
//计算下标
Integer hash = hash(k);
//判断是否是数组还是链表,链表就需要继续循环
Entry<K,V>entry=findValue(table[hash],k);
return entry==null?null: entry.getValue();
}
private Entry<K, V> findValue(Entry<K, V> kvEntry, K k) {
if (k.equals(kvEntry.getKey())||k==kvEntry.getKey()){
//如果下标节点的key等于我们传入的key,就返回
return kvEntry;
}else {
//存在链表,递归寻找,直到next为空的情况
if (kvEntry.next!=null){
return findValue(kvEntry,k);
}
}
return null;
}
@Override
public int size() {
return size;
}
class Entry<K,V> implements Map.Entry<K,V>{
K k; V v; int hash; Entry<K,V> next;
public Entry(K k, V v,int hash,Entry<K,V>next) {
this.k = k;
this.v = v;
this.hash = hash;
this.next = next;
}
@Override
public K getKey() {
return k;
}
@Override
public V getValue() {
return v;
}
}
}
测试:
public class MapTest {
@Test
public void test1() {
HashMap<Integer, String> map = new HashMap<>();
int a = 1;
int b = 0;
System.out.println(map.put(4, "1"));
System.out.println(map.put(2, "2"));
System.out.println(map.put(3, "3"));
System.out.println(map.put(4, "6"));
System.out.println(map.put(4, "0"));
System.out.println(map.get(4));
}
什么是hash值:
实则就是通过一定的哈希算法(MD5,SHA-1),将一段较长的数据加密成一段较短的数据,这个较短的数据就是较长数据的哈希值,他的特点是唯一的,但凡大数据中有一点细微变化,那么他的哈希值都会跟着变化;
哈希值的作用:
哈希值是通过对文件内容进行加密运算后得到的一组二进制值,基本上用来对文件校验,做对比或者签名;
HashMap和HashTable的区别:
1.线程安全问题:
首先HashMap是线程不安全的,效率高,而Hashtable是线程安全的,效率低;
为什么说hashtable是线程安全的呢,因为它的内部方法用了同步锁synchronized,在多线程并发的情况下可以使用这个,不需要为自己的方法实现同步
而HashMap是线程不安全的,但是它的效率,速度都要比HashTable快得多,一般用在单线程的情况下,如果碰到多线程并发的情况需要操作的话,那么就使用ConcurrenHashMap,虽然ConcurrenHashMap也是线程安全的,但是它的速度就要比HashTable快得多,因为它是CAS+局部锁,以及自旋锁,并不是对整个数据都做锁的,它的锁粒度比hashtable锁粒度更细;
2.key和value支不支持null的问题
首先HashTable它既不支持key为null,也不支持value为null而HashMap的key支持为null,但是只是唯一,而value却可以支持多个为null;
3.初始容量大小和扩容大小的不同
首先HashTable的默认初始值为11,每次扩容的容量会变为原来的2n+1
而HashMap的默认初始值为16,而每次扩容的容量为变为原来的2倍
实际上之所以不同的原因是因为他们两个的Hash计算方式不同,HashTable侧重于哈希的均匀,使得哈希减少冲突,而HashMap是为了加快hash的速度,将Hash的大小固定为2的幂,HashTable和HashMap的底层都是用的哈希表,但是位运算法却不同,HashTable用的是除留取余,使得哈希更均匀,而HashMap直接对2取模,更改得到位运算;
4.hashCode的算法:
散列算法
通过我们的key的值,计算出每一个字符串的ascii码,然后相加进行取模 算出hash表中的下标
比如429/当前数组的长度=下标;
5.为什么jdk1.8要使用红黑树
因为如果不用红黑树,那么会一直循环链表去做查询操作,红黑树结构是左中右,小中大的结构,效率要比链表快一倍
6.jdk8中HashMap什么时候将链表转化为红黑树?
当链表中元素个数大于8的时候,这个时候还会去判断数组元素是否大于64,如果不大于,继续走扩容,如果大于等于就会将链表转化为红黑树;
7.为什么链表个数8,数组长度一定要大于等于64才进行转化红黑树?
因为数组的长度还比较小,就先利用扩容来缩短链表的长度;
8.jdk7和jdk8的区别:
1.jdk7中的节点使用的是Entry对象封装,jdk8使用的是node节点封装
2.jdk8加入了红黑树
3.jdk7中插入使用的是头插法,因为头插法速度更快,无需遍历链表,但是再多线程并发的情况下使用头插法会出现循环链表的问题,会导致CPU飙高,而jdk8使用了尾插法,本身就要去遍历链表,所以直接使用尾插法
4.jdk7的hash算法比jdk8的hash算法更复杂,jdk8中使用的更散列,元素个数更散列所以查询性能更好,jdk7中没有红黑树,所以只能优化hash算法使得元素更散列
5.jdk8的扩容条件和jdk7不一样,除开判断size是否大于阈值之外,jdk7还判断了table[i]是否为空,不为空的时候才会进行扩容,jdk8中则没有该条件
6.jdk7和jdk8扩容过程中转移元素逻辑不一样,jdk7中每次转移一个元素,jdk8是先算出来当前位置那些元素处于新数组高位,那些处于新数组低位,然后一次性转移;