スプリッタヌを曞く

皆さんの倚くはすでにストリヌムAPI-Java 8ストリヌムを味わっおいたすが、コレクション、配列、乱数からの既補のストリヌムを䜿甚するだけでなく、根本的に新しいストリヌムを䜜成したい人もいたす。 これを行うには、スプリッタヌを䜜成する必芁がありたす。 Spliteratorは、内郚ロゞックのパブリック郚分であるストリヌムを埋めたす。 この蚘事では、スプリッタヌを䜜成した方法ず理由を説明したす。

スプリッタヌずは


スプリッタヌは8぀のメ゜ッドを含むむンタヌフェヌスであり、そのうちの4぀はすでにデフォルトの実装を持っおいたす。 残りのメ゜ッドは、 tryAdvance 、 trySplit 、 estimateSizeおよびtrySplitです。 プリミティブ型int 、 longおよびdouble特別なスプリッタヌ倉曎もありたす。これらは、ボクシングを回避するためにいく぀かの远加メ゜ッドを远加したす。 スプリッタヌは通垞のむテレヌタヌのようなものです。 䞻な違い-2぀の郚分に分割分割する機胜-は、スレッドの䞊列操䜜の基本です。 たた、最適化するために、スプリッタヌには倚数のフラグ特性があり、正確にたたはほがそのサむズを報告できたす。 最埌に、スプリッタヌは決しおデヌタ゜ヌスを倉曎したせん。むテレヌタヌのようなremoveメ゜ッドはありたせん。 より詳现にメ゜ッドを怜蚎しおください。


既存のスプリッタヌを䜿甚しおストリヌムを䜜成するのは非垞に簡単です-StreamSupport.streamを呌び出す必芁がありたす。

スプリッタヌを曞く必芁がないずき


理解すべき䞻なこずは、スプリッタヌ自䜓は必芁なく、ストリヌムが必芁であるこずです。 既存の機胜を䜿甚しおストリヌムを䜜成できる堎合は、それを行う必芁がありたす。 たずえば、XML DOMストリヌムず友達になり、 NodeListを䜿甚しおストリヌムを䜜成するずしたす。 このような暙準的な方法はありたせんが、远加のスプリッタなしで簡単に蚘述できたす。

 public class XmlStream { static Stream<Node> of(NodeList list) { return IntStream.range(0, list.getLength()).mapToObj(list::item); } } 

同様に、任意の非暙準コレクション別の䟋はorg.json.JSONArray にストリヌムを远加できたす。これにより、序数で長さず芁玠をすばやく返すこずができたす。

trySplitを曞くのが難しいか怠けおいるずtrySplit堎合は、スプリッタヌをたったく曞かない方が良いです。 次に、プロトンスレッドラむブラリを蚘述し、䞊列スレッドの存圚を完党に無芖する仲間を瀺したす 。 圌は、共有方法がたったくわからないスプリッタヌをたくさん曞いた。 たったく分割しないスプリッタヌは、悪い、䟡倀のないスプリッタヌです。 そうしないでください。 この堎合、通垞のむテレヌタヌを蚘述し、 Spliterators.spliteratorメ゜ッドを䜿甚しおその䞊にスプリッタヌを䜜成するか、事前にコレクションのサむズがわからない堎合はSpliterators.spliteratorUnknownSizeを䜜成するこずをお勧めしたす。 これらのメ゜ッドには、少なくずも陀算のヒュヌリスティックがありたす。反埩子の䞀郚をバむパスし、それを配列に枛算しお、この配列の新しいスプリッタヌを䜜成したす。 ストリヌムが長時間の操䜜を継続する堎合、䞊列化により䜜業が加速されたす。

暙準のIterableたたはCollectionむンタヌフェむスを実装するず、 default spliterator() defaultメ゜ッドが提䟛されたす。 もちろん、改善できるかどうかを確認する䟡倀はありたす。 䜕らかの方法で、スプリッタヌを䜜成する必芁はほずんどありたせん。 これは、デヌタ構造たずえば、 leventovのようにプリミティブのコレクションを開発しおいる堎合に䟿利です。

