聊聊java中的哪些Map:(三)HashMap中的Iterator和Spliterator

对于迭代器模式,相信大家都不是很陌生,在HashMap中也很好的实现了迭代器模式。同时,HashMap还有一个更具特色的Spliterator。本文对着两者的源码进行分析。

1.Iterator

HashMap中全部的迭代器都继承了抽象类HashIterator.

1.1 HashIterator

HashIterator是HashMap种所有迭代器的基类。通过抽象类的方式实现了大多数方法,如hashnext、nextNode、remove等。但是需要注意的是,HashIterator并没用实现Iterator接口。而是各自的迭代器在各自实现这个接口。这样一来通过抽象类的方式将公共的功能呢进行了提取。这也是我们在写代码的过程中值得借鉴的地方。

abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot
   
    //空构造方法
    HashIterator() {
        //modcout的次数
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }
    //是否有下一个元素 链表非常好实现
    public final boolean hasNext() {
        return next != null;
    }
    //下一个元素节点
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }
    //移除当前节点
    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        K key = p.key;
        removeNode(hash(key), key, null, false, false);
        expectedModCount = modCount;
    }
}

1.2 Iterator实现类

我们再来看看Interator的实现类:

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

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

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

可以看到这样一来代码相当简洁。此时对于key、value和entry,之后具体迭代的时候iterator方法不一样。而在调用next的时候Node为TreeNode的父类,在此也可以通用。
另外由于抽象类中增加了forEach方法,那么可以配合lambda表达式使用。

System.out.println(map.keySet().iterator().next());
map.entrySet().forEach(e -> {
    System.out.println(e);
});

2.Spliterator

Spliterator是java1.8新增的接口,即为splitable iterator的意思,接口是java为了并行遍历数据源中的元素而设计的。与iterator相比,一个是顺序遍历,一个是将数据拆分为若干部分之后进行并行遍历。以配合Stream的并行流。
Spliterator的一个特点是每次将元素拆分出去一半。对于HashMap,由于hashMap底层是链表,如果要完全精确到元素,势必会造成算法的复杂和性能的低下。因此,Spliterator在HashMap的实现过程中,直接是按bucket进行处理,这样会导致每次拆分的数据并不均匀。HashMap中实际上是按照bucket的数量平均拆分,只是可能每个bucket上面Node的数量可能不一致,另外有的bucket可能为空。我们来通过源码查看这个过程。

2.1 Spliterator的使用

我们以keySpliterator为例子进行验证。

private static void testMap() {
    Map<Integer,Integer> map = new HashMap<>();
    IntStream.range(0,20).forEach(i -> map.put(i,i));
    System.out.println(map.size());
    Spliterator<Integer> s1 = map.keySet().spliterator();
    Spliterator<Integer> s2 = s1.trySplit();

    s1.forEachRemaining(i -> System.out.println("s1:"+i));
    System.out.println("===============");
    s2.forEachRemaining(i -> System.out.println("s2:"+i));
    System.out.println("===============");

}

上述代码执行结果如下:

20
s1:16
s1:17
s1:18
s1:19
===============
s2:0
s2:1
s2:2
s2:3
s2:4
s2:5
s2:6
s2:7
s2:8
s2:9
s2:10
s2:11
s2:12
s2:13
s2:14
s2:15
===============

我们可以看到,在KeySpliterator过程中,将KeySet分为了两部分,其中一部分有16个值,另外一部分为4个。前面说过Spliterator是按bucket进行区分,当有20个元素的时候,bucket的数量为32,那么Spliterator实际上一次分除来的是16个bucket,但是由于hashMap中数据分布的原因,这样就造成了上述情况。

2.2 HashMapSpliterator

HashMapSpliterator是一个基类,keySpliterator和ValueSpiterator、EntrySpliterator都继承了这个类,不过这里有一点我还不太明白的就是,与HashIterator相比,为什么HashIterator是abstrat的抽象类,HashIterator里面也没有定义抽象方法。

2.2.1 成员变量

//需要拆分的hashMap
final HashMap<K,V> map;
//当前节点
Node<K,V> current;          // current node
//bucket的index
int index;                  // current index, modified on advance/split
//计算出的fence
int fence;                  // one past last index
//
int est;                    // size estimate
//期望的modCount 
int expectedModCount;       // for comodification checks

2.2.2 getFence

这是一个初始化方法,在每次调用Spliterator的时候,都会调用这个方法。

final int getFence() { // initialize fence and size on first use
    int hi;
    //如果完成了初始化,则fence不为-1 只有new的时候传入的fence为-1
    if ((hi = fence) < 0) {
       //下面的值都是在new中传入赋值
        HashMap<K,V> m = map;
        est = m.size;
        expectedModCount = m.modCount;
        Node<K,V>[] tab = m.table;
        hi = fence = (tab == null) ? 0 : tab.length;
    }
    return hi;
}

2.2.3 estimateSize

这个方法的目的是再次调用初始化方法并返回est的结果,确保初始化方法被调用。

public final long estimateSize() {
        getFence(); // force init
        return (long) est;
    }

2.2.4 构造函数

HashMapSpliterator(HashMap<K,V> m, int origin,
                       int fence, int est,
                       int expectedModCount) {
        this.map = m;
        this.index = origin;
        this.fence = fence;
        this.est = est;
        this.expectedModCount = expectedModCount;
    }

