在 Java 开发中,HashMap 是我们经常使用的数据结构之一。然而,很多开发者可能没有意识到合理设置 HashMap 的初始值大小对于性能的重要性。在这篇博客中,我们将深入探讨如何做出明智的选择,以优化 HashMap 的性能。
一、HashMap 的工作原理
在深入探讨初始值大小之前,让我们先简要回顾一下 HashMap 的工作原理。HashMap 基于哈希表实现,通过计算键的哈希值来确定元素在内部数组中的存储位置。当发生哈希冲突时,会采用链表或红黑树的方式来解决冲突。
数组是一块连续的内存区域,一经初始化,其大小便无法更改。而数组作为 HashMap 的底层数据结构,当数据量超出数组长度,若仍要向其中存储数据,就需对数组进行“扩容”操作。所谓扩容,即重新开辟一片更长的内存空间作为当前 HashMap 的底层数据结构,并将原有数据复制至新空间。
由此可见,HashMap 的扩容会给程序造成性能上的损失。
二、初始值大小的影响
HashMap 的默认初始容量为 16,负载因子为 0.75。这意味着当 HashMap 中的元素数量达到 16 * 0.75 = 12 时,它会自动进行扩容操作。扩容是一个相对昂贵的操作,因为它需要重新计算所有元素的哈希值,并将它们重新分配到新的更大的内部数组中。
如果我们在使用 HashMap 时能够提前估计元素的数量,并合理设置初始值大小,就可以减少扩容操作的次数,从而提高性能。
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
从上面的注释中可以看出,在调用无参的构造方法时,HashMap 默认的容量是 16,且 loadFactor 的值为 0.75。
三、如何合理设置初始值大小
- 估计元素数量
首先,我们需要对要存储在 HashMap 中的元素数量有一个大致的估计。如果估计不准确,也可以稍微高估一些,以避免频繁扩容。 - 计算合适的初始容量
假设我们估计要存储的元素数量为 n,为了减少扩容次数,我们可以将初始容量设置为大于 n / 0.75 的最近的 2 的幂次方数。例如,如果预计存储 100 个元素,那么合适的初始容量可以计算为:
intcapacity= (int) Math.ceil(100 / 0.75);
capacity = Integer.highestOneBit(capacity - 1) << 1;
在上述代码中,Math.ceil 用于向上取整,Integer.highestOneBit 用于获取最高位为 1 的位置,然后左移一位得到 2 的幂次方数。
四、示例代码
- 如何设置初始值大小
import java.util.HashMap;
public class HashMapCapacityExample {
public static void main(String[] args) {
// 估计要存储 200 个元素
int estimatedElementCount = 200;
HashMap<String, Integer> myHashMap = new HashMap<>(calculateInitialCapacity(estimatedElementCount));
// 向 HashMap 中添加元素
//...
}
public static int calculateInitialCapacity(int estimatedElementCount) {
int capacity = (int) Math.ceil(estimatedElementCount / 0.75);
capacity = Integer.highestOneBit(capacity - 1) << 1;
return capacity;
}
}
- HashMap在扩容中实际情况
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
public class HashMapCapacityExample {
public static void main(String[] args) throws Exception {
HashMap<Integer, Integer> hashMap = new HashMap<>();
Class<?> mapType = hashMap.getClass();
//尝试从获取到的类对象中获取名为 threshold 的字段。getDeclaredField 方法可以获取到类中声明的字段,包括私有字段。
Field threshold = mapType.getDeclaredField("threshold");
//由于 threshold 字段可能是私有的,通过设置其可访问性为 true ,以便后续能够对其进行操作。
threshold.setAccessible(true);
//尝试从类对象中获取名为 capacity 的方法。
Method capacity = mapType.getDeclaredMethod("capacity");
//将获取到的 capacity 方法设置为可访问,即使它可能是私有的,以便后续能够调用该方法。
capacity.setAccessible(true);
//打印刚初始化的HashMap的容量、阈值和元素数量
System.out.println("容量:"+capacity.invoke(hashMap)+" 阈值:"+threshold.get(hashMap)+" 元素数量:"+hashMap.size());
for (int i = 0;i<17;i++){
hashMap.put(i,i);
//动态监测HashMap的容量、阈值和元素数量
System.out.println("容量:"+capacity.invoke(hashMap)+" 阈值:"+threshold.get(hashMap)+" 元素数量:"+hashMap.size());
}
}
}
运行结果:
容量:16 阈值:0 元素数量:0
容量:16 阈值:12 元素数量:1
容量:16 阈值:12 元素数量:2
容量:16 阈值:12 元素数量:3
容量:16 阈值:12 元素数量:4
容量:16 阈值:12 元素数量:5
容量:16 阈值:12 元素数量:6
容量:16 阈值:12 元素数量:7
容量:16 阈值:12 元素数量:8
容量:16 阈值:12 元素数量:9
容量:16 阈值:12 元素数量:10
容量:16 阈值:12 元素数量:11
容量:16 阈值:12 元素数量:12
容量:32 阈值:24 元素数量:13
容量:32 阈值:24 元素数量:14
容量:32 阈值:24 元素数量:15
容量:32 阈值:24 元素数量:16
容量:32 阈值:24 元素数量:17
五、总结
合理设置 HashMap 的初始值大小是优化性能的一个重要环节。通过准确估计元素数量,并根据计算规则设置合适的初始容量,可以减少扩容操作带来的性能开销,提高程序的运行效率。在实际开发中,我们应该根据具体的业务场景和数据规模,灵活运用这一技巧,以提升 HashMap 的性能表现。
希望这篇博客能帮助您更好地理解和应用 HashMap 的初始值大小设置,让您的代码更加高效和出色!