注册

为什么 Java 大佬都不推荐使用 keySet() 遍历HashMap?

在Java编程中,HashMap 是一种非常常见的数据结构。我们经常需要对其中的键值对进行遍历。通常有多种方法可以遍历 HashMap,其中一种方法是使用 keySet() 方法。


然而,很多Java大佬并不推荐这种方法。为什么呢?


已收录于,我的技术网站:ddkk.com 里面有,500套技术系列教程、1万+道,面试八股文、BAT面试真题、简历模版,工作经验分享、架构师成长之路,等等什么都有,欢迎收藏和转发。


keySet() 方法的工作原理


首先,让我们来看一下 keySet() 方法是如何工作的。keySet() 方法返回 HashMap 中所有键的集合 (Set<K>)。然后我们可以使用这些键来获取相应的值。


代码示例如下:


// 创建一个HashMap并填充数据
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

// 使用keySet()方法遍历HashMap
for (String key : map.keySet()) {
// 通过键获取相应的值
Integer value = map.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}

这个代码看起来没什么问题,但在性能和效率上存在一些隐患。


keySet() 方法的缺点


1、 多次哈希查找:如上面的代码所示,使用 keySet() 方法遍历时,需要通过键去调用 map.get(key) 方法来获取值。这意味着每次获取值时,都需要进行一次哈希查找操作。如果 HashMap 很大,这种方法的效率就会明显降低。


2、 额外的内存消耗keySet() 方法会生成一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图,但仍然需要额外的内存开销来维护这个集合的结构。如果 HashMap 很大,这个内存开销也会变得显著。


3、 代码可读性和维护性:使用 keySet() 方法的代码可能会让人误解,因为它没有直接表现出键值对的关系。在大型项目中,代码的可读性和维护性尤为重要。


更好的选择:entrySet() 方法


相比之下,使用 entrySet() 方法遍历 HashMap 是一种更好的选择。entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry<K, V>>)。通过遍历这个集合,我们可以直接获取每个键值对,从而避免了多次哈希查找和额外的内存消耗。


下面是使用 entrySet() 方法的示例代码:


// 创建一个HashMap并填充数据
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

// 使用entrySet()方法遍历HashMap
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// 直接获取键和值
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}

entrySet() 方法的优势


1、 避免多次哈希查找:在遍历过程中,我们可以直接从 Map.Entry 对象中获取键和值,而不需要再次进行哈希查找,提高了效率。


2、 减少内存消耗entrySet() 方法返回的是 HashMap 内部的一个视图,不需要额外的内存来存储键的集合。


3、 提高代码可读性entrySet() 方法更直观地表现了键值对的关系,使代码更加易读和易维护。


性能比较


我们来更深入地解析性能比较,特别是 keySet()entrySet() 方法在遍历 HashMap 时的性能差异。


主要性能问题


1、 多次哈希查找: 使用 keySet() 方法遍历 HashMap 时,需要通过键调用 map.get(key) 方法获取值。这意味着每次获取值时都需要进行一次哈希查找操作。哈希查找虽然时间复杂度为 O(1),但在大量数据下,频繁的哈希查找会累积较高的时间开销。


2、 额外的内存消耗keySet() 方法返回的是一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图,但仍然需要额外的内存来维护这个集合的结构。


更高效的选择:entrySet() 方法


相比之下,entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry<K, V>>)。通过遍历这个集合,我们可以直接获取每个键值对,避免了多次哈希查找和额外的内存消耗。


性能比较示例


让我们通过一个具体的性能比较示例来详细说明:


import java.util.HashMap;
import java.util.Map;

public class HashMapTraversalComparison {
public static void main(String[] args) {
// 创建一个大的HashMap
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, i);
}

// 测试keySet()方法的性能
long startTime = System.nanoTime(); // 记录开始时间
for (String key : map.keySet()) {
Integer value = map.get(key); // 通过键获取值
}
long endTime = System.nanoTime(); // 记录结束时间
System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");

// 测试entrySet()方法的性能
startTime = System.nanoTime(); // 记录开始时间
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey(); // 直接获取键
Integer value = entry.getValue(); // 直接获取值
}
endTime = System.nanoTime(); // 记录结束时间
System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");
}
}

深度解析性能比较示例


1、 创建一个大的 HashMap



Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, i);
}


  • 创建一个包含100万个键值对的 HashMap
  • "key" + ii
  • 这个 HashMap 足够大,可以明显展示两种遍历方法的性能差异。

2、 测试 keySet() 方法的性能


