Java8中Spliterator详解

Spliterator接口使用

Spliterator是Java 8中加入的一个新接口;这个名字代表“可拆分迭代器”(splitable iterator)。和Iterator一样,Spliterator也用于遍历数据源中的元素,但它是为了并行执行而设计的。Java 8已经为集合框架中包含的所有数据结构提供了一个默认的Spliterator实现。集合实现了Spliterator接口,接口提供了一个spliterator方法。

1.Spliterator简介

用于遍历和分区源元素的对象。Spliterator覆盖的元素源可以是数组, 集合Collection,IO通道或生成器函数。

Spliterator可以单独遍历元素(tryAdvance())或批量顺序遍历元素(forEachRemaining())。

Spliterator还可以将它的一些元素(使用trySplit())进行分区作为另一个Spliterator,以用于可能并行的操作。使用无法拆分的Spliterator或以高度不平衡或低效的方式进行操作不太可能从并行性中受益。遍历和拆分至用完元素;每个Spliterator仅对单个批量计算有用。

Spliterator还会报告 ORDERED, DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENTSUBSIZED中的结构,源和元素的一组characteristics()Spliterator客户端可以使用它们来控制,专门化或简化计算。例如,Collection的Spliterator将报告SIZEDSet的Spliterator将报告DISTINCT,而SortedSet的Spliterator也会报告SORTEDCharacteristics报告为简单的联合位集。一些特性还约束了方法行为;例如,如果ORDERED,遍历方法必须符合其记录的顺序。未来可能会定义新的特性,因此实现者不应给未列出的值赋予意义。

不报告IMMUTABLECONCURRENT的Spliterator应该有一个记录的策略:当spliterator绑定到元素源时;以及检测绑定后检测到的元素源的结构干扰。延迟绑定的Spliterator在第一次遍历,第一次拆分或第一次查询时绑定元素源以预估大小,而不是在创建Spliterator时绑定。非延迟绑定的Spliterator在构造点或任何方法的第一次调用时绑定到元素源。在绑定之前对源所做的修改会在遍历Spliterator时反映出来。绑定Spliterator后,如果检测到结构性干扰,应尽最大努力抛出ConcurrentModificationException。执行此操作的Spliterators称为fail-fast。Spliterator的批量遍历方法(forEachRemaining())可以优化遍历并在遍历所有元素后检查结构干扰,而不是检查每个元素并立即失败。

Spliterators可以通过estimateSize()方法提供对剩余元素数量的估计。理想情况下,如特性SIZED所反映的,该值正好对应于成功遍历中将遇到的元素数。 然而,即使在不完全已知的情况下,估计值的值对于在源上执行的操作仍然有用,例如帮助确定进一步拆分还是按顺序遍历其余元素更适合。

尽管它们在并行算法中具有明显的实用性,但spliterators不是线程安全的; 相反,使用spliterators的并行算法的实现应确保spliterators一次仅由一个线程使用。 这通常很容易通过串行线程限制来实现,而串行线程限制通常是通过递归分解工作的典型并行算法的自然结果。调用trySplit()的线程可以将返回的Spliterator移交给另一个线程,该线程又可以遍历或进一步拆分该Spliterator。 如果两个或多个线程在同一个spliterator上并发运行,则拆分和遍历的行为是不确定的。如果原始线程将一个spliterator移交给另一个线程进行处理,最好是在使用tryAdvance()消费任何元素之前进行切换,因为某些保证(例如SIZED拆分迭代器的estimateSize()的准确性)仅在遍历开始之前有效。

为int,long和double值提供了Spliterator的原始子类型特化。tryAdvance(java.util.function.Consumer)forEachRemaining(java.util.function.Consumer)的子类型默认实现将原始值装箱为其对应包装类的实例。这种装箱可能会破坏使用原始特化所获得的任何性能优势。为避免装箱,应使用相应的基于原语的方法。例如,应优先使用Spliterator.OfInt.tryAdvance(java.util.function.IntConsumer)Spliterator.OfInt.forEachRemaining(java.util.function.IntConsumer),而不是Spliterator.OfInt.tryAdvance(java.util.function.Consumer)Spliterator.OfInt.forEachRemaining(java.util.function.Consumer)。使用基于装箱的方法tryAdvance()forEachRemaining()遍历原始值不会影响值转换为装箱值的顺序。

