实在没想到系列——HashMap实现底层细节之keySet,values,entrySet的一个底层实现细节

本文深入探讨了HashMap中keySet、values及entrySet的实现原理,特别是keySet如何通过内部迭代器机制与HashMap同步更新。

转载自:实在没想到系列——HashMap实现底层细节之keySet,values,entrySet的一个底层实现细节

我在看HashMap源码的时候发现了一个没思考过的问题,在这次之前可以说是完全没有思考过,所以一开始对这个点有疑问的时候,也没有想到居然有这么个语法细节存在,弄得我百思不得其解,直到自己动手做实验改写了代码才完全明白。

HashMap里面保存的数据最底层是一个Entry型的数组,这个Entry则保留了一个键值对,还有一个指向下一个Entry的指针。所以HashMap是一种结合了数组和链表的结构。正因为如此,你有3种对数据的观测方式:keySet,values,entrySet。第一个是体现从key的值角度出发的结果。它里面包含了这个键值对表里面的所有键的值的集合,因为HashMap明确规定一个键只能对应一个值,所以不会有重复的key存在,这也就是为什么可以用集合来装key。第二个values则是从键值对的值的角度看这个映射表,因为可以有多个key对应一个值,所以可能有多个相同的values。(这个观点和函数的观点相似)第三个角度是最基本的角度,也就是从键值对的角度思考这个问题。它返回一个键值对的集合。(键值对相等当且仅当键和值都相等)。

以上是大致的理解。在此基础上,java的源码:(这里我只用了keySet说明这个问题)

 1     public Set<K> keySet() {
 2         Set<K> ks = keySet;
 3         return (ks != null ? ks : (keySet = new KeySet()));
 4     }
 5 
 6     private final class KeySet extends AbstractSet<K> {
 7         public Iterator<K> iterator() {
 8             return newKeyIterator();
 9         }
10         public int size() {
11             return size;
12         }
13         public boolean contains(Object o) {
14             return containsKey(o);
15         }
16         public boolean remove(Object o) {
17             return HashMap.this.removeEntryForKey(o) != null;
18         }
19         public void clear() {
20             HashMap.this.clear();
21         }
22     }

看上去简单明了,可是我发现了一个细节并且与之纠缠了一个下午(这个语法细节隐藏的很深)。

这个地方我们可以看到,当向一个HashMap调用keySet()方法的时候就是返回一个集合,其内容是所有的key的值。可是问题是这个地方到底是怎么实现的。从代码可以看到这个地方直接返回了一个叫keySet的东西。那么这个东西究竟是什么呢?按住command键可以直接去看这个变量声明的地方:

在AbstractMap.class里面:

1     transient volatile Set<K>        keySet = null;
2     transient volatile Collection<V> values = null;

也就是说,这个地方是从HashMap的父类AbstractMap里面继承过来的两个集合类型(第一个就是我说的keySet,第二个和这个完全一样的过程)。

可是问题还是没有解决,这个keySet为什么能返回当前HashMap的key的值得集合呢?我一开始只是抱着“简单看看”的想法来看这个地方,因为我的想象是可能能在哪里找到一个显而易见的同步方法,使得keySet的里面的值随着table(这也就是那个基础数组,储存了所有的键值对Entry)的值变化而变化。可是我发现:“没有”。

第一时间我觉得我可能没有找对位置,因为一般它提供的这些类的继承关系比较复杂,可能不在这个地方,可能在别的地方实现了,可是我翻来覆去找半天确实发现没有,也就是说:“没有明确的代码让keySet同步HashMap”。这下问题就变大了,事实上如果你在AbstractMap里面找只找得到如下代码:

 1     public Set<K> keySet() {
 2         if (keySet == null) {
 3             keySet = new AbstractSet<K>() {
 4                 public Iterator<K> iterator() {
 5                     return new Iterator<K>() {
 6                         private Iterator<Entry<K,V>> i = entrySet().iterator();
 7 
 8                         public boolean hasNext() {
 9                             return i.hasNext();
10                         }
11 
12                         public K next() {
13                             return i.next().getKey();
14                         }
15 
16                         public void remove() {
17                             i.remove();
18                         }
19                     };
20                 }
21 
22                 public int size() {
23                     return AbstractMap.this.size();
24                 }
25 
26                 public boolean isEmpty() {
27                     return AbstractMap.this.isEmpty();
28                 }
29 
30                 public void clear() {
31                     AbstractMap.this.clear();
32                 }
33 
34                 public boolean contains(Object k) {
35                     return AbstractMap.this.containsKey(k);
36                 }
37             };
38         }
39         return keySet;
40     }