通过构造函数完成了赋值,实际上各个子类都是调用的super的方法,就是这个方法。另外我们可以看看再KeySet中的调用:

public final Spliterator<K> spliterator() {
    return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}

其他构造函数也类似,都是在各自对应的Set的内部类中持有。
此时默认传入的index为0,fence为-1,est为0,expectModCount为0。

2.3 KeySpliterator

我们来看看KeySpliterator的源码:

2.3.1 类结构及构造函数

static final class KeySpliterator<K,V>
    extends HashMapSpliterator<K,V>
    implements Spliterator<K> {
    KeySpliterator(HashMap<K,V> m, int origin, int fence, int est,
                   int expectedModCount) {
        super(m, origin, fence, est, expectedModCount);
    }
}

可以看到这里继承了HashMapSpliterator并实现了Spliterator接口,这是一个非常值得借鉴的地方,可以消除很多冗余代码。
构造函数中直接调用了super方法。

2.3.2 trySplit

这个方法主要是来对Spliterator进行分割。如果Spliterator可以被分割,那么会返回一个新的Spliterator,在传统的情况下,比如list,如果为偶数,则拆分一半,如果为奇数,则会少一个。反正是基本做了对等的拆分。也就是我们在大数据中常用的分治法。
那么对于HashMap,实际上bucket是2的幂,那么直接位移就能实现,我们来看源码;

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

通过getFence方法的调用,完成初始化赋值,如果已经被赋值过了,那么返回est的值,我们在前面看过构造函数中第一次传入的est为0,hi的值为tab的size,那么回到前面部分的测试代码中,这里est第一次值为tab的size,而lo初始化为index=0,因此mid=size>>>1,我们上面的例子,size为32,那么第一次得到的mid为16。所以每次拆分的都是16个bucket。
之后再new了一个新的keySpliterator。我们可以通过debug观察这个过程:


image.png

执行到new的时候:


image.png

下一次再进行拆分的时候,hi就变成了16而index则记录了索引,这样配合找到下一次需要拆分的结果。

2.3.3 forEachRemaining

对Spliterator执行遍历:

public void forEachRemaining(Consumer<? super K> 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
     //常规的逻辑都在else部分
        mc = expectedModCount;
    //如果table不为空切length>hi,则开始遍历
    if (tab != null && tab.length >= hi &&
        (i = index) >= 0 && (i < (index = hi) || current != null)) {
        Node<K,V> p = current;
        current = null;
        //遍历hashMap
        do {
        //如果p为空则移动到下一个bucket
            if (p == null)
                p = tab[i++];
            else {
            //遍历链表 调用accept
                action.accept(p.key);
                p = p.next;
            }
        //注意,通过hi进行控制
        } while (p != null || i < hi);
        if (m.modCount != mc)
            throw new ConcurrentModificationException();
    }
}

通过这个代码我们可以看出,实际上,拆分的方法主要通过index和hi进行控制,index为下标,hi为上表,将index和hi之间的部分遍历出来,这就是forEarch函数的主要工作。
由于Hashmap结构的复杂性,并没有采用直接循环tryAdvance来实现,而是自行实现了遍历方法。

2.3.4 tryAdvance

对单个元素进行遍历,前进一次。

public boolean tryAdvance(Consumer<? super K> action) {
    int hi;
    if (action == null)
        throw new NullPointerException();
    Node<K,V>[] tab = map.table;
    if (tab != null && tab.length >= (hi = getFence()) && index >= 0) {
        while (current != null || index < hi) {
          //此处一定会取到一个元素,防止由于index<hi的时候取值为null
            if (current == null)
                current = tab[index++];
            else {
                K k = current.key;
                current = current.next;
                action.accept(k);
                if (map.modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                return true;
            }
        }
    }
    return false;
}

这个方法需要注意的就是,由于Hashmap的特殊结构,可能某个bucket没有Node,那么这个算法会保证只要Hashmap中有元素,那么一定会 取出一个。此时current指向了这个取出的node。后续如果继续执行,则由于current不为空,则直接链表遍历。
这个算法的设计也是非常巧妙的。

2.3.4 characteristics

characteristics用于表示实现类的特征,这样再操作的时候就会根据这个特征而影响之前的tryAdvance等方法。但是并不能自行指定这个值。

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

返回值是65,Spliterator.SIZED 与Spliterator.DISTINCT之和。
常用的返回值解释:

public static final int ORDERED    = 0x00000010; //表示有序 每次遍历结果相同
public static final int DISTINCT   = 0x00000001;//表示唯一重复
//public static final int SORTED     = 0x00000004;//表示按一定规律排列,有比较器
public static final int SIZED      = 0x00000040;//表示大小确定
public static final int NONNULL    = 0x00000100;//表示没有null元素
public static final int IMMUTABLE  = 0x00000400;//表示元素不可变
public static final int CONCURRENT = 0x00001000;//表示支持多线程可并发操作
public static final int SUBSIZED = 0x00004000;//表示子Spliterator具有sized属性

这个值的特性由这个容器的实现者根据容器的特性来定义。而不是我们再使用过程中可以来设置的。

3.总结

以上即使对Hashmap中的Interator和Spliterator的说明。是对于前面两部分关于HashMap源码的补充。其中代码的设计模式通过抽象类来消除冗余代码。另外,Spliterator是java8中配合Stream的并行流而引入的接口。我们需要掌握这个接口的使用场景和作用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343