注意: 与迭代器一样,Spliterators用于遍历源的元素。除了顺序遍历之外,Spliterator API还支持有效的并行遍历,支持分解以及单元素迭代。此外,通过Spliterator访问元素的协议旨在实现比Iterator更小的每个元素开销,并避免使用hasNext()next()的单独方法所涉及的固有竞争。

对于可变源,如果源在Spliterator绑定到其数据源的时间与遍历结束之间受到结构干扰(元素添加,替换或删除),则可能会发生任意和非确定性行为。例如,当使用java.util.stream框架时,这种干扰将产生任意的,非确定性的结果。

源的结构干扰可以通过以下方式进行管理(近似地按降低期望值的顺序):

  • 源不能在结构上受到干扰。
    例如,CopyOnWriteArrayList的实例是不可变的源。 从源创建的Spliterator报告IMMUTABLE的特性。
  • 源管理并发修改。
    例如,ConcurrentHashMap的键集是并发源。 从源创建的Spliterator报告CONCURRENT的特性。
  • 可变源提供延迟绑定和快速失败的Spliterator。
    延迟绑定缩小了干扰影响计算的窗口;快速失败,尽最大努力检测遍历开始后发生结构性干扰并抛出ConcurrentModificationException。 例如,ArrayList和JDK中的许多其它非并发Collection类提供了一个延迟绑定,快速失败的spliterator。
  • 可变源提供了一个非延迟绑定但无法快速失败的Spliterator。
    由于潜在干扰窗口较大,源会增加抛出ConcurrentModificationException的可能性。
  • 可变源提供了延迟绑定和非快速失败的Spliterator。
    由于未检测到干扰,因此在遍历开始之后,源存在任意的,非确定性行为风险。
  • 可变源提供非延迟绑定和非快速失败的Spliterator。
    在构造之后可能发生未检测到的干扰,源增加了任意,非确定性行为的风险。

例,这是一个类(除了例证外不是非常有用的类),它维护一个数组,其中实际数据保存在偶数位置,而不相关的标记数据保存在奇数位置。它的Spliterator忽略tags。

class TaggedArray<T> {
    private final Object[] elements; // immutable after construction
    TaggedArray(T[] data, Object[] tags) {
        int size = data.length;
        if (tags.length != size) throw new IllegalArgumentException();
        this.elements = new Object[2 * size];
        for (int i = 0, j = 0; i < size; ++i) {
            elements[j++] = data[i];
            elements[j++] = tags[i];
        }
    }

    public Spliterator<T> spliterator() {
        return new TaggedArraySpliterator<>(elements, 0, elements.length);
    }

    static class TaggedArraySpliterator<T> implements Spliterator<T> {
        private final Object[] array;
        private int origin; // current index, advanced on split or traversal
        private final int fence; // one past the greatest index

        TaggedArraySpliterator(Object[] array, int origin, int fence) {
            this.array = array; this.origin = origin; this.fence = fence;
        }

        public void forEachRemaining(Consumer<? super T> action) {
            for (; origin < fence; origin += 2)
                action.accept((T) array[origin]);
        }

        public boolean tryAdvance(Consumer<? super T> action) {
            if (origin < fence) {
                action.accept((T) array[origin]);
                origin += 2;
                return true;
            }
            else // cannot advance
                return false;
        }

        public Spliterator<T> trySplit() {
            int lo = origin; // divide range in half
            int mid = ((lo + fence) >>> 1) & ~1; // force midpoint to be even
            if (lo < mid) { // split out left half
                origin = mid; // reset this Spliterator's origin
                return new TaggedArraySpliterator<>(array, lo, mid);
            }
            else       // too small to split
                return null;
        }