long startTime = System.nanoTime(); // 记录开始时间
for (String key : map.keySet()) {
Integer value = map.get(key); // 通过键获取值
}
long endTime = System.nanoTime(); // 记录结束时间
System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");


  • 使用 keySet() 方法获取所有键,并遍历这些键。
  • 在每次迭代中,通过 map.get(key) 方法获取值。
  • 记录开始时间和结束时间,计算遍历所需的总时间。

3、 测试 entrySet() 方法的性能


startTime = System.nanoTime(); // 记录开始时间
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey(); // 直接获取键
Integer value = entry.getValue(); // 直接获取值
}
endTime = System.nanoTime(); // 记录结束时间
System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");


  • 使用 entrySet() 方法获取所有键值对,并遍历这些键值对。
  • 在每次迭代中,直接从 Map.Entry 对象中获取键和值。
  • 记录开始时间和结束时间,计算遍历所需的总时间。

性能结果分析


假设上述代码的运行结果如下:


keySet() 方法遍历时间: 1200000000 纳秒
entrySet() 方法遍历时间: 800000000 纳秒

可以看出,使用 entrySet() 方法的遍历时间明显短于 keySet() 方法。这主要是因为:


1、 避免了多次哈希查找: 使用 keySet() 方法时,每次获取值都需要进行一次哈希查找。而使用 entrySet() 方法时,键和值直接从 Map.Entry 对象中获取,无需再次查找。


2、 减少了内存消耗: 使用 keySet() 方法时,额外生成了一个包含所有键的集合。而使用 entrySet() 方法时,返回的是 HashMap 内部的一个视图,无需额外的内存开销。


小结一下


通过性能比较示例,我们可以清楚地看到 entrySet() 方法在遍历 HashMap 时的效率优势。使用 entrySet() 方法不仅能避免多次哈希查找,提高遍历效率,还能减少内存消耗。


综上所述,在遍历 HashMap 时,entrySet() 方法是更优的选择。


几种高效的替代方案


除了 entrySet() 方法外,还有其他几种高效的替代方案,可以用于遍历 HashMap


以下是几种常见的高效替代方案及其优缺点分析:


1. 使用 entrySet() 方法


我们已经讨论过,entrySet() 方法是遍历 HashMap 时的一个高效选择。它直接返回键值对的集合,避免了多次哈希查找,减少了内存开销。


import java.util.HashMap;
import java.util.Map;

public class EntrySetTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}

2. 使用 forEach 方法


从 Java 8 开始,Map 接口提供了 forEach 方法,可以直接对每个键值对进行操作。这种方式利用了 lambda 表达式,代码更简洁,可读性强。


import java.util.HashMap;
import java.util.Map;

public class ForEachTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

map.forEach((key, value) -> {
System.out.println("Key: " + key + ", Value: " + value);
});
}
}

3. 使用 iterator 方法


另一种遍历 HashMap 的方法是使用迭代器 (Iterator)。这种方法适用于需要在遍历过程中对集合进行修改的情况,比如删除某些元素。


import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class IteratorTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}

4. 使用 Streams API


Java 8 引入了 Streams API,可以结合 stream() 方法和 forEach 方法来遍历 HashMap。这种方法可以对集合进行更复杂的操作,比如过滤、映射等。


import java.util.HashMap;
import java.util.Map;

public class StreamTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);

map.entrySet().stream().forEach(entry -> {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
});
}
}

优缺点分析


entrySet() 方法



  • 优点:避免多次哈希查找,减少内存消耗,代码简单明了。
  • 缺点:没有特定缺点,在大多数情况下是最佳选择。

forEach 方法



  • 优点:代码简洁,可读性强,充分利用 lambda 表达式。
  • 缺点:仅适用于 Java 8 及以上版本。

iterator 方法



  • 优点:适用于需要在遍历过程中修改集合的情况,如删除元素。
  • 缺点:代码稍显繁琐,不如 entrySet()forEach 方法直观。

Streams API 方法



  • 优点:支持复杂操作,如过滤、映射等,代码简洁。
  • 缺点:仅适用于 Java 8 及以上版本,性能在某些情况下可能不如 entrySet()forEach

结论


在遍历 HashMap 时,entrySet() 方法是一个高效且广泛推荐的选择。对于更现代的代码风格,forEach 方法和 Streams API 提供了简洁且强大的遍历方式。如果需要在遍历过程中修改集合,可以使用 iterator 方法。根据具体需求选择合适的遍历方法,可以显著提高代码的效率和可读性。


已收录于,我的技术网站:ddkk.com 里面有,500套技术系列教程、1万+道,面试八股文、BAT面试真题、简历模版,工作经验分享、架构师成长之路,等等什么都有,欢迎收藏和转发。


作者:架构师专栏
来源:juejin.cn/post/7393663398406799372

0 个评论

要回复文章请先登录注册