JavaでのSplitteratorの紹介
1. 概要
Java8で導入されたSpliteratorインターフェースは、シーケンスのトラバースとパーティション化に使用できます。 これは、 Streams 、特に並列のものの基本ユーティリティです。
この記事では、その使用法、特性、メソッド、および独自のカスタム実装を作成する方法について説明します。
2. Spliterator API
2.1. tryAdvance
これは、シーケンスをステップスルーするために使用される主な方法です。 メソッドは、スプリッターの要素を1つずつ順番に消費するために使用されるコンシューマーを取得し、トラバースする要素がない場合はfalseを返します。
ここでは、これを使用して要素をトラバースおよびパーティション化する方法を見ていきます。
まず、35000の記事を含む ArrayList があり、Articleクラスが次のように定義されていると仮定します。
public class Article {
private List<Author> listOfAuthors;
private int id;
private String name;
// standard constructors/getters/setters
}
次に、記事のリストを処理し、各記事名に「 –発行者Baeldung」の接尾辞を追加するタスクを実装しましょう。
public String call() {
int current = 0;
while (spliterator.tryAdvance(a -> a.setName(article.getName()
.concat("- published by Baeldung")))) {
current++;
}
return Thread.currentThread().getName() + ":" + current;
}
このタスクは、実行が終了したときに処理された記事の数を出力することに注意してください。
もう1つの重要なポイントは、 tryAdvance()メソッドを使用して次の要素を処理したことです。
2.2. trySplit
次に、 Spliterators (名前の由来)を分割し、パーティションを個別に処理してみましょう。
trySplit メソッドは、それを2つの部分に分割しようとします。 次に、呼び出し元が要素を処理し、最後に、返されたインスタンスが他の要素を処理して、2つを並行して処理できるようにします。
最初にリストを生成しましょう:
public static List<Article> generateElements() {
return Stream.generate(() -> new Article("Java"))
.limit(35000)
.collect(Collectors.toList());
}
次に、 spliterator()メソッドを使用して、Spliteratorインスタンスを取得します。 次に、 trySplit()メソッドを適用します。
@Test
public void givenSpliterator_whenAppliedToAListOfArticle_thenSplittedInHalf() {
Spliterator<Article> split1 = Executor.generateElements().spliterator();
Spliterator<Article> split2 = split1.trySplit();
assertThat(new Task(split1).call())
.containsSequence(Executor.generateElements().size() / 2 + "");
assertThat(new Task(split2).call())
.containsSequence(Executor.generateElements().size() / 2 + "");
}
分割プロセスは意図したとおりに機能し、レコードを均等に分割しました。
2.3. 推定サイズ
estimatedSize メソッドは、要素の推定数を示します。
LOG.info("Size: " + split1.estimateSize());
これは出力します:
Size: 17500
2.4. hasCharacteristics
このAPIは、指定された特性がのプロパティと一致するかどうかをチェックします
LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464
3. スプリッターの特性
それはその振る舞いを説明する8つの異なる特徴を持っています。 これらは、外部ツールのヒントとして使用できます。
- SIZED – estimateSize()メソッドで正確な数の要素を返すことができる場合
- SORTED –ソートされたソースを反復処理している場合
- SUBSIZED – trySplit()メソッドを使用してインスタンスを分割し、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
}
次に、著者のストリームをトラバースしながら著者をカウントするクラスを実装します。 次に、クラスはストリームに対してリダクションを実行します。
クラスの実装を見てみましょう。
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()メソッドが反復的に作成者を1つずつトラバースし、次に combine()がそれらの値を使用して2つのカウンターを合計します。 最後に、 getCounter()はカウンターを返します。
さて、これまでに行ったことをテストします。 記事の著者リストを著者のストリームに変換してみましょう。
Stream<Author> stream = article.getListOfAuthors().stream();
そして、 countAuthor()メソッドを実装して、RelatedAuthorCounterを使用してストリームの削減を実行します。
private int countAutors(Stream<Author> 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. カスタマイズする方法
これを解決するには、関連するidとarticleIdが一致する場合にのみ作成者を分割するスプリッターを実装する必要があります。 カスタムSpliteratorの実装は次のとおりです。
public class RelatedAuthorSpliterator implements Spliterator<Author> {
private final List<Author> list;
AtomicInteger current = new AtomicInteger();
// standard constructor/getters
@Override
public boolean tryAdvance(Consumer<? super Author> action) {
action.accept(list.get(current.getAndIncrement()));
return current.get() < list.size();
}
@Override
public Spliterator<Author> 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<Author> 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<Author> stream2 = StreamSupport.stream(spliterator, true);
assertThat(Executor.countAutors(stream2.parallel())).isEqualTo(9);
}
また、カスタム Spliterator は作成者のリストから作成され、現在の位置を保持してトラバースします。
各メソッドの実装について詳しく説明しましょう。
- tryAdvance – は、作成者を現在のインデックス位置の Consumer に渡し、その位置をインクリメントします
- trySplit – は分割メカニズムを定義します。この場合、 RelatedAuthorSpliterator は、IDが一致したときに作成され、分割によってリストが2つの部分に分割されます。
- estimatedSize –リストサイズと現在繰り返されている作成者の位置の差です
- 特性– Spliterator 特性を返します。この場合、 estimatedSize()メソッドによって返される値は正確であるため、SIZEDです。 さらに、 CONCURRENT は、このSpliteratorのソースが他のスレッドによって安全に変更される可能性があることを示します
5. プリミティブ値のサポート
Spliterator APIは、double、int、longなどのプリミティブ値をサポートします。
ジェネリックとプリミティブ専用のSpliteratorを使用する場合の唯一の違いは、指定されたConsumerとSpliteratorのタイプです。
たとえば、 int 値に必要な場合は、intConsumerを渡す必要があります。 さらに、プリミティブ専用のSpliteratorsのリストは次のとおりです。
- OfPrimitive
>> :他のプリミティブの親インターフェース - OfInt :intに特化したSpliterator
- OfDouble :double専用のSpliterator
- OfLong :long専用のSpliterator
6. 結論
この記事では、Java 8 Spliteratorの使用法、メソッド、特性、分割プロセス、プリミティブのサポート、およびそれをカスタマイズする方法について説明しました。
いつものように、この記事の完全な実装は、Githubのにあります。