        public long estimateSize() {
            return (long)((fence - origin) / 2);
        }

        public int characteristics() {
            return ORDERED | SIZED | IMMUTABLE | SUBSIZED;
        }
    }
}

例如,并行计算框架(如java.util.stream包)将如何在并行计算中使用Spliterator,这是实现关联并行forEach的一种方法,它说明了拆分子任务直到估计的工作量小到足以按顺序执行的主要用法习惯。这里我们假设跨子任务的处理顺序无关紧要;不同的(分支)任务可能以不确定的顺序进一步拆分和并行处理元素。此示例使用CountedCompleter;类似的用法适用于其它并行任务结构。

static <T> void parEach(TaggedArray<T> a, Consumer<T> action) {
    Spliterator<T> s = a.spliterator();
    long targetBatchSize = s.estimateSize() / (ForkJoinPool.getCommonPoolParallelism() * 8);
    new ParEach(null, s, action, targetBatchSize).invoke();
}

static class ParEach<T> extends CountedCompleter<Void> {
    final Spliterator<T> spliterator;
    final Consumer<T> action;
    final long targetBatchSize;

    ParEach(ParEach<T> parent, Spliterator<T> spliterator,
            Consumer<T> action, long targetBatchSize) {
        super(parent);
        this.spliterator = spliterator; this.action = action;
        this.targetBatchSize = targetBatchSize;
    }

    public void compute() {
        Spliterator<T> sub;
        while (spliterator.estimateSize() > targetBatchSize &&
               (sub = spliterator.trySplit()) != null) {
            addToPendingCount(1);
            new ParEach<>(this, sub, action, targetBatchSize).fork();
        }
        spliterator.forEachRemaining(action);
        propagateCompletion();
    }
}

说明: 如果布尔系统属性org.openjdk.java.util.stream.tripwire设置为true,则在对原始子类型特化进行操作时,如果出现原始值的装箱,则会报告诊断警告。

2.Spliterator方法

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
}

T是Spliterator遍历的元素的类型。tryAdvance方法的行为类似于普通的Iterator,因为它会按顺序一个一个使用Spliterator中的元素,并且如果还有其他元素要遍历就返回true。但trySplit是专为Spliterator接口设计的,因为它可以把一些元素划出去分给第二个Spliterator(由该方法返回),让它们两个并行处理。Spliterator还可通过estimateSize方法估计还剩下多少元素要遍历,因为即使不那么确切,能快速算出来是一个值也有助于让拆分均匀一点。

2.1tryAdvance

boolean tryAdvance(Consumer<? super T> action)

如果存在剩余元素,则对其执行给定操作,返回true;否则返回false。如果此Spliterator为ORDERED,则会对遇见顺序中的下一个元素执行操作。操作抛出的异常会转发给调用者。

如果指定的操作为null则会抛出NullPointerException。

2.2forEachRemaining

default void forEachRemaining(Consumer<? super T> action)

在当前线程中按顺序对每个剩余元素执行给定操作,直到所有元素都已处理或操作抛出异常为止。如果此Spliterator为ORDERED,则会按遇见顺序执行操作。操作抛出的异常会转发给调用者。

说明: 默认实现重复调用tryAdvance(java.util.function.Consumer <? super T>),直到它返回false。应该尽可能地覆盖它。

2.3trySplit

Spliterator<T> trySplit()

如果可以对此spliterator进行分区,则返回一个Spliterator覆盖元素,这些元素在从此方法返回时将不被此Spliterator覆盖。
如果此Spliterator为ORDERED,则返回的Spliterator必须覆盖一个严格的元素前缀。

除非此Spliterator包含无限数量的元素,否则重复调用trySplit()必须最终返回null。在非null返回时:

  • 在拆分之前为estimateSize()报告的值必须在拆分后大于或等于为此返回的Spliterator的estimateSize()
  • 如果此Spliterator是SUBSIZED,则在拆分之前,此spliterator的estimateSize()必须等于为此拆分后返回的Spliterator的estimateSize()的总和。

