hashmap

1.为什么用(h = key.hashCode()) ^ (h >>> 16)算hash?
这要从数组下标位置的确定来考虑:tab[i = (n - 1) & hash],数组下标的确定是数组长度-1然后&元素的hash值。
1.1(n - 1) & hash为什么能保证数组下标不能越界呢?
这就跟hashMap容量有关,hashMap巧妙的利用了2的幂方来作为容量,默认情况下初始容量为16,之后扩容是旧容量的2倍。我们以16为例:16-1 =15,写成二进制:1111,这种二进制跟任何一个二进制取&都会小于15,这就相当于%操作,但&的效率会高很多。下标现在是可以确定了,但如果元素的hash值不够散列,就会造成hash碰撞问题,hash值相同的key会以单链表或红黑树的形式储存。Hash碰撞比较严重的话会严重影响hashMap查询和插入的性能,所以应该尽量使hash值随机。
从上面我们知道一个容量小于2的16次方的hashMap,元素的高16位是完全没有用的,顶多只能用到低16位,因为2的16次方-1写成二进制:0000 0000 0000 0000 1111 1111 1111 1111,高16都是0,跟任何数&高16位都会是0.所以在这种情况下高16位几乎没有用,不会影响元素的位置,而大多数情况下hashMap的元素都不会超过2的16次方.所以应该要想办法让元素hash值高16位参与下标确定.
h >>> 16就是向右移16的意思,此时hash的高16就变成了低16,现在就是高16怎样跟低16运算才能足够的随机,因为结果越随机,(n - 1) & hash算出来的值就会更随机.&和|只有0和1结果,^会更加均匀.(h = key.hashCode()) ^ (h >>> 16)算hash的方式就出来了.
2.长度为什么是的2的幂次方
2.1可以使用(n - 1) & hash方式确定下标,效率更高。
2.2扩容时重新计算位置会更加简单(针对有hash冲突情况),扩容后新容量是旧容量的2倍,相当于原二进制向左移动一位,比如之前容量是16,二进制是1 0000,扩容后是32,二进制是
10 0000.也就是元素的高位部分最后一个会参与(n - 1) & hash位置计算,而使用hash&oldCap方式就能确定参与运算的高位会不会改变原来的位置.当参与计算的高位是1时,新位置=j+oldCap,当参与计算的高位是0时,新位置=j。
3.1.8如何解决了1.7之前会造成死循环的问题?
3.11.7会造成死循环的由来
假设现在有a和b两个元素的key的hash值冲突了,那么这两个元素就会以链表的形式储存:a->b->null.现在线程1和线程2都来进行扩容,假设线程1和线程2同时执行Entry<K,V> next = e.next; 然后线程1暂时挂起,线程2继续执行扩容,扩容后:b->a->null.此时线程1开始执行执行Entry<K,V> next = e.next;,a的next是b,执行之后:a->null,此时在执行执行Entry<K,V> next = e.next;b的next是a,执行之后b->a->null,再执行执行Entry<K,V> next = e.next;a的next是null。执行之后:a->b->a->null.这就形成了环状,在get时就会形成死循环。
3.2 1.8如何解决死循环
从上面可以看出,形成环的主要原因是要把单链表倒置,也就是头插入,如果是尾插入就不会改变next指针的指向。也就不会形成环状链表,所以1.8使用了尾插入,而且使用尾插入还有一个好处是hash冲突元素扩容新位置会很好计算,上面以阐释,这里就不展开说明了。
3.21.8 还有什么线程安全问题?
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null)
可能会形成数据覆盖,线程a和线程b同时执行(p = tab[i = (n - 1) & hash]) == null)且i是一样的,之后线程a挂起,线程b执行,之后线程a再执行就会覆盖线程b的元素。
++size
可能使size跟实际size不一样。

### HashMap 的原理与实现 #### 哈希表的基本概念 `HashMap` 是基于哈希表(Hash Table)实现的,其核心思想是通过键的哈希值将数据存储到数组中的特定位置。为了处理哈希冲突(即不同键计算出相同的哈希值),`HashMap` 使用了链表和红黑树的结合结构[^1]。 在 Java 中,每个键值对被称为一个 `Entry` 对象。`HashMap` 内部维护了一个数组,数组的每个元素是一个桶(bucket)。每个桶中可以包含一个或多个 `Entry`,这些 `Entry` 以链表或红黑树的形式存储[^3]。 #### 存储机制 当向 `HashMap` 插入一个键值对时,首先调用键的 `hashCode()` 方法获取哈希值,然后通过哈希值与数组长度进行取模运算确定该键值对应存储在数组的哪个位置(即哪个桶中)[^2]。例如: ```java int index = hash(key) % table.length; ``` 如果多个键的哈希值相同,则它们会被存储在同一桶中,形成链表结构。为了避免链表过长导致查找效率下降,`HashMap` 在链表长度超过阈值(默认为8)时将其转换为红黑树;而当链表长度低于阈值时则再退化回链表[^3]。 #### 数组长度的设计 `HashMap` 的数组长度总是为 2 的次幂,这样设计的原因是为了优化哈希值的分布均匀性。通过位运算代替取模运算,可以更快地定位桶的位置。具体来说,当数组长度为 2 的幂时,可以通过以下方式计算索引: ```java index = (table.length - 1) & hash(key); ``` 这种方式能够确保哈希值的低位均匀分布,从而减少哈希冲突的概率[^2]。 #### 键的可变性问题 在使用自定义对象作为 `HashMap` 的键时,需要特别注意对象的可变性问题。如果对象的状态发生变化,并且影响了 `hashCode()` 或 `equals()` 方法的结果,会导致 `HashMap` 无法正确找到对应的键值对。因此,在重写 `equals()` 方法时,必须同时重写 `hashCode()` 方法,以保证一致性[^4]。 #### 线程安全性 `HashMap` 是非线程安全的,这意味着在多线程环境下,如果多个线程同时修改 `HashMap` 的内容,可能会导致数据不一致或其他异常行为。例如,在扩容过程中,链表的重新哈希可能导致死循环问题。如果需要线程安全的实现,可以考虑使用 `ConcurrentHashMap` 或者通过外部同步机制来保护 `HashMap`[^2]。 #### 性能优化 JDK 1.8 对 `HashMap` 进行了性能优化,特别是在处理哈希冲突时引入了红黑树结构。相比传统的链表结构,红黑树能够在较短的时间内完成插入、删除和查找操作,尤其是在大规模数据的情况下,这种优化显著提升了性能[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值