JavaにおけるSpliteratorの紹介

JavaのSpliteratorの概要

1. 概要

Java 8で導入されたSpliteratorインターフェースは、used for traversing and partitioning sequencesにすることができます。 これは、Streams、特に並列のものの基本ユーティリティです。

この記事では、その使用法、特性、方法、および独自のカスタム実装を作成する方法について説明します。

2. Spliterator API

2.1. tryAdvance

これは、シーケンスのステップ実行に使用される主な方法です。 メソッドtakes a Consumer that’s used to consume elements of the Spliterator one by one sequentiallyは、トラバースする要素がない場合にfalseを返します。

ここでは、それを使用して要素をトラバースおよびパーティション分割する方法を見ていきます。

まず、35000の記事を持つArrayListがあり、Articleクラスが次のように定義されていると仮定します。

public class Article {
    private List listOfAuthors;
    private int id;
    private String name;

    // standard constructors/getters/setters
}

次に、記事のリストを処理し、各記事名に「– published by example”」という接尾辞を追加するタスクを実装しましょう。

public String call() {
    int current = 0;
    while (spliterator.tryAdvance(a -> a.setName(article.getName()
      .concat("- published by example")))) {
        current++;
    }

    return Thread.currentThread().getName() + ":" + current;
}

このタスクは、実行の終了時に処理された記事の数を出力することに注意してください。

もう1つの重要な点は、tryAdvance()メソッドを使用して次の要素を処理したことです。

2.2. trySplit

次に、Spliterators(したがって名前)を分割し、パーティションを個別に処理しましょう。

trySplitメソッドは、それを2つの部分に分割しようとします。 次に、呼び出し元が要素を処理し、最後に、返されたインスタンスが他のインスタンスを処理し、2つを並行して処理できるようにします。

最初にリストを生成しましょう:

public static List
generateElements() { return Stream.generate(() -> new Article("Java")) .limit(35000) .collect(Collectors.toList()); }

次に、spliterator()メソッドを使用してSpliteratorインスタンスを取得します。 次に、trySplit()メソッドを適用します。

@Test
public void givenSpliterator_whenAppliedToAListOfArticle_thenSplittedInHalf() {
    Spliterator
split1 = Executor.generateElements().spliterator(); Spliterator
split2 = split1.trySplit(); assertThat(new Task(split1).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); assertThat(new Task(split2).call()) .containsSequence(Executor.generateElements().size() / 2 + ""); }

The splitting process worked as intended and divided the records equally

2.3. 推定サイズ

estimatedSizeメソッドは、要素の推定数を示します。

LOG.info("Size: " + split1.estimateSize());

これは出力されます:

Size: 17500

2.4. hasCharacteristics

このAPIは、指定された特性がSpliterator.のプロパティと一致するかどうかをチェックします。次に、上記のメソッドを呼び出すと、出力はそれらの特性のint表現になります。

LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464

3. Spliterator Characteristics

それはその振る舞いを説明する8つの異なる特徴を持っています。 これらは、外部ツールのヒントとして使用できます。

  • estimateSize()メソッドで正確な数の要素を返すことができる場合はSIZED

  • SORTED –ソートされたソースを反復処理する場合

  • SUBSIZEDtrySplit()メソッドを使用してインスタンスを分割し、SIZEDでもあるスプリッターを取得する場合

  • CONCURRENT –ソースを同時に安全に変更できるかどうか

  • DISTINCT –検出された要素の各ペアの場合x, y, !x.equals(y)

  • IMMUTABLE –ソースによって保持されている要素を構造的に変更できない場合

  • NONNULL –ソースがnullを保持しているかどうか

  • ORDERED –順序付けられたシーケンスを反復する場合

4. カスタムSpliterator

4.1. いつカスタマイズするか

まず、次のシナリオを想定します。

著者のリストを含む記事クラスと、複数の著者を含めることができる記事があります。 さらに、関連記事のIDが記事IDと一致する場合、その記事に関連する著者を考慮します。

Authorクラスは次のようになります。

public class Author {
    private String name;
    private int relatedArticleId;

    // standard getters, setters & constructors
}

次に、著者のストリームを走査しながら著者をカウントするクラスを実装します。 次に、ストリーム上のthe class will perform a reduction

クラスの実装を見てみましょう。

public class RelatedAuthorCounter {
    private int counter;
    private boolean isRelated;

    // standard constructors/getters

    public RelatedAuthorCounter accumulate(Author author) {
        if (author.getRelatedArticleId() == 0) {
            return isRelated ? this : new RelatedAuthorCounter( counter, true);
        } else {
            return isRelated ? new RelatedAuthorCounter(counter + 1, false) : this;
        }
    }

    public RelatedAuthorCounter combine(RelatedAuthorCounter RelatedAuthorCounter) {
        return new RelatedAuthorCounter(
          counter + RelatedAuthorCounter.counter,
          RelatedAuthorCounter.isRelated);
    }
}

上記のクラスの各メソッドは、トラバース中にカウントする特定の操作を実行します。

最初にaccumulate() method traverse the authors one by one in an iterative way、次にcombine() sums two counters using their values。 最後に、getCounter()はカウンターを返します。

次に、これまでに行ったことをテストします。 記事の著者リストを一連の著者に変換してみましょう。

Stream stream = article.getListOfAuthors().stream();

そして、countAuthor() method to perform the reduction on the stream using RelatedAuthorCounterを実装します。

private int countAutors(Stream stream) {
    RelatedAuthorCounter wordCounter = stream.reduce(
      new RelatedAuthorCounter(0, true),
      RelatedAuthorCounter::accumulate,
      RelatedAuthorCounter::combine);
    return wordCounter.getCounter();
}

シーケンシャルストリームを使用した場合、出力は期待どおりの“count = 9”になりますが、操作を並列化しようとすると問題が発生します。

次のテストケースを見てみましょう。

@Test
void
  givenAStreamOfAuthors_whenProcessedInParallel_countProducesWrongOutput() {
    assertThat(Executor.countAutors(stream.parallel())).isGreaterThan(9);
}

どうやら、何かがおかしくなりました。ストリームをランダムな位置で分割すると、作者が2回カウントされます。

4.2. カスタマイズする方法

これを解決するには、implement a Spliterator that splits authors only when related id and articleId matchesにする必要があります。 カスタムSpliteratorの実装は次のとおりです。

public class RelatedAuthorSpliterator implements Spliterator {
    private final List list;
    AtomicInteger current = new AtomicInteger();
    // standard constructor/getters

    @Override
    public boolean tryAdvance(Consumer action) {
        action.accept(list.get(current.getAndIncrement()));
        return current.get() < list.size();
    }

    @Override
    public Spliterator trySplit() {
        int currentSize = list.size() - current.get();
        if (currentSize < 10) {
            return null;
        }
        for (int splitPos = currentSize / 2 + current.intValue();
          splitPos < list.size(); splitPos++) {
            if (list.get(splitPos).getRelatedArticleId() == 0) {
                Spliterator spliterator
                  = new RelatedAuthorSpliterator(
                  list.subList(current.get(), splitPos));
                current.set(splitPos);
                return spliterator;
            }
        }
        return null;
   }

   @Override
   public long estimateSize() {
       return list.size() - current.get();
   }

   @Override
   public int characteristics() {
       return CONCURRENT;
   }
}

ここで、countAuthors()メソッドを適用すると、正しい出力が得られます。 次のコードはそれを示しています。

@Test
public void
  givenAStreamOfAuthors_whenProcessedInParallel_countProducesRightOutput() {
    Stream stream2 = StreamSupport.stream(spliterator, true);

    assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9);
}

また、カスタムSpliteratorは作成者のリストから作成され、現在の位置を保持することによってそれをトラバースします。

各メソッドの実装について詳しく説明します。

  • *tryAdvance* –は、作成者を現在のインデックス位置のConsumerに渡し、その位置をインクリメントします

  • *trySplit* –は分割メカニズムを定義します。この場合、RelatedAuthorSpliteratorはIDが一致したときに作成され、分割によってリストが2つの部分に分割されます。

  • estimatedSize –リストサイズと現在繰り返されている作成者の位置の差です

  • characteristicsestimatedSize()メソッドによって返される値が正確であるため、Spliterator特性を返します。この場合はSIZEDです。さらに、CONCURRENTは、このSpliteratorのソースが他のスレッドによって安全に変更される可能性があることを示します

5. プリミティブ値のサポート

Spliterator API supports primitive values including double, int and long

ジェネリック専用とプリミティブ専用のSpliteratorを使用する場合の唯一の違いは、指定されたConsumerSpliteratorのタイプです。

たとえば、int値に必要な場合は、intConsumerを渡す必要があります。 さらに、プリミティブ専用のSpliteratorsのリストは次のとおりです。

  • OfPrimitive<T, T_CONS, T_SPLITR extends Spliterator.OfPrimitive<T, T_CONS, T_SPLITR>>:他のプリミティブの親インターフェイス

  • OfIntintに特化したSpliterator

  • OfDoubledouble専用のSpliterator

  • OfLonglong専用のSpliterator

6. 結論

この記事では、Java 8Spliterator usage, methods, characteristics, splitting process, primitive support and how to customize it.について説明しました。

いつものように、この記事の完全な実装はover on Githubにあります。