そしおただ曞く


この問題を解決するために、新しいスプリッタヌを䜜成したす。特定のストリヌムの゜ヌスストリヌムの隣接する倀からペアのストリヌムを䜜成したす。 Javaで倀のペアを衚すために䞀般に受け入れられおいるタむプはなく、可胜なオプションが倚すぎるため2぀の倀の配列、2぀の倀のリスト、同じタむプのキヌず倀を持぀Map.Entryなどを䜿甚、ナヌザヌに提䟛したす2぀の倀を組み合わせる方法を圌に決めさせたす。 ぀たり、次のシグネチャを持぀メ゜ッドを䜜成する必芁がありたす。

 public static <T, R> Stream<R> pairMap(Stream<T> stream, BiFunction<T, T, R> mapper) {...} 

これにより、任意のタむプを䜿甚しおペアを衚すこずができたす。 たずえば、 Map.Entry必芁な堎合

 public static <T> Stream<Map.Entry<T, T>> pairs(Stream<T> stream) { return pairMap(stream, AbstractMap.SimpleImmutableEntry<T, T>::new); } 

そしお、䞀般的に、䞭間コンテナにペアを積み重ねるこずなく、すぐに興味深いものを蚈算できたす。

 public static Stream<Integer> diff(Stream<Integer> stream) { return pairMap(stream, (a, b) -> b - a); } 

このメ゜ッドは、敎数のストリヌムによる隣接芁玠の差のストリヌムを返したす。 ご想像のずおり、最終ストリヌムでは元の芁玠より芁玠が1぀少なくなりたす。

pairMap通垞の䞭間操䜜のようpairMap芋せる、぀たり、実際には、タヌミナル操䜜になるたで蚈算を実行しないようにしたす。 これを行うには、入力ストリヌムからspliteratorを取埗する必芁がありたすが、求められるたで䜕もしたせん。 もう1぀の小さいが重芁なこず close()新しいスレッドをclose()ずきは、元のスレッドを閉じる必芁がありたす。 その結果、メ゜ッドは次のようになりたす。

 public static <T, R> Stream<R> pairMap(Stream<T> stream, BiFunction<T, T, R> mapper) { return StreamSupport.stream(new PairSpliterator<>(mapper, stream.spliterator()), stream.isParallel()).onClose(stream::close); } 

spliterator()メ゜ッドを呌び出した埌の゜ヌスストリヌムは「䜿甚枈み」になり、これ以䞊おを調理するこずはできたせん。 しかし、これは正垞です。これは、新しい操䜜を远加するずきにすべおの䞭間スレッドで発生したす。 2぀のストリヌムを結合するStream.concatメ゜ッドは次のようになりたす。 PairSpliterator自䜓を蚘述するPairSpliteratorたす。

ポむントに行きたしょう。


最も簡単なこずは、 characteristics()メ゜ッドを蚘述するこずです。 䞀郚の特性は元のスプリッタヌから継承されたすが、NONNULL、DISTINCT、およびSORTEDをリセットする必芁がありたす。任意のマッパヌ関数を䜿甚した埌にこれらの特性を保蚌するこずはできたせん。

 public int characteristics() { return source.characteristics() & (SIZED | SUBSIZED | CONCURRENT | IMMUTABLE | ORDERED); } 

tryAdvanceメ゜ッドの実装は、かなり単玔でなければなりたせん。 元のスプリッタヌからデヌタを読み取り、䞭間バッファヌの前の芁玠を蚘憶し、最埌のペアのmapperを呌び出すだけです。 ストリヌムの先頭にいるかどうかを芚えおおくだけの䟡倀がありたす。 ブヌル倉数hasPrevこれに圹立ち、前の倀があるかどうかを瀺したす。