看上去完全不是一个同步过程,至少在我的理解中把一个容器的东西搬运到另外一个容器需要用循环把东西一个一个搬运过去,哪怕只是浅拷贝把指针的值丢过去。这一节代码怎么看都和“让keySet这个set持有table里面的key的值的集合”没有任何关系。但是确确实实是这个地方实现了同步。

看如下代码:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4 
 5         testIterator t = new testIterator();
 6         Set<Integer> set = t.keySet();
 7         System.out.println(set);
 8 
 9     }
10 }
11 
12 
13 class testIterator {
14     public Set<Integer> keySet() {
15 
16         final ArrayList<Integer> result = new ArrayList<Integer>();
17         result.add(1);
18         result.add(2);
19         result.add(3);
20 
21         Set<Integer> keySet = new AbstractSet<Integer>() {
22             public Iterator<Integer> iterator() {
23                 return new Iterator<Integer>() {
24                     private Iterator<Integer> i = result.iterator();
25 
26                     @Override
27                     public boolean hasNext() {
28                         return i.hasNext();
29                     }
30 
31                     @Override
32                     public Integer next() {
33                         return i.next();
34                     }
35 
36                     @Override
37                     public void remove() {
38                         i.remove();
39                     }
40                 };
41             }
42 
43             @Override
44             public int size() {
45                 return 0;
46             }
47         };
48 
49         return keySet;
50     }
51 }

这个地方的结果是:

[1, 2, 3]

为什么呢?这个地方的代码是按照HashMap的代码改写的,我再改写一下如下所示:

 1 public class Main {
 2 
 3     public static void main(String[] args) {
 4         ArrayList<Integer> array = new ArrayList<Integer>();
 5         array.add(1);
 6         array.add(2);
 7         array.add(3);
 8         
 9         mySet set = new mySet(array.iterator());
10         System.out.println(set);
11     }
12 
13 }
14 
15 class mySet extends AbstractSet<Integer> {
16 
17     private Iterator<Integer> iter;
18 
19     public mySet(Iterator<Integer> i) {
20         iter = i;
21     }
22 
23     @Override
24     public Iterator<Integer> iterator() {
25         return iter;
26     }
27 
28     @Override
29     public int size() {
30         return 0;
31     }
32 
33 }

也是一样的效果。换句话说,直接让一个set它持有一个别人的Iterrator,它会认为自己是它。同时如果调试运行会发现set的值真的变了。同时这么做是有问题的,调试运行的结果和直接运行不一样同时再加上一句:System.out.println(set); 会发现第一次打印了1,2,3,第二次为null。换句话说这样的代码产生了不确定的行为。但是这代码可以说明一些问题,至少表示离问题近了。

到目前为止,可以知道keySet返回的并不是个“新”的东西,所以也没有把HashMap里面的key的值一个一个放到set的这个过程,而是通过生成一个set,这个set直接和HashMap的Iterator挂钩来反映HashMap的变化。这个地方的“挂钩”的具体过程是keySet继承了AbstractSet这个抽象类,这个抽象类需要重写iterator() 方法。

具体的代码调用过程如下:

