Hash算法和HashMap hashmap和hashtable的区别 fail-fast概念

本文介绍了HashMap和HashTable的底层实现、扩容机制、索引计算方法。当容量不足时,HashMap初始长度16,扩容2倍。JDK1.7前用位桶+链表,JDK1.8用位桶+链表/红黑树解决冲突。还提及了开放定址法、再哈希法等冲突解决办法,对比了HashMap和HashTable的差异,以及fail - fast概念。

重点是这:hashMap:https://blog.csdn.net/u012512634/article/details/72

hash表:java中哈希表及其应用详解_xiaoxik的博客-CSDN博客

hash算法:关于Java的Hash算法的深入理解_千年道士的博客-CSDN博客_hash算法

HashMap:底层是用(Entry键值对)数组+链表实现的,用链地址法来解决冲突;

HashTable:底层是用Hash表实现的,Hash表底层也是数组+链表;解决冲突方法有开放定址法,再散列函数法,链地址法;

当容量不够的时候,会进行扩容(HashMap初始长度是16):

 HashMap的索引计算方法
HashMap通过put(key,value)来执行存储数据操作,通过get(key)来获取数据,而在这过程中存储的索引是非常重要的,它相当于一个坐标,有了它,就能保证我们在使用hashMap操作时的准确性。

假如调用hashMap.put(“money”,1000)方法,将会在HashMap的table数组中插入一个key是"money"的元素;这时需要通过hash()函数来确定该entry的具体插入位置,而hash()方法内部会调用hashCode()函数得到"money"的hashCode;然后putVal()方法经过一定计算得到最终的插入位置index,最终将这个entry插入到table的index位置。

key的hash值计算是通过hashCode的高16位异或低16位实现的:

h = (key.hashCode()) ^ (h>>16)

使用位运算代替了取模运算,在table的长度比较小的情况下,也能保证hashCode的高位参与到地址映射的计算当中,同时不会有太大的开销。

以下是hashmap索引过程:
在这里插入图片描述

 拿到hash值之后,最后通过 hashmap容器长度(初始长16)length, 计算hash & (length-1)的值得到下标index;

这个方法中h代表hashcode,length代表数组长度。我们发现它是用的逻辑与操作,那么问题就来了,逻辑与操作能准确的算出来一个数组下标?我们来算算,假设hashcode是01010101(二进制表示),length为00010000(16的二进制表示),那么h & (length-1)则为:

h:  0101 0101
15: 0000 1111
  &
    0000 0101


对于上面这个运行结果的取值方法我们来讨论一下:因为15的高四位都是0,低四位都是1,而与操作的逻辑是两个运算位都为1结果才为1,所以对于上面这个运算结果的高四位肯定都是0,而低四位和h的低四位是一样的,所以结果的取值范围就是h的低四位的一个取值范围:0000-1111,也就是0至15,所以这个结果是符合数组下标的取值范围的


那么假设length为17呢?那么h & (length-1)则为:

h:  0101 0101
16: 0001 0000
  &
    0001 0000


当length为17时,上面的运算的结果取值范围只有两个值,要么是0000 0000,要么是0001 000,这是不太好的
所以我们发现,如果我们想把HashCode转换为覆盖数组下标取值范围的下标,跟我们的length是非常相关的,length如果是16,那么减一之后就是15(0000 1111),正是这种高位都为0,低位都为1的二级制数才保证了可以对任意一个hashcode经过逻辑与操作后得到的结果是我们想要的数组下标。这就是为什么在真初始化HashMap的时候,对于数组的长度一定要是二次方数,二次方数和算数组下标是息息相关的,而这种位运算是要比取模更快的,用(length-1)保证下标index一定落在数组范围内。

装载因子α = 插入元素个数/哈希表中的数组个数,表示哈希表的装满程度,同时也能代表链表的平均长度

如图,如果在长度为5,装载因子为0.6的哈希表中插入3个数据,接下来再插入第4个数据的时候,就会进行rehash操作;

这里的数据计算是通过计算总的数据量,即后面链表中的数据也会计算进去;

HashMap扩容是2倍    arraylist扩容1.5倍