此方法可能由于任何原因返回null,包括空闲,在遍历开始后无法拆分,数据结构约束和效率考虑。

注意: 理想的trySplit方法有效地(无遍历)将其元素精确地分成两半,允许平衡并行计算。许多偏离这种理想仍然非常有效;例如,仅近似地拆分一个近似平衡的树,或者叶子节点可能包含一个或两个元素的树,无法进一步拆分这些节点。 然而,平衡的大偏差和/或过低效率的trySplit机制通常会导致较差的并行性能。
递归拆分过程
这个拆分过程也受Spliterator本身的特性影响,而特性是通过characteristics方法声明的。

2.4estimateSize

long estimateSize()

返回forEachRemaining(java.util.function.Consumer <? super T>)遍历将遇到的元素数量的估计值,如果无限,未知或计算成本太高,则返回Long.MAX_VALUE。

如果此Spliterator为SIZED且尚未部分遍历或拆分,或者此Spliterator已进行SUBSIZED且尚未部分遍历,则此估计值必须是完整遍历将遇到的元素的准确计数。否则,此估计值可能是任意不准确的,但必须按照trySplit()调用中指定的方式减少。

2.5getExactSizeIfKnown

default long getExactSizeIfKnown()

如果此Spliterator为SIZED则返回estimateSize()的便捷方法,否则为-1。

说明: 如果Spliterator报告SIZED的特性,则默认实现返回estimateSize()的结果,否则返回-1。

2.6characteristics

int characteristics()

返回此Spliterator及其元素的一组特性。结果表示为 ORDERED, DISTINCT, SORTED, SIZED, NONNULL, IMMUTABLE, CONCURRENT, SUBSIZED的ORed值。在trySplit调用之前或之间对给定spliterator上的characteristics()重复调用应始终返回相同的结果。
如果Spliterator报告一组不一致的特征(从单个调用或跨多个调用返回的特性),则无法保证使用此Spliterator进行的任何计算。

注意: 拆分前给定spliterator的特性可能与拆分后的特性不同。有关具体示例,请参见特性值SIZEDSUBSIZEDCONCURRENT

2.7hasCharacteristics

default boolean hasCharacteristics(int characteristics)

如果此Spliterator的characteristics()包含所有给定特性,则返回true,否则为false。

说明: 如果设置了给定特性的对应位,则默认实现返回true。

2.8getComparator

default Comparator<? super T> getComparator()

如果此Spliterator的源由Comparator比较器为SORTED,则返回该Comparator。如果源按Comparable自然顺序为SORTED,则返回null。否则,如果源不是SORTED,则抛出IllegalStateException

说明: 默认实现始终抛出IllegalStateException

3.Spliterator的特性

Spliterator接口声明的一个抽象方法是characteristics它将返回一个int,代表Spliterator本身特性集的编码。使用Spliterator可以用这些特性来更好地控制和优化它的使用。

字段描述
CONCURRENTCharacteristic value signifying that the element source may be safely concurrently modified (allowing additions, replacements, and/or removals) by multiple threads without external synchronization.
DISTINCTCharacteristic value signifying that, for each pair of encountered elements x, y, !x.equals(y).
IMMUTABLECharacteristic value signifying that the element source cannot be structurally modified; that is, elements cannot be added, replaced, or removed, so such changes cannot occur during traversal.
NONNULLCharacteristic value signifying that the source guarantees that encountered elements will not be null.
ORDEREDCharacteristic value signifying that an encounter order is defined for elements.
SIZEDCharacteristic value signifying that the value returned from estimateSize() prior to traversal or splitting represents a finite size that, in the absence of structural source modification, represents an exact count of the number of elements that would be encountered by a complete traversal.
SORTEDCharacteristic value signifying that encounter order follows a defined sort order.
SUBSIZEDCharacteristic value signifying that all Spliterators resulting from trySplit() will be both SIZED and SUBSIZED.