当你调用HashMap的keySet()方法的时候:

 1 public Set<K> keySet() {
 2         Set<K> ks = keySet;
 3         return (ks != null ? ks : (keySet = new KeySet()));
 4     }
 5 
 6     private final class KeySet extends AbstractSet<K> {
 7         public Iterator<K> iterator() {
 8             return newKeyIterator();
 9         }
10         public int size() {
11             return size;
12         }
13         public boolean contains(Object o) {
14             return containsKey(o);
15         }
16         public boolean remove(Object o) {
17             return HashMap.this.removeEntryForKey(o) != null;
18         }
19         public void clear() {
20             HashMap.this.clear();
21         }
22     }

可见:会返回一个名字叫keySet的Set。但是这个keySet如上面所写的是来自AbstractMap的一个引用。我前面思路错的原因是因为我一直认为需要去AbstractMap里面找它的具体实现,其实不是的。这个ks的第一次初始化就反映了问题的本质是通过引用。看它的初始化过程:返回了一个“newKeyIterator();”对象。那么这个对象是什么呢?

再往前的代码:

1     Iterator<K> newKeyIterator()   {
2         return new KeyIterator();
3     }

它调用了一个方法返回了一个 KeyIterator 对象。这个对象的代码如图所示:

1     private final class KeyIterator extends HashIterator<K> {
2         public K next() {
3             return nextEntry().getKey();
4         }
5     }

它又基础自HashIterator。看上去这个过程比较复杂,其实看源代码的话可以很清楚它的意图:keySet和values和entrySet本质既然一样,就可以通过封装其相同的部分(也就是这里的HashIterator),再各自实现最重要的next方法。

