HashMap是Java中最常用的数据结构之一,它允许将键值对存储在一个哈希表中,从而实现快速的查找和插入操作。在本篇博客中,我们将深入探讨HashMap的实现原理和性能优化方法。
目录
HashMap的实现原理
HashMap是一种基于哈希表实现的数据结构。它可以将键值对映射到一个数组中的某个位置,从而实现快速的插入和查找操作。HashMap的核心数据结构是一个数组,每个数组元素称为桶(bucket),每个桶中存储一个链表或红黑树。
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
HashMap其实也是用一个线性的数组实现的,所以可以理解为其存储数据的容器就是一个线性数组。但一个线性的数组怎么实现按键值对来存取数据呢?这里HashMap有做一些处理。
当我们向HashMap中插入一个键值对时,首先计算出该键值对的哈希值,然后将其对数组长度取模,得到该键值对应的桶的位置。如果该桶中已经存在元素,则将该元素添加到链表或红黑树的末尾;否则,将该元素作为链表或红黑树的头节点。
当我们需要查找一个键值对时,首先计算出该键的哈希值,然后将其对数组长度取模,得到该键值对应的桶的位置。然后遍历该桶对应的链表或红黑树,查找是否存在该键值对。如果找到了,则返回对应的值;否则,返回null。
HashMap的数据结构可以用以下伪代码表示:
class Entry {
Object key;
Object value;
Entry next;
}
class HashMap {
int capacity; // 数组长度
float loadFactor; // 负载因子
int size; // 元素数量
Entry[] table; // 数组
// ...
}
其中,Entry
表示HashMap中的一个键值对,capacity
表示数组长度,loadFactor
表示负载因子,size
表示元素数量,table
表示数组。每个Entry
对象包含了一个键、一个值和一个指向下一个Entry
对象的指针。当多个键映射到同一个桶时,它们会组成一个链表或红黑树。
当我们需要查找一个键值对时,首先计算出该键的哈希值,然后将其对数组长度取模,得到该键值对应的桶的位置。然后遍历该桶对应的链表,查找是否存在该键值对。如果找到了,则返回对应的值;否则,返回null。
HashMap的性能优化方法
1.初始容量和负载因子
在创建HashMap时,我们可以指定其初始容量和负载因子。初始容量指的是HashMap中桶的数量,负载因子指的是当HashMap中元素数量达到总容量的百分之多少时,需要对HashMap进行扩容操作。默认的初始容量为16,负载因子为0.75。如果我们预先知道HashMap中元素的数量,可以通过设置初始容量来减少扩容操作的次数,从而提高性能。
2.哈希函数
HashMap中使用的哈希函数是Java中的Object类中的hashCode()方法。然而,该方法并不是一个好的哈希函数,因为它很容易产生哈希冲突。为了提高HashMap的性能,我们可以自定义哈希函数,使其更加均匀地分布元素。
3.链表长度
当HashMap中某个桶中的链表长度过长时,查找元素的效率会大大降低。为了避免这种情况,我们可以通过调整负载因子来控制HashMap的容量和元素数量之间的比例,从而减少链表长度。当链表中Node节点的数量大于8并且数组的长度大于64时,链表会转换成一个红黑树,有利于查找搜索
public class Node {
public Node next;
private Object data;
public Node(Object data) {
this.data = data;
}
//链表:链表是一种物理存储单元上非连续、非顺序的存储结构
//特点:插入、删除时间复杂度O(1) 查找遍历时间复杂度0(N) 插入快 查找慢
public static void main(String[] args) {
Node node=new Node(15);
node.next=new Node(1);
node.next.next=new Node(9);
}
4.线程安全
HashMap在多线程环境下可能会出现线程安全问题。为了解决这个问题,我们可以使用ConcurrentHashMap类,它提供了一些线程安全的方法,如put()和rehash()。
ConcurrentHashMap的put方法源码如下:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
// 根据key的hash定位出一个segment,如果指定index的segment还没初始化,则调用ensureSegment方法初始化
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 调用segment的put方法
return s.put(key, hash, value, false);
}
ConcurrentHashMap在rehash的时候是有锁的,所以在rehash过程中,其他线程无法对segment的hash表做操作,这就保证了线程安全。
总结
HashMap 容量为2次幂的原因,就是为了数据的的均匀分布,减少hash冲突,毕竟hash冲突越大,代表数组中一个链的长度越大,这样的话会降低HashMap的性能。
HashMap是Java中最常用的数据结构之一,它的实现原理是通过哈希表实现的。为了提高HashMap的性能,我们可以通过调整初始容量和负载因子、自定义哈希函数、控制链表长度和使用线程安全的方法来优化HashMap的性能。