HashMap
1、数据结构
JDK7:
在 JDK 7 中,HashMap 的内部实现采用了数组+链表的方式来解决哈希冲突。具体来说,HashMap 的内部维护了一个 Entry 数组,每个数组元素都是一个链表的头节点,链表中存储了哈希冲突的键值对。
在 JDK 7 的实现中,HashMap 使用了链表来处理哈希冲突,当发生哈希冲突时,新的键值对会被添加到链表的头部。这样做的好处是简单、高效,但是当链表过长时,会导致性能下降,查找效率变低。
JDK 7 中的 HashMap 在插入、查找、删除操作的平均时间复杂度都是 O(1),但是在最坏情况下,如果链表过长,时间复杂度可能达到 O(n),其中 n 是链表的长度。
需要注意的是,在 JDK 7 中,当链表长度达到一定阈值(默认为 8)时,链表会转换为红黑树,以提高查找效率。这是因为红黑树的查找时间复杂度是 O(log n),比链表的 O(n) 更低。这个优化使得在哈希冲突较多的情况下,HashMap 仍然能够保持较高的性能。
总结起来,JDK 7 中的 HashMap 使用了数组+链表的方式来解决哈希冲突,并且在链表长度达到一定阈值时,会转换为红黑树以提高查找效率。这些优化使得 HashMap 在实际应用中能够高效地存储和查找大量的键值对。
在 JDK 7 中,HashMap 在插入元素时使用的是头插法。所谓头插法,是指将新的元素插入链表的头部。
当发生哈希冲突时,新的键值对会被插入到链表的头部,成为新的头节点,而原来的头节点则成为新节点的后继节点。这样做的好处是简单、高效,因为插入操作只需要修改几个指针的指向即可完成。
然而,使用头插法也存在一个问题,就是链表的顺序与元素插入顺序相反。也就是说,最新插入的元素会排在链表的前面,而最早插入的元素会排在链表的后面。
另外需要注意的是,在 JDK 7 中,由于链表中的节点是通过引用来连接的,如果链表中存在循环引用的情况,可能会导致内存泄漏。因此,在使用链表的数据结构时,需要特别注意避免出现循环引用的情况。
总结起来,JDK 7 中的 HashMap 在插入元素时使用头插法,将新的键值对插入到链表的头部。这种插入方式简单高效,但需要注意避免链表中的循环引用问题。
JDK8:
对于 JDK 8 中的 HashMap,它的内部实现使用了数组+单向链表+红黑树的混合结构来解决哈希冲突。这种结构既保留了 JDK 7 中的链表解决冲突方式,也引入了红黑树用于优化性能。
具体来说,JDK 8 中的 HashMap 在内部维护了一个 Entry 数组,每个数组元素都是一个单向链表(或红黑树)的头节点。链表和红黑树的选择是根据链表长度和扩容时的条件来决定的。
当插入新的键值对时,HashMap 会先根据键的哈希值确定插入的位置,如果该位置已经存在元素,则根据键的值进行比较来判断是否是同一个键。如果不是同一个键,发生哈希冲突时,新的键值对会被插入到链表(或红黑树)的头部。
当链表长度达到一定的阈值(默认为 8)时,链表会转换为红黑树,以提高查找效率。这个优化是为了应对在哈希冲突较多的情况下,链表长度过长导致的性能下降问题。
在 JDK8 中,HashMap 的插入、查找、删除操作的平均时间复杂度仍然是 O(1),但是红黑树的平均查找时间复杂度是 O(log n),比链表的 O(n) 更低。
总结一下,JDK 8 中的 HashMap 使用数组+单向链表+红黑树的混合结构来解决哈希冲突,链表会在长度达到一定阈值时转换为红黑树,以提高查找效率。这些优化使得 HashMap 在实际应用中能够高效地存储和查找大量的键值对。
在 JDK 8 中的 HashMap,在插入元素时使用的是尾插法。所谓尾插法,是指将新的元素插入链表的尾部。
当发生哈希冲突时,新的键值对会被插入到链表的尾部,成为后继节点,而原来的尾节点则成为新节点的前驱节点。这样做的好处是在链表的末尾插入元素,可以保持链表中元素的插入顺序和元素在哈希表中的顺序一致。
相比于头插法,尾插法在遍历链表时的顺序更符合插入顺序,更容易理解和调试。同时,尾插法可以尽量避免链表循环引用的问题。因为它是将新节点插入到已有链表的尾部,不会改变链表中原有节点的指引,从而避免了循环引用的产生。
需要注意的是,在 JDK 8 中,在链表长度达到一定阈值(默认为 8)时,链表会被转换为红黑树。这个优化是为了提高查找效率。但是,红黑树在插入操作时并没有尾插法的概念,而是通过旋转和调整来维持树结构的平衡。
综上所述,JDK 8 中的 HashMap 在插入元素时使用尾插法,将新的键值对插入到链表的尾部,可以尽量避免链表循环引用的问题。在链表长度达到一定阈值时,链表会被转换为红黑树以提高查找效率。
引入红黑树是为了提高查找效率。
在 JDK 8 中的 HashMap,在内部使用链表来解决哈希冲突。但是,在哈希冲突较多、链表长度过长的情况下,使用链表进行查找的效率会下降,因为链表的查找操作需要依次遍历链表中的每个节点。
为了解决这个问题,JDK 8 中的 HashMap 引入了红黑树。当链表长度达到一定阈值(默认为 8)时,会将链表转换为红黑树。
红黑树是一种自平衡的二叉搜索树,它的平均查找时间复杂度是 O(log n),比链表的 O(n) 更低。使用红黑树作为替代链表的数据结构,可以大大提高查找操作的效率。
需要注意的是,红黑树在插入、删除节点时需要进行旋转和节点颜色的调整,这些操作相对复杂。因此,红黑树的优化是在哈希冲突较多的情况下才发挥作用,对于冲突较少的场景,依然使用链表进行存储和查找是更高效的选择。
总结一下,引入红黑树是为了提高在哈希冲突较多、链表长度过长时的查找效率。红黑树是一种自平衡的二叉搜索树,相比链表具有更低的平均查找时间复杂度,从而优化了 HashMap 的性能。但对于哈希冲突较少的情况,链表仍然是更高效的选择。
创建对象
在创建 HashMap 对象时,主要会进行以下操作:
- 分配内存空间:HashMap 内部会分配一定大小的数组,用于存储键值对。
- 初始化加载因子:加载因子是指在哈希表中存储元素的填充程度。HashMap 使用加载因子来控制何时需要扩容。在 JDK 8 中,默认加载因子为 0.75。这意味着当 HashMap 的实际元素个数达到数组容量的 75% 时,会触发扩容操作。
- 初始化容量:HashMap 可以在创建时指定初始容量,即数组的大小。如果未指定初始容量,默认为 16。初始容量应该根据预期存储的元素数量进行合理的设置,以避免频繁的扩容操作。
- 初始化其他属性:HashMap 还会初始化其他一些属性,如实际元素个数(size)、阈值(threshold)等。阈值是根据加载因子和初始容量计算得出的,它表示数组的容量上限,当实际元素个数达到阈值时,会触发扩容操作。
需要注意的是,HashMap 的创建并不会立即分配数组空间,而是在第一次插入元素时才会进行数组的分配和初始化操作。
总结一下,创建 HashMap 对象时会进行一系列的初始化操作,包括分配数组空间、初始化加载因子、初始化容量和其他属性。在创建时可以指定初始容量,加载因子默认为 0.75。
增加元素
在 HashMap 的 put 方法中:
- 首先,对传入的 key 进行判断,如果 key 为 null,则会将该键值对放在数组的第一个位置。
- 接着,HashMap 会对 key 进行哈希运算以得到对应的哈希值。哈希值是一个整数,用于确定元素在数组中的位置。
- 在进行哈希运算之后,HashMap 会对哈希值进行右移 16 位,将高 16 位与低 16 位进行异或运算。这个操作是为了让哈希值的高位也参与到计算中,以增加哈希值的随机性,从而减少哈希冲突的概率。
- 接下来,HashMap 会根据哈希值和数组的长度计算出元素在数组中的索引位置。
- 如果计算得到的索引位置已经存在其他键值对(即发生了哈希冲突),HashMap 会使用链表或红黑树解决冲突。最新插入的键值对会被插入到链表或红黑树的头部或树的叶子节点。
- 如果计算得到的索引位置无冲突,即没有其他键值对,那么新的键值对会直接插入到数组的该位置。
需要注意的是,HashMap 使用数组和链表(或红黑树)的组合来实现底层的数据存储结构。当链表长度达到一定阈值时,链表会被转换为红黑树,以提高查找效率。
总结一下,在 HashMap 的 put 方法中,key 可以为 null,并且可以放在数组的下标为 0 的位置。对于非 null 的 key,会对其进行 hash 运算,并将哈希值的高 16 位与低 16 位进行异或运算,然后根据计算得到的索引位置进行插入操作。如果发生了哈希冲突,会使用链表或红黑树来解决冲突。
在 HashMap 的 put 方法中,会调用 putVal 方法来实际执行键值对的插入操作。下面是 putVal 方法的主要流程:
-
首先,检查数组是否为空或长度为 0。如果是,则进行数组的初始化,将长度设为 16,阈值设为 12(长度乘以加载因子)。
-
根据给定的键 key 进行哈希计算,得到哈希值。
-
通过对数组长度减 1 的与运算,得到插入的索引位置。
-
如果计算得到的索引位置对应的数组元素为空(即为 null),则直接将键值对插入该位置。
-
如果计算得到的索引位置对应的数组元素不为空,则比较键的 hash 值和键值是否相同。
- 如果相同,则说明是同一个键,直接替换对应的值。
- 如果不同,则判断当前数组元素是否为树结构。
-
如果当前数组元素是树,则按照红黑树的插入操作将键值对插入到树中。
-
如果当前数组元素不是树结构(即为链表),则按照链表的插入顺序将键值对插入到链表的尾部。
-
如果链表的长度达到阈值 8,且数组长度达到阈值 64,则将链表转换为红黑树。
-
如果键值对插入成功后,实际元素个数达到扩容阈值(即实际元素个数达到阈值大小的 75%),则触发扩容操作,对数组进行扩容。
需要注意的是,扩容操作会重新计算所有元素的索引位置,以保持哈希表的均匀性。
总结一下,putVal 方法会在 HashMap 的 put 操作中被调用来进行键值对的插入操作。它会根据传入的键值对和数组长度在数组中找到合适的位置进行插入。如果该位置已经存在元素,会根据情况进行替换、插入到链表或插入到红黑树中。当链表长度达到一定阈值且数组长度达到一定大小时,会将链表转换为红黑树。如果插入操作导致实际元素个数超过扩容阈值,会触发数组的扩容操作。
在 HashMap 中删除元素的过程如下:
-
根据要删除的键 key 进行哈希计算,找到对应的索引位置。
-
在该索引位置找到对应的元素,如果元素为 null,则表示没有要删除的元素,操作结束。
-
如果找到了元素,则根据情况进行删除操作。
-
如果该元素所在的位置是一个红黑树节点,先进行红黑树的删除操作,然后再进行后续处理。
-
如果该元素所在的位置是一个普通链表节点,根据节点个数判断是否需要进行转换:
- 如果链表节点个数小于等于 6,则不需要转换,直接在链表中删除该节点。
- 如果链表节点个数大于 6,则将链表转换为红黑树,并在红黑树中删除该节点。
-
如果删除操作成功,更新元素个数(size)。
需要注意的是,删除操作涉及到红黑树的平衡调整,以及链表和红黑树之间的转换。这些操作都是为了保持 HashMap 的性能和结构的平衡。
总结一下,删除元素时,会根据 key 找到对应的元素进行删除。如果是红黑树节点,则进行红黑树的删除操作;如果是链表节点,则根据节点个数判断是否需要转换为红黑树。删除操作完成后,会更新元素个数。
HashMap是线程安全吗?
在多线程环境下,HashMap 的 put 方法是不安全的,而 get 方法是安全的。
当多个线程同时对 HashMap 进行 put 操作时,可能会导致数据的不一致性或损坏。这是因为在 put 方法中,会涉及到数组的扩容、链表的转换、红黑树的插入等操作,这些操作可能会导致数据的冲突或覆盖。
相比之下,HashMap 的 get 方法是安全的。在多线程环境下,多个线程同时对 HashMap 进行 get 操作不会导致数据的冲突或损坏。get 方法只是简单地根据键进行查询操作,不会对数据结构进行修改。
为了在多线程环境下安全地使用 HashMap,可以使用线程安全的 ConcurrentHashMap,或者通过加锁来保证互斥访问。另外,也可以使用并发集合类,如 ConcurrentMap 接口的实现类,来替代 HashMap,以提供更好的线程安全性和并发性能。