3.1ORDERED

特性值表示为元素定义了遇见顺序。如果是这样,这个Spliterator保证方法trySplit()拆分一个严格的元素前缀,方法tryAdvance(java.util.function.Consumer<? super T>)按前缀顺序逐个元素,以及forEachRemaining(java.util.function.Consumer<? super T>)以遇见顺序执行操作。

如果相应的Collection.iterator()记录顺序,则Collection具有遇见顺序。如果是这样,则遇见顺序与记录的顺序相同。否则,集合没有遇见顺序。

注意: 遇见顺序保证是任何List的升序索引顺序。但是没有保证基于散列的集合(如HashSet)的顺序。报告ORDERED的Spliterator的客户端应该保留非交换并行计算中的排序约束。

3.2DISTINCT

特性值表示对于每对遇到的元素xy!x.equals(y)。例如,这适用于基于Set的Spliterator。

3.3SORTED

特性值表示遇见顺序遵循定义的排序顺序。如果是这样,方法getComparator()返回关联的Comparator,如果所有元素都是Comparable并按其自然顺序排序,则返回null。
报告SORTED的Spliterator也必须报告ORDERED

注意: 在JDK中实现NavigableSetSortedSet的Collection类的spliterators拆分器报告SORTED

3.4SIZED

特性值表示在遍历或拆分之前从estimateSize()返回的值表示有限大小,在没有结构源修改的情况下,它表示完全遍历将遇见的元素数量的精确计数。

注意: 涵盖了Collection的所有元素,大多数Collections的Spliterator,报告了这一特性。子spliterators拆分器,例如HashSet的子拆分器,它覆盖元素的子集并近似于其报告的大小。

3.5NONNULL

特性值表示源保证遇见的元素不为null。(例如,这适用于大多数并发集合,队列和映射。)

3.6IMMUTABLE

特性值表示元素源不能进行结构修改;也就是说,元素不能添加,替换或删除,因此在遍历期间不能发生此类更改。不报告IMMUTABLECONCURRENT的Spliterator应该具有关于遍历期间检测到的结构干扰的记录策略(例如抛出ConcurrentModificationException)。

3.7CONCURRENT

特性值表示可以在没有外部同步的情况下由多个线程安全地同时修改元素源(允许添加,替换和/或删除)。如果是这样,Spliterator应该有一个关于遍历期间修改影响的记录策略。

顶级Spliterator不应同时报告CONCURRENTSIZED,因为有限大小(如果已知)可能会在遍历期间并发修改源时发生更改。这样的Spliterator是不一致的,并且不能保证使用该Spliterator进行的任何计算。如果已知子拆分大小,并且在遍历时不体现对源的添加或删除,则子spliterators拆分器可能报告SIZED

注意: 大多数并发集合保持一致性策略,保证Spliterator构造点上存在的元素的准确性,但可能不体现后续的添加或删除。

3.8SUBSIZED

特性值表示由trySplit()产生的所有Spliterator都将是SIZEDSUBSIZED。(这意味着所有子Spliterator,无论是直接还是间接,都将是SIZED。)
根据SUBSIZED的要求不报告SIZED的Spliterator是不一致的,并且不能保证使用该Spliterator进行的任何计算。

注意: 一些spliterators拆分器,例如用于近似平衡二叉树的顶级拆分器,将报告SIZED但不是SUBSIZED,因为通常知道整个树的大小而不是子树的确切大小。

4.自定义Spliterator