这是HashIterator的源代码:

 1     private abstract class HashIterator<E> implements Iterator<E> {
 2         Entry<K,V> next;        // next entry to return
 3         int expectedModCount;   // For fast-fail
 4         int index;              // current slot
 5         Entry<K,V> current;     // current entry
 6 
 7         HashIterator() {
 8             expectedModCount = modCount;
 9             if (size > 0) { // advance to first entry
10                 Entry[] t = table;
11                 while (index < t.length && (next = t[index++]) == null)
12                     ;
13             }
14         }
15 
16         public final boolean hasNext() {
17             return next != null;
18         }
19 
20         final Entry<K,V> nextEntry() {
21             if (modCount != expectedModCount)
22                 throw new ConcurrentModificationException();
23             Entry<K,V> e = next;
24             if (e == null)
25                 throw new NoSuchElementException();
26 
27             if ((next = e.next) == null) {
28                 Entry[] t = table;
29                 while (index < t.length && (next = t[index++]) == null)
30                     ;
31             }
32             current = e;
33             return e;
34         }

可见,对于迭代器的操作,其实都是根据底层的table来实现的,也就是直接操作键值对。在得到Entry之后再获得它的key或者value。正因为如此,迭代器的底层直接根据table进行操作,所以如果有别的容器持有了这个迭代器内部类,就可以直接实现同步中的可见性:对HashMap的改变体现在table,而传递出去的内部类可以访问table。

而这之所以可以实现的更底层一步的地方是迭代器的具体实现。一方面它是一个内部类可以直接访问HashMap的table,另外一个方面是它用了类似指针的next引用,也就可以实现迭代。这种暴露一个内部类来实现外部访问的方式我还真是第一次具体见到。

到这里我们就可以明白这整个过程了。

<think>我们被要求详细讲解Java中遍历HashMap的三种方式:keySet(), entrySet(), values()。同时,我们需要参考提供的引用内容,并按照要求进行回答。 引用[1]展示了使用keySet()遍历HashMap的示例。 引用[2]提到了五种方法,但我们只需要关注三种:keySet(), entrySet(), values()(另外两种是迭代器和Lambda表达式等)。 引用[3]展示了使用entrySet()、keySet()和values()三种方式遍历HashMap的示例。 引用[4]则从源码角度解释了entrySetkeySetvalues。 因此,我们的回答将围绕这三种方式展开,并参考引用内容。 回答结构: 1. 介绍HashMap遍历的三种方式。 2. 分别详细讲解每种方式,包括代码示例和说明。 3. 比较三种方式的优缺点和适用场景。 4. 根据要求,生成相关问题。 注意:在代码示例中,我们将使用引用中的示例,并适当调整和解释。</think>### Java 遍历 HashMap 的三种方式详解 在 Java 中遍历 `HashMap` 主要有三种核心方式,分别通过 `keySet()`、`entrySet()` 和 `values()` 方法实现。每种方式适用于不同场景,下面结合代码示例详细说明: --- #### 1. **keySet() 方式** 通过获取键集合遍历,再根据键获取值 **适用场景**:需要单独处理键或键值对分离操作的场景 ```java Map<String, Integer> map = new HashMap<>(); map.put("Alice", 30); map.put("Bob", 25); // 遍历键集合 for (String key : map.keySet()) { Integer value = map.get(key); // 根据键获取值 System.out.println("Key: " + key + ", Value: " + value); } ``` **输出**: ``` Key: Alice, Value: 30 Key: Bob, Value: 25 ``` **特点**: - 需二次查询:通过 `map.get(key)` 获取值,效率较低(时间复杂度 $O(1)$ 但需两次哈希计算)[^1] - 适合键处理:如过滤特定键或修改键关联值 - **缺点**:遍历过程不能直接修改键(会导致 `ConcurrentModificationException`) --- #### 2. **entrySet() 方式** 直接获取键值对集合遍历(**推荐方式**) **适用场景**:需要同时访问键和值的场景(最高效) ```java for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); Integer value = entry.getValue(); System.out.println(entry); // 直接输出键值对 } ``` **输出**: ``` Alice=30 Bob=25 ``` **特点**: - 最高效:一次遍历同时获取键值,无需二次查询(时间复杂度 $O(1)$)[^3] - 支持修改:可通过 `entry.setValue()` 安全修改值 - 源码原理:`entrySet()` 返回内部 `EntrySet` 视图,直接操作底层数组[^4] --- #### 3. **values() 方式** 仅遍历值集合 **适用场景**:只需操作值的场景(如统计、聚合) ```java for (Integer value : map.values()) { System.out.println("Value: " + value); } ``` **输出**: ``` Value: 30 Value: 25 ``` **特点**: - 最简洁:忽略键信息,代码更简洁 - 高效取值:直接访问值数组(时间复杂度 $O(1)$)[^4] - **局限性**:无法获取对应键的信息 --- ### 三种方式对比总结 | **方法** | **获取内容** | **效率** | **适用场景** | |---------------|-------------|----------|----------------------------------| | `keySet()` | 键 → 值 | 较低 | 需单独处理键 | | `entrySet()` | 键 + 值 | 最高 | 同时操作键值对(推荐) | | `values()` | 值 | 高 | 仅需操作值(如求和/过滤) | > **最佳实践**: > - 优先使用 `entrySet()`(90%场景适用) > - 只关心值时用 `values()` > - 避免在循环中多次调用 `map.get(key)`(改用 `entrySet()`) --- ### 完整示例代码 ```java import java.util.*; public class HashMapTraversal { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("Alice", 30); map.put("Bob", 25); map.put("Charlie", 35); // 1. keySet() 遍历 System.out.println("=== keySet() ==="); for (String key : map.keySet()) { System.out.println(key + " : " + map.get(key)); } // 2. entrySet() 遍历 System.out.println("\n=== entrySet() ==="); for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + " => " + entry.getValue()); } // 3. values() 遍历 System.out.println("\n=== values() ==="); int sum = 0; for (int value : map.values()) { sum += value; System.out.println("Value: " + value); } System.out.println("Total: " + sum); } } ``` **输出**: ``` === keySet() === Alice : 30 Bob : 25 Charlie : 35 === entrySet() === Alice => 30 Bob => 25 Charlie => 35 === values() === Value: 30 Value: 25 Value: 35 Total: 90 ``` 通过合理选择遍历方式,可显著提升 HashMap 操作效率[^2][^3]。对于 Java 8+ 项目,还可结合 `forEach` + Lambda 表达式实现更简洁的遍历(如 `map.forEach((k,v) -> ...)`)[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值