Java 集合学习笔记:HashMap - 迭代器

Posted 笑虾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合学习笔记:HashMap - 迭代器相关的知识,希望对你有一定的参考价值。

Java 集合学习笔记:HashMap - 迭代器

iterators

HashIterator 自己实现了 hasNextnextNoderemove 三个方法。(它没有实现 Iterator
因为是个抽象类,无法实例化,所以只能作为其他类的公共部分
三个子类 KeyIteratorValueIteratorEntryIterator 继承 HashIterator 并各自己实现自己的 next
共同实现了 Iterator 接口中的所有方法。

HashIterator

这段源码中亮点应该就是通过 if + do while 实现遍历的部分了吧。

abstract class HashIterator 
    Node<K,V> next;        // 下一次调用 nextNode 会返回的结点
    Node<K,V> current;     // 当前结点
    int expectedModCount;  // 预期修改次数。用于检测并发冲突
    int index;             // 当前索引
    
	// 初始化迭代器
    HashIterator() 
        expectedModCount = modCount; // 缓存修改次数
        Node<K,V>[] t = table;		 // 缓存哈希表
        current = next = null;		 // 当前、下一次将要返回的 entry 都置空
        index = 0;					 // 当前所在位置的索引放到 0
        // 如果哈希表不为空,先来到第一个结点
        // do-while 遍历哈希表,找到第一个结点,给 next。(它也是所在桶的第一个节点)
        if (t != null && size > 0)  // advance to first entry
            do  while (index < t.length && (next = t[index++]) == null);
        
    

hasNext

    public final boolean hasNext() 
        return next != null;
    

nextNode

    final Node<K,V> nextNode() 
        Node<K,V>[] t;
        Node<K,V> e = next;
        // 检测并发冲突
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // 检测将要返回的结点如果为空就抛锅。因为调用 nextNode() 前,我们通常是检测过 hasNext()的。
        if (e == null)
            throw new NoSuchElementException();
        // 1. 用即将返回的 e 更新 current 指针。
        // 2. if 这句就是在遍历链表
        // 2.1. 取出当前结点 current 的下一个结点给 next 指针。
        // 2.2. 如果 next 为 null 说明链表遍历完了。要是哈希表不为空,就继续遍历哈希表,找下一个桶。
        // 3. do-while 是遍历哈希表
        // 3.1. index 还没走到头,就一直取下一个索引对应的值来判断。找到非空的,就是 next 指针该有的值了。
        if ((next = (current = e).next) == null && (t = table) != null) 
            do  while (index < t.length && (next = t[index++]) == null);
        
        return e;
    

remove

	// remove 与 nextNode 成对调用。移除后 current 会被置 null
    public final void remove() 
    	// 拿到当前对象,并检测。如果为空抛锅。
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        // 并发冲突检测
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        // current 被置 null,在下次调用 nextNode 前都无法再使用 remove
        current = null;
        // 拿到 key 调用底层 removeNode 方法
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        // 同步修改次数
        expectedModCount = modCount;
    


KeyIterator

继承 HashIterator 重写 next 方法,返回具体的 key 值。

final class KeyIterator extends HashIterator implements Iterator<K> 
    public final K next()  return nextNode().key; 

ValueIterator

继承 HashIterator 重写 next 方法,返回具体的 value 值。

final class ValueIterator extends HashIterator implements Iterator<V> 
    public final V next()  return nextNode().value; 

EntryIterator

继承 HashIterator 重写 next 方法,原样返回结点 Node,只不过返回类型改为接口 Map.Entry
NodeMap.Entry 的实现类:static class Node<K,V> implements Map.Entry<K,V> ...

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> 
   public final Map.Entry<K,V> next()  return nextNode(); 

spliterators

  1. 可拆分迭代器的区间定义是 [左边界索引, 右边界索引) 右闭右开。
  2. 初始时如:[0, arr.length]

HashMapSpliterator

作为其他几个可拆分迭代器的基类,实现了公用的方法。

static class HashMapSpliterator<K,V> 
    final HashMap<K,V> map;		// 临时变量缓存将被拆分的 HashMap m
    Node<K,V> current;          // 当前结点(下一次将要处理的结点)
    int index;                  // 左边界[包含] current 的索引, 在 advance/split 中被更改
    int fence;                  // 右边界(不包含)
    int est;                    // 元素个数(不是精确值,是个估值)
    int expectedModCount;       // 预期修改次数。用于检测并发冲突

	// (map, 0, -1, 0, 0);
    HashMapSpliterator(HashMap<K,V> m, int origin,
                       int fence, int est,
                       int expectedModCount) 
        this.map = m;
        this.index = origin;
        this.fence = fence; // 初始化 -1
        this.est = est;
        this.expectedModCount = expectedModCount;
    

getFence 获取拆分器的右边界

获取拆分器的右边界。(如果还没有就先获取)

  1. 将一堆临时变量初始化赋值。(只有第一次调用时才会进入 if )
  2. 算出并返回右边界。
    final int getFence() 
        int hi;
        // fence 初始化 -1,所以第一次调用此方法是会进入 if
        if ((hi = fence) < 0) 
            HashMap<K,V> m = map;	// 取hashmap实例
            est = m.size;			// 获取元素个数
            expectedModCount = m.modCount;	// 同步计数:预期修改次数 = 修改次数
            Node<K,V>[] tab = m.table;	// 缓存哈希表
            hi = fence = (tab == null) ? 0 : tab.length; // 哈希表不为空返回长度。
        
        return hi;
    

estimateSize 估计剩余元素的个数

    public final long estimateSize() 
        getFence(); // 初始化
        return (long) est;
    

KeySpliterator

static final class KeySpliterator<K,V> 
	extends HashMapSpliterator<K,V> 
	implements Spliterator<K> 
	
    /**
     * @param m					将要被拆分的 HashMap
     * @param origin			左边界
     * @param fence				右边界
     * @param est				剩余元素个数
     * @param expectedModCount	预期修改次数
     */
    KeySpliterator(HashMap<K,V> m, int origin, int fence, int est, int expectedModCount) 
        super(m, origin, fence, est, expectedModCount);
    

1. trySplit 尝试拆分

尝试拆分,如果可拆分,创建一个新的拆分器并返回。
注意:是按哈希表拆分,并不理会桶中结点的个数。
除非每个桶中都平均分配元素,否则 est >>>= 1 算出来的元素个数,肯定是不准确的。

  1. 算出左、中、右三个位置的索引。
  2. 判断如果还能拆分,就劈成两半,用左半边 [左,中) 创建一个新拆分器返回。
  3. 当前拆分器更新一下左边界,新范围[中,右)
    public KeySpliterator<K,V> trySplit() 
    	// 获取右、左边界 hi 和 lo,算出中间位置 = (左 + 右) / 2
        int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
        // 1. 左边界 >= 中间,表示已无可再分了。
        // 2. 正常情况下 current 为空,原因见 tryAdvance 部分。
        // 3. 新拆分器的范围[左,中),当前拆分器的左更新为中[中,右)
        //    new 的同时更新当前拆分器 index,est
        //    est >>>= 1 位运算结果作为参数。一步搞定当前和新拆分器的 size
        return (lo >= mid || current != null) ? null 
        		: new KeySpliterator<>(
        			map, lo, index = mid, est >>>= 1, expectedModCount
        		);
    

2. forEachRemaining 遍历剩余元素

遍历每个剩余元素依次执行给定的操作 action,直到所有元素都处理完毕或该操作抛出异常。如果该SpliteratorORDERED,则按遇到顺序执行操作。动作抛出的异常被传递给调用者。

  1. 准备工作:声明临时变量。检测并发冲突。初始化。
  2. 哈希表有内容就进去遍历。消费每一个元素的 key
    public void forEachRemaining(Consumer<? super K> action) 
    	// i	临时变量用于缓存当前桶索引
    	// hi	临时变量用于缓存右边界
    	// mc	临时变量用于缓存预期修改次数
        int i, hi, mc;
        
        // 传进来的 Lambda 为空,直接抛锅。是个 null 让我怎么执行?
        if (action == null)
            throw new NullPointerException();
            
        HashMap<K,V> m = map;		// 临时变量缓存将被拆分的 HashMap。下面会用它来取 table、modCount
        Node<K,V>[] tab = m.table;	// 临时变量缓存此 hashmap 中的哈希表
        
        // 确保第一次调用时进入if。
        // 道理与 getFence 中 if ((hi = fence) < 0) 一样。 
        // 对比一下就能发现,这里是个简化了的 getFence() 
        if ((hi = fence) < 0) 
        	// 同步三个层级的:缓存预期修改次数
        	// 当前方法的 = 迭代器的 = hashMap的
            mc = expectedModCount = m.modCount;	
            hi = fence = (tab == null) ? 0 : tab.length;
        
        else
            mc = expectedModCount;
            
        // 符合条件就执行遍历
        // 1. 哈希表不为空,且长度 >= 右边界 (如果哈希表都没东西,就没必要遍历了)
        // 2. 左边界 >= 0 				   (索引值不可能 < 0)
        // 3. 当前索引 < 右边界 || 当前结点非空 
        //	(没有右边界,可以继续找剩余的桶,当前结点非空,可以消费之)
        if (tab != null && tab.length >= hi &&
            (i = index) >= 0 && (i < (index = hi) || current != null)) 
            
            Node<K,V> p = current;
            current = null;
            
            // 真正开始遍历工作
            do 
            	// 如果结点 p 为null,说明链表走到头了,去下一个桶。
            	// 否则:先消费一下 key,再继续下一个结点。
            	// 继续条件:
            	// p != null 表示当前有结,可以继续。
            	// i < hi	 表示还未到达右边界,可以继续。
                if (p == null) 
                    p = tab[i++];
                else 
                    action.accept(p.key);
                    p = p.next;
                
             while (p != null || i < hi); 
            
            // 检测并发冲突
            if (m.modCount != mc)
                throw new ConcurrentModificationException();
        
    

3. tryAdvance 如果还有元素就消费一个

如果存在剩余元素,则对其执行给定操作 action,返回true;否则返回false
如果该Spliterator为ORDERED,则操作将按遇到顺序对下一个元素执行。
action 抛出的异常被传递给调用者。
注意:只有 current == null,的情况下,才可以才分。因为它不为空,说明调用过 n 次 tryAdvance current 走到了链表中的某个结点。此时不允许拆分,只有处理完当前桶中所有节点,current 为 null 后才能拆分。

  1. 准备工作:声明临时变量。检测并发冲突。初始化。
  2. 哈希表非空,且长度 >= 右边界,且当前索引 >=0 遍历结点
  3. 找到下一个结果,找到就消费它。
    public boolean tryAdvance(Consumer<? super K> action) 
        int hi;
        // 传进来的 Lambda 为空,直接抛锅。是个 null 让我怎么执行?
        if (action == null)
            throw new NullPointerException();
            
        // 如果前面没报错,这里再声明临时变量拿哈希表。免得浪费资源。
        Node<K,V>[] tab = map.table;
        
        // 1. 哈希表不为空,且长度大于当前拆分器的右边界,且当前索引 >= 0
        if (tab != null && tab.length >= (hi = getFence()) && index >= 0) 
        	// current 不为空,则继续消费它。
        	// index < hi,表示当前还未走到右边界,可以继续。
            while (current != null || index < hi) 
            	// 如果 current 结点为 null,说明链表走到头了,去下一个桶看看。
            	// 否则,当前结点不为空继续遍历链表。
                if (current == null)
                    current = tab[index++]; // 取下一个桶的首结点。(同时还更新了index)
                else 
                    K k = current.key;		// 取出当前结点的 key
                    current = current.next; // 遍历到下一个结点。
                    action.accept(k);		// 消费 key
                    // 检测并发冲突
                    if (map.modCount != expectedModCount)
                        throw new ConcurrentModificationException();
                    return true; // 消费成功返回 true
                
            
        
        return false;
    

4. characteristics 返回特征

返回该Spliterator及其元素的一组特征。
这里用的是位运算|,8个特性每个特性占一位,可以组合。参考:《Java8实战》读书笔记06:Parallel Stream 并行流 - 表7-2 Spliterator的特性
在给定的分割器上重复调用characteristics(),在调用trySplit之前或中间,应该总是返回相同的结果。否则不能保证使用该Spliterator进行的任何计算。

public int characteristics() 
    return (fence < 0 || est == map.size ? Spliterator.SIZED : 0) |
        Spliterator.DISTINCT;

ValueSpliterator

逻辑与 KeySpliterator 基本相同,只是把消费对象从 key 改为了 value

static final class ValueSpliterator<K,V> 
	extends HashMapSpliterator<K,V> 
	implements Spliterator<V> 


    ValueSpliterator(HashMap<K,V> m, int origin, int fence, int est, int expectedModCount) 
        super(m, origin, fence, est, expectedModCount);
    

    public ValueSpliterator<K,V> trySplit() 
        int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
        return (lo >= mid || current != null) ? null :
            new ValueSpliterator<>(map, lo, index = mid, est >>>= 1,
                                      expectedModCount);
    

    public void forEachRemaining(Consumer<? super V> action) 
        int i, hi, mc;
        if (action == null)
            throw new NullPointerException();
        HashMap<K,V> m = map;
        Node<K,V>[] tab = m.table;
        if ((hi = fence) < 0) 
            mc = expectedModCount = m.modCount;
            hi = fence = (tab == null) ? 0 : tab.length;
        
        else
            mc = expectedModCount;
        if (tab != null && tab.length >= hi &&
            (i = index) >= 0 && (i < (index = hi以上是关于Java 集合学习笔记:HashMap - 迭代器的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合学习笔记:IterableIterator

Java 集合学习笔记:IterableIterator

Java 集合学习笔记:IterableIterator

9.0对于java集合的迭代器的底层分析

Java学习笔记5.2.3 List接口 - 遍历集合

Java 集合学习笔记:HashMap