import java.util.Objects;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class TestSpliterator {

    private static String s = 
    		"1|2|3\n" +
            "200|300|400\n" +
            "800|500|600\n" +
            "100|900|200\n" +
            "12|21|77\n" +
            "32|47|90\n" +
            "43|87|31\n" +
            "56|34|57\n" +
            "67|26|73\n";

    public static void main(String[] args) {
        TextLine textLine = new TextLine(s);
        textLine.stream().forEach(System.out::println);
        System.out.println("---------------------------");
        textLine.parallelStream().forEach(System.out::println);
    }


    static class TextLine {
        private final String[] data;

        public TextLine(String text) {
            Objects.requireNonNull(text, "The parameter can not be null");
            this.data = text.split("\n");
        }

        public Stream<String> stream() {
            return StreamSupport.stream(new TextLineSpliterator(), false);
        }

        public Stream<String> parallelStream() {
            return StreamSupport.stream(new TextLineSpliterator(), true);
        }

        private class TextLineSpliterator implements Spliterator<String> {

            private int start, end;

            public TextLineSpliterator() {
                this.start = 0;
                this.end = TextLine.this.data.length - 1;
            }

            public TextLineSpliterator(int start, int end) {
                this.start = start;
                this.end = end;
            }

            @Override
            public boolean tryAdvance(Consumer<? super String> action) {
                if (start <= end) {
                    action.accept(TextLine.this.data[start++]);
                    return true;
                }
                return false;
            }

            @Override
            public Spliterator<String> trySplit() {
                int mid = (end - start) / 2;
                if (mid <= 1) {
                    // 返回null表示要处理的String已经足够小,可以顺序处理
                    return null;
                }

                int left = start;
                int right = start + mid;
                start = start + mid + 1;
                return new TextLineSpliterator(left, right);
            }

            @Override
            public long estimateSize() {
                return end - start;
            }

            @Override
            public long getExactSizeIfKnown() {
                return estimateSize();
            }

            @Override
            public int characteristics() {
                return IMMUTABLE | SIZED | SUBSIZED;
            }

        }

    }

}

1|2|3
200|300|400
800|500|600
100|900|200
12|21|77
32|47|90
43|87|31
56|34|57
67|26|73
---------------------------
32|47|90
43|87|31
56|34|57
67|26|73
100|900|200
12|21|77
1|2|3
200|300|400
800|500|600

串行执行tryAdvance
并行执行trySplit

本文参考:
Spliterator (Java Platform SE 8 )

  • 22
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
Java 8的Collectors.toMap方法可以将Stream的元素转换为一个Map对象。该方法有三个参数:keyMapper,valueMapper和mergeFunction。其,keyMapper用于将Stream的元素转换为Map的key,valueMapper用于将Stream的元素转换为Map的value,mergeFunction用于处理当key重复时的冲突情况。 下面是一个例子,假设我们有一个List对象,其包含多个Person对象,每个Person对象都有一个唯一的id属性和一个name属性。我们可以使用Collectors.toMap方法将List转换为一个以id为key,以Person对象为value的Map对象: ```java List<Person> personList = new ArrayList<>(); // 假设我们已经将多个Person对象添加到了personList Map<Integer, Person> personMap = personList.stream() .collect(Collectors.toMap(Person::getId, Function.identity())); ``` 在上面的例子,Person::getId表示将Person对象的id属性作为Map的key,Function.identity()表示将Person对象本身作为Map的value。如果我们想要将Person对象的name属性作为Map的value,可以这样写: ```java Map<Integer, String> personMap = personList.stream() .collect(Collectors.toMap(Person::getId, Person::getName)); ``` 如果我们的List有重复的id,那么上面的代码将会抛出IllegalStateException异常。为了避免这种情况,我们可以使用mergeFunction参数来处理冲突。例如,如果我们想要将重复的id的Person对象合并为一个List,可以这样写: ```java Map<Integer, List<Person>> personMap = personList.stream() .collect(Collectors.toMap(Person::getId, Collections::singletonList, (list1, list2) -> { List<Person> resultList = new ArrayList<>(list1); resultList.addAll(list2); return resultList; })); ``` 在上面的代码,Collections::singletonList表示将Person对象转换为只包含一个元素的List,mergeFunction参数则表示将两个List合并为一个List。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值