trySplitメ゜ッドtrySplit 、゜ヌススプリッタヌでtrySplitを呌び出すこずで実装するのtrySplit最適です。 ここでの䞻な困難は、元のストリヌムの2぀の分離された郚分のゞャンクションでペアを凊理するこずです。 このペアは、前半をバむパスするスプリッタヌで凊理する必芁がありたす。 したがっお、埌半の最初の倀を保存し、最埌に達したずきに、最埌の倀ずずもにmapperに送信するこずで再び動䜜する必芁がありたす。

これに察凊した埌、コンストラクタヌを䜜成したす。

 class PairSpliterator<T, R> implements Spliterator<R> { Spliterator<T> source; boolean hasLast, hasPrev; private T cur; private final T last; private final BiFunction<T, T, R> mapper; public PairSpliterator(BiFunction<T, T, R> mapper, Spliterator<T> source) { this(mapper, source, null, false, null, false); } public PairSpliterator(BiFunction<T, T, R> mapper, Spliterator<T> source, T prev, boolean hasPrev, T last, boolean hasLast) { this.source = source; //   this.hasLast = hasLast; //       (   ) this.hasPrev = hasPrev; //     this.cur = prev; //   this.last = last; //     this.mapper = mapper; } // ... } 

tryAdvanceメ゜ッドラムダを䜿甚しお元のtryAdvanceに枡す代わりに、セッタヌぞのリンクを䜿甚

 void setCur(T t) { cur = t; } @Override public boolean tryAdvance(Consumer<? super R> action) { if (!hasPrev) { //    :      if (!source.tryAdvance(this::setCur)) { return false; //    —  } hasPrev = true; } T prev = cur; //    if (!source.tryAdvance(this::setCur)) { //     if (!hasLast) return false; //    —  hasLast = false; //       cur = last; } action.accept(mapper.apply(prev, cur)); //   action  mapper' return true; } 

そしお、ここにtrySplit()メ゜ッドがありたす

 public Spliterator<R> trySplit() { Spliterator<T> prefixSource = source.trySplit(); //    if (prefixSource == null) return null; //   —       T prev = cur; //       ,     if (!source.tryAdvance(this::setCur)) { //      source = prefixSource; //      —    return null; } boolean oldHasPrev = hasPrev; hasPrev = true; //      ,      return new PairSpliterator<>(mapper, prefixSource, prev, oldHasPrev, cur, true); } 

estimateSize()を蚘述するこずは難しくありたせん。゜ヌススプリッタヌがそのサむズを掚定できる堎合は、フラグを確認し、1぀たたは2぀のナニットを埮調敎するだけです。

 public long estimateSize() { long size = source.estimateSize(); if (size == Long.MAX_VALUE) //       —     return size; if (hasLast) //          size++; if (!hasPrev && size > 0) //        size--; return size; } 

このフォヌムでは、このスプリッタヌが私のStreamExラむブラリに入りたした。 唯䞀の違いは、プリミティブ型のバヌゞョンを䜜成する必芁があったこずです。たあ、 pairMapは静的メ゜ッドではありたせん。

これはすべお非垞に遅いようですか


すべおが速床でそれほど悪くはありたせん。 たずえば、StackOverflowの次の問題を考えおみたしょう。指定されたIntegerセットから、それらに続く数よりも小さいものだけを残し、結果を新しいリストに保存したす。 タスク自䜓は非垞に単玔なので、時間のかなりの郚分がオヌバヌヘッドに費やされたす。 2぀の玠朎な実装を提案できたす。むテレヌタ任意のコレクションで動䜜したすず芁玠番号によるアクセス高速ランダムアクセスのリストでのみ動䜜したすです。 むテレヌタnaiveIteratorを䜿甚したバリアントは次のずおりです。

 List<Integer> result = new ArrayList<>(); Integer last = null; for (Integer cur : input) { if (last != null && last < cur) result.add(last); last = cur; } 

ただし、ランダムアクセスnaiveGetの堎合

 List<Integer> result = new ArrayList<>(); for (int i = 0; i < input.size() - 1; i++) { Integer cur = input.get(i), next = input.get(i + 1); if (cur < next) result.add(cur); } 

StreamExラむブラリを䜿甚した゜リュヌションは非垞にコンパクトで、どのデヌタ゜ヌスstreamExでも機胜したす。

 List<Integer> result = StreamEx.of(input).pairMap((a, b) -> a < b ? a : null).nonNull().toList(); 

コメンテヌタヌは、さらに3぀の実甚的な゜リュヌションを提案したした。 埓来の倚かれ少なかれ投祚数が最も倚かったため、入力時にランダムアクセスリストが必芁ですこの゜リュヌションストリヌムを呌び出したしょう。

 List<Integer> result = IntStream.range(0, input.size() - 1).filter(i -> input.get(i) < input.get(i + 1)).mapToObj(input::get) .collect(Collectors.toList()); 

以䞋は、䞊列化削枛しない副䜜甚を䌎う削枛です。

 List<Integer> result = new ArrayList<>(); input.stream().reduce((a, b) -> { if (a < b) result.add(a); return b; }); 

最埌の1぀はコレクタヌであり、これも䞊列ではありたせんコレクタヌ。

 public static Collector<Integer, ?, List<Integer>> collectPrecedingValues() { int[] holder = { Integer.MAX_VALUE }; return Collector.of(ArrayList::new, (l, elem) -> { if (holder[0] < elem) l.add(holder[0]); holder[0] = elem; }, (l1, l2) -> { throw new UnsupportedOperationException("Don't run in parallel"); }); } List<Integer> result = input.stream().collect(collectPrecedingValues()); 

ストリヌムずstreamExの䞊列バヌゞョンも比范察象になりたす。 長さn = 10,000、100,000、1,000,000の芁玠のランダムな敎数の配列で実隓を行いたす玄半分が結果になりたす。 完党なJMHベンチマヌクコヌドはこちらです。 すべおのアルゎリズムが同じ結果の配列を生成するこずが怜蚌されたす。

枬定は、クアッドコアCore-i5で実行されたした。 結果は次のようになりたす操䜜ごずにマむクロ秒単䜍ですべおの時間、少ない方が良い
アルゎリズムn = 10,000n = 100,000n = 1,000,000
naiveIterator97.7904.010592.7
ナむヌブ99.81084.411424.2
コレクタヌ112.51404.914387.2
枛らす112.11139.512001.5
ストリヌム146.41624.116600.9
streamEx115.21247.112967.0
streamParallel56.9582.36120.5
streamExParallel53.4516.75353.4
pairMapstreamExを含むバヌゞョンは、埓来のストリヌミングバヌゞョンstreamずコレクタヌを含むバヌゞョンの䞡方を远い越しおいるこずがわかりたす。 同時に、streamExのパラレルバヌゞョンはストリヌムのパラレルバヌゞョンよりも高速であり、小さなデヌタセットであっおもすべおのシリアルバヌゞョンを倧幅に䞊回りたす。 これは、 Stream Parallel Guidanceの経隓則ず䞀臎しおいたす。少なくずも100マむクロ秒実行される堎合、タスクを䞊列化するのは理にかなっおいたす。

独自のストリヌムを䜜成する堎合は、適切なスプリッタヌに応じお、タスクの䞊列化方法に䟝存するこずに泚意しおください。 分割に煩わされたくない堎合は、スプリッタヌをたったく䜜成せずに、ナヌティリティメ゜ッドを䜿甚しおください。 たた、既存のJDK機胜を䜿甚しおストリヌムを䜜成できる堎合は、新しいスプリッタヌを䜜成しないでください。 優れたスプリッタヌがあれば、それほど難しくないタスクでも䞊列凊理で高速化できたす。

Source: https://habr.com/ru/post/J256905/


All Articles