1.  当发生地址冲突的时候(不是容量不够的时候) 链地址法:

 以上是JDK1.7之前采用位桶+链表,缺点是:当冲突频繁发生时,查找时间复杂度变成了O(n);

java7及以前采用的是头插法插入节点,在扩容时会改变链表中元素的顺序,并以自己的节点作为新的头结点作为next指向,直观来看就是链表翻转了,并发线程下next指针可能被覆盖了就会出现链表成环问题;头插法链表成环问题见:

【图解】面试题:为什么HashMap会产生死循环_hashmap的死循环面试-CSDN博客

成环的流程图可以参考这个: HashMap并发时造成死循环问题解析 - 腾讯云开发者社区-腾讯云

循环形成的关键原因,多线程对链表扩容导致反转时:
  1. 头插法导致顺序反转:T2 迁移后链表顺序为 C → B → A
  2. T1 的中间状态失效:T1 在挂起前记录的next(即 B),其next已被 T2 修改为指向 A;
  3. 引用混乱:T1 继续迁移时,将 A 的next设为 B,同时 B 的next又指向 A,形成环。

   JDK1.8采用位桶+链表/红黑树,当一个格子内的数据长度超过了8,就会转化为红黑树,这样查找时间变成了O(logn);

java8之后采用尾插法插入节点,扩容将保持链表元素顺序,并解决了链表成环问题

根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

hash 冲突除了链地址法, 有那些解决办法?

开放定址法、再哈希法。

2.开放地址法

线性探测法:如果键值对a在index中插入了,键值对b也需要插入index,那么b会去找相邻的位置(index+1,+2,+n...)进行存放;

缺点:键值对增多之后,可能会在连续的位置形成键值对,发生冲突需要很长时间才能探测到合适位置

平方探测法:如果键值对a在index中插入了,键值对b也需要插入index,b先在index+1^2位置看是否能存放,然后再到index+2^2,+3^2..,+n^2进行存放;好处是能解决线性探测的缺点;

缺点:对于落在同一index的键值对,后面重试index一直都一样,越到后面就越需要很久探测到合适位置;

双散列:首先可以构造一个虚拟的键值对c的散列坐标indexFake,b如果在index位置上冲突了,那么后面可能移动到的位置是index+indexFake,+2*indexFake..+n*indexFake

3.再hash法

即重新hash一次,算出index值;

  1. 存储时
    • 根据键对象的hashCode()计算哈希值,确定其在数组中的存储位置(桶位)。
    • 若该位置已有元素,通过equals()比较键是否相等:
      • 若相等,覆盖旧值;
      • 若不等,则以链表(或红黑树)形式存储在该桶位下(解决哈希冲突)。
  2. 查找时
    • 根据键对象的hashCode()计算哈希值,定位到对应桶位。
    • 遍历桶位中的元素,通过equals()匹配目标键,找到则返回对应值。

  1. 相等性一致性
    • a.equals(b)为 true,则a.hashCode()必须等于b.hashCode()
    • a.equals(b)为 false,a.hashCode()b.hashCode()可以不等,但尽可能减少哈希冲突(即让不同对象的 hashCode 差异更大)。
  2. 对称性与传递性
    • equals需满足对称性(a.equals(b)为 true 则b.equals(a)也为 true)和传递性(若a.equals(b)b.equals(c),则a.equals(c)为 true)。

典型错误与正确实现示例

错误场景若只重写equals而不重写hashCode,会导致:

  • 存储时两个相等对象可能被分配到不同桶位,无法正确覆盖旧值;
  • 查找时无法通过 hashCode 定位到目标桶位,导致查找失败。

错误场景:若只重写hashCode而不重写equals,会导致:

存储时重复键问题

场景:创建两个内容相同的 Book 对象:

    • Book book1 = new Book("Java编程", "张三");
      Book book2 = new Book("Java编程", "张三");
      Map<Book, Integer> map = new HashMap<>();
      map.put(book1, 1);
      map.put(book2, 2); // 预期覆盖book1,实际存入两个键
      

原因:book1book2的 hashCode 相同,但equals比较(默认比较引用)返回 false,因此 HashMap 认为是两个不同的键,导致重复存储。

查找时无法正确获取值

场景:用book2查找book1存入的值:

Integer value = map.get(book2); // 预期返回1,实际返回null
  • 原因:HashMap 通过 hashCode 定位到桶位后,遍历元素时用equals比较,由于book1.equals(book2)返回 false,无法找到目标键。

哈希冲突与性能损耗

虽然 hashCode 正确,但equals未重写会导致大量本可合并的键被视为不同键,增加链表长度,降低 HashMap 性能(退化为链表查找)

hashMap的源码:

具体如何重写equals和两种hashCode的方式:

//重写equals方法
@Override
public boolean equals(Object o) {
    // 快速判断:同一对象引用直接返回true
    if (this == o) return true;
    // 空引用或类型不同返回false
    if (o == null || getClass() != o.getClass()) return false;
    // 类型转换
    Student student = (Student) o;
    // 比较关键属性(需覆盖所有影响对象相等性的字段)
    return id == student.id && name.equals(student.name);
}



//重写hashCode的两种方法
@Override
public int hashCode() {
    // 方案1:使用Objects.hash组合字段(推荐,简洁且高效)
    return Objects.hash(id, name);
    
    // 方案2:手动计算哈希(适用于性能敏感场景)
    int result = 31 * id; // 31是质数,可减少冲突  (5<<i) - i 位运算方便
    result += name.hashCode();
    return result;
}

kotlin的data class自动编译生成:

(this.num * 31) + this.name.hashCode();

 HashMap和HashTable比较:

不同:

1.HashMap支持null值和null键的,而HashTable则会抛出空指针。原因是类内部做了处理。

2.HashMap不是线程安全的,而HashTable是线程安全的。因为HashTable中的方法的是用Synchronize同步过的

 如果HashMap想进行同步的话,可以用ConcurrentHashMap,采用了锁分段技术,不同的竞争资源采用不同的锁:

相较于hashtable和用了Synchronize同步的hashmap,比如一个线程再进行putall写入大量数据,期间调用线程B去get,那么线程B就会阻塞,唯一的好处就是get的时候能获取到完整更新后的全部数据

ConcurrentHashMap:一个ConcurrentHashMap包含了一个Segment数组,维护了一个HashEntry数组,每次读取数组中的数据的时候都需要获取segment对应的锁;java8之后采用cas+sychronize来代替锁分段实现;

在Java 8中,整个哈希表被分成若干个段,每个段都被实现为一个数组,每个数组元素都是一个链表或红黑树。每个元素都是一个桶,通过哈希函数将键值对映射到桶中。每个桶内部都通过synchronized来实现同步,以保证线程安全性而对于读操作,使用了无锁的CAS操作。 相比于分段锁,这种实现方式有以下优点:
减少锁竞争:由于每个桶内部采用了synchronized来实现同步,不同的线程可以同时访问不同的桶,从而减少了锁竞争。
更高的并发度:由于不再受限于固定数量的段,ConcurrentHashMap 可以根据需要动态调整大小,并支持更高的并发度。
更好的扩展性:由于不再需要维护多个段的锁,因此在扩展时可以更容易地添加或删除桶,而不需要重构整个数据结构。
更好的性能:使用CAS操作替代了分段锁,避免了分段锁中的自旋等待开销,提高了并发性能。

3.初始化的hashMap长度是16,而hashTable初始化长度是11;

相同:HashTable除了和HashMap有上述不同,其他基本相同,比如都用哈希表来Entry对象(含键值的对象)

fail-fast:指的就是多个线程在arraylist等非线程安全的集合中进行增删等操作时,造成的异常;

HashMap中hash函数怎么是是实现的?还有哪些 hash 的实现方式?
  1. 对key的hashCode做hash操作(高16bit不变,低16bit和高16bit做了一个异或);
  2. h & (length-1); //通过位操作得到下标index。

  还有数字分析法、平方取中法、分段叠加法、 除留余数法、 伪随机数法。?

Hash函数的其他构造方法(算hashcode):

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值