JavaにおけるSpliteratorの紹介
1概要
Java 8で導入された
Spliterator
インターフェースは、
トラバースおよび分割シーケンス
に使用できます。これは
Streams
の基本ユーティリティ、特にパラレルユーティリティです。
この記事では、その使用方法、特性、メソッド、そして独自のカスタム実装の作成方法について説明します。
2.
Spliterator
API
2.1.
tryAdvance
これは、シーケンスをステップスルーするために使用される主な方法です。メソッドは、
Spliterator
の要素を1つずつ順番に消費するために使用される
Consumer
を取り、** 横断する要素がない場合は
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は、与えられた特性が
Spliteratorのプロパティと一致するかどうかをチェックします。その後、上記のメソッドを呼び出すと、出力はそれらの特性の
int__表現になります。
LOG.info("Characteristics: " + split1.characteristics());
Characteristics: 16464
3.
Spliterator
の特徴
それはその動作を説明する8つの異なる特性を持っています。
これらは外部ツールのヒントとして使うことができます。
正確な数の要素を返すことができる場合は
SIZED
**
–
estimateSize()
メソッドで
SORTED
** – ソートされたソースを反復処理する場合
-
SUBSIZED
–
trySplit()
メソッドを使用してインスタンスを分割した場合
同様に
SIZED
のSpliteratorを入手する
CONCURRENT
** – ソースを同時に安全に修正できる場合
-
DISTINCT
– 検出された要素の各ペア__x、yに対して
!x.equals(y)
IMMUTABLE__ ** – ソースによって保持されている要素が構造的にできない場合
修正済み
NONNULL
** – sourceがnullを保持するかしないか
-
ORDERED
– 順序付きシーケンスを反復処理する場合
4.カスタム
分割子
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);
}
}
上記のクラスの各メソッドは、移動中にカウントするための特定の操作を実行します。
最初に、
acccumulate()メソッドは作者を1つずつ反復的にトラバース
し、次に
combine()__がそれらの値を使って2つのカウンタを合計します
。
最後に、
getCounter()
がカウンタを返します。
それでは、これまでに行ったことをテストします。記事の著者リストを一連の著者に変換しましょう。
Stream<Author> stream = article.getListOfAuthors().stream();
そして
RelatedAuthorCounter
を使用してストリームのリダクションを実行する
countAuthor()
メソッドを実装します。
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
を実装する必要があります。これが私たちのカスタム
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
を渡す必要があります。さらに、これはプリミティブ専用
分割器
のリストです。
-
OfPrimitive <T、T
CONS、T__SPLITRはSpliteratorを拡張したものです。OfPrimitive<T、
T
CONS、T
SPLITR >>
:他のプリミティブの親インタフェース
**
OfInt
:
int
に特化した
Spliterator__
-
OfDouble
:
double
専用の
Spliterator
-
OfLong
:
long
専用の
Spliterator
6. 結論
この記事では、Java 8 ** Splitrator __の使用法、メソッド、特性、分割プロセス、プリミティブサポート、およびそれをカスタマイズする方法について説明しました。
いつものように、この記事の完全な実装はhttps://github.com/eugenp/tutorials/tree/master/core-java-8[Githubで動く]にあります。