ストリヌムAPIナニバヌサル䞭間操䜜

私は、暙準のJava 8ストリヌムAPIを拡匵する無料のStreamExラむブラリを開発し、そこに新しい操䜜、コレクタヌ、およびストリヌム゜ヌスを远加しおいたす。 通垞、すべおを連続しお远加するわけではありたせんが、可胜性のある各機胜を包括的に怜蚎したす。 たずえば、新しい䞭間操䜜を远加するず、次のような疑問が生じたす。

  1. それは本圓に䞭間的なものでしょうか、぀たり、端末操䜜が完了するたで゜ヌスに觊れたせんか
  2. 圌女は怠け者で、゜ヌスから必芁以䞊のデヌタを匕き出すこずはありたせんか
  3. 無限のストリヌムで動䜜したすか 限られた量のメモリが必芁ですか
  4. それはうたく䞊列したすか

これらのポむントのマむナスは、そのような操䜜を远加するかどうかを真剣に考えさせたす。 最初のマむナス-それはすぐではありたせん。 たずえば、jOOλの競合他瀟にはshuffle操䜜があり、これは䞭間のように芋えたすが、実際にはリストのストリヌム党䜓を盎接消費し、それを混合しお新しいストリヌムを䜜成したす。 私はそれを尊重したせん。

残りの項目のマむナスは、それらに違反する暙準的な操䜜があるため、すぐに「いいえ」を意味するわけではありたせん。 2番目のアむテムはflatMap() 、3番目- sorted() 、4番目-すべおの皮類のlimit()およびtakeWhile() JDK-9でにtakeWhile()たす。 それでも、私はこれを避けようずしたす。 しかし、先日、私は自分では䞊列凊理がうたくいかず、䜿甚方法によっおは無限のストリヌムで動䜜しない可胜性があるこずを発芋したしたが、それでもあたりにも優れおいたす。 これにより、文字通り数行で、既存の䞭間操䜜ず、存圚しない操䜜の束をほがすべお衚珟するこずができたす。 操䜜headTailを呌び出したした。

操䜜メ゜ッドは、2぀の関数パラメヌタヌを受け入れたす以䞋では、 PECSを省略したす。

 <R> StreamEx<R> headTail(BiFunction<T, StreamEx<T>, Stream<R>> mapper, Supplier<Stream<R>> supplier) 

最初の関数は、元のストリヌムの最初の芁玠ず他のすべおの芁玠を含むストリヌムの2぀の匕数を取りたす。 関数はこれで䜕でもでき、新しいストリヌムを返すこずができたす。それは次の操䜜に転送されたす。 2番目の匕数は、匕数を取らず、最初の関数ず同じタむプのストリヌムを返す関数です。 元のストリヌムが空であるこずが刀明した堎合に呌び出されたす。 実際、関数党䜓の1぀だけが呌び出され、ストリヌム党䜓の端末操䜜䞭に1回だけ呌び出されたす。

倚くの堎合、2番目の関数は空のストリヌムのみを返す必芁がありたす元のストリヌムが空の堎合、結果は空になりたす。したがっお、省略できたす。

 <R> StreamEx<R> headTail(BiFunction<T, StreamEx<T>, Stream<R>> mapper) 

これで䜕ができるか芋おみたしょう。 単玔な䜿甚䟋は次のようになりたす。

 StreamEx.of("name", "John", "Mary", "Lucy") .headTail((head, tail) -> tail.map(str -> head+": "+str)) .forEach(System.out::println); 

結論

 name: John name: Mary name: Lucy 

ここでは、ストリヌムの最初の芁玠を削陀し、それを䜿甚しお埌続の芁玠ず連結したす。 したがっお、最初の行にヘッダヌがあるテキストファむルを解析できたす。 しかし、それはかなり退屈です。 この方法で遊ぶず、はるかに匷力であるこずがわかりたした。 それを通しお、JDKからの䞻芁な䞭間操䜜を衚珟しおみたしょう。

Stream.map


マップ操䜜は、指定された関数を元のストリヌムのすべおの芁玠に適甚したす。 これは、headTailを通しおどのように芋えるかです
 public static <T, R> StreamEx<R> map(StreamEx<T> input, Function<T, R> mapper) { return input.headTail((head, tail) -> map(tail, mapper).prepend(mapper.apply(head))); } 

ここでは、別の単玔なプリペンド操䜜を䜿甚したすが、それなしでは䜕も起こりたせん。 これは、2぀のストリヌムの連結に関するトピックのバリ゚ヌションですStream.concatは暙準APIにありたす。 ここでは、末尟を呌び出しおから、関数をストリヌムの先頭にあるhead芁玠に適甚した結果を远加したす。

これは再垰に䌌おいたすが、誰もが再垰がスタックを消費しおいるこずを知っおいたす。 関数型プログラミング蚀語では、末尟再垰の最適化によっお節玄される堎合がありたすが、Javaではそうではなく、予期されおいたせん。 ただし、この堎合、これは完党な再垰ではありたせん。自分自身の䞭でmapメ゜ッドを呌び出すのではなく、埌で呌び出される関数を䜜成するだけです。 この堎合、個々のheadTail()ぞの倉曎がストリヌムの先頭のみに圱響し、テヌルは倉曎されないたたであれば、呌び出しの深さを制埡できるこずが刀明したした。 この機胜を「テヌルストリヌムの最適化」ずはあたり考えおいたせんでした。 䞭間操䜜prepend ストリヌムの先頭に䜕かを远加する、 mapFirst 残りの郚分に觊れるこずなくストリヌムの最初の芁玠を倉曎する、およびheadTail自䜓ずheadTailたす。 原則ずしお、暙準のskipおよびdropWhileJDK-9を䜿甚に拡匵できたすが、私のラむブラリは暙準操䜜が元のStream APIず完党に互換性があるこずを玄束しおおり、ここで埮劙な違いが生じたす。

䜕らかの方法で、䞊蚘のマップ操䜜は、䞀定サむズよりも倧きいスタックたたはメモリを消費せず、任意の長さのストリヌムにたったく適甚できたす。 他の操䜜を芋おみたしょう。

Stream.limit


ストリヌムを指定された長さに制限したす。 それを1぀の芁玠に制限する堎合は、単玔にヘッドからストリヌムを䜜成したす。そうでない堎合は、制限を枛らしおテヌルを呌び出したすハンドルn <= 0-読者のための挔習。

 public static <T> StreamEx<T> limit(StreamEx<T> input, int n) { return input.headTail((head, tail) -> n > 1 ? limit(tail, n - 1).prepend(head) : Stream.of(head)); } 

最初は少し違う方法で曞きたしたflatMap匕数のように、headTail匕数は空のストリヌムの代わりにnullを返すこずができたす

 public static <T> StreamEx<T> limit(StreamEx<T> input, int n) { return input.headTail((head, tail) -> n > 0 ? limit(tail, n - 1).prepend(head) : null); } 

ただし、この実装には欠点がありたす。゜ヌスから必芁以䞊に1぀の芁玠を考慮したすn = 0の堎合、head匕数が読み取られたすが、䜿甚されたせん。 これは重倧な堎合がありたす。 たずえば、このようなコヌドは機胜するはずです。

 limit(StreamEx.of(new Random().ints(0, 1000).boxed().distinct()), 1000).forEach(System.out::println); 

0から999たでの乱数の無限のストリヌムから、䞀意の乱数を遞択したす。 1000個のナニヌクな番号がありたすが、1001個はありたせん。そのため、゜ヌスから1001番目の番号を取埗しようずするず、すべおがフリヌズしたす。

Stream.skip


最初のn個の芁玠を捚おたす。 n = 0の堎合、ヘッドを接着したたたテヌルを返したす。それ以倖の堎合は、匕数を枛らしお呌び出したす。

 static <T> StreamEx<T> skip(StreamEx<T> input, int n) { return input.headTail((head, tail) -> n > 0 ? skip(tail, n - 1) : tail.prepend(head)); } 


Stream.flatMap


ストリヌム䞊の各芁玠を衚瀺し、それらから共通のストリヌムを䜜成したす。 この堎合、実装はマップず同じです。

 public static <T, R> StreamEx<R> flatMap(StreamEx<T> input, Function<T, Stream<R>> mapper) { return input.headTail((head, tail) -> flatMap(tail, mapper).prepend(mapper.apply(head))); } 

ここでの唯䞀の違いは、ストリヌムを受け入れる別のプリペンドが䜿甚されるこずです実際、最初のプリペンドはこの特殊なケヌスです。

Stream.peek


ストリヌムの各芁玠に察しお远加のアクションを実行し、ストリヌムをそのたた返したす。 アクションを実行し、頭を尻尟に接着したす。
 public static <T> StreamEx<T> peek(StreamEx<T> input, Consumer<T> consumer) { return input.headTail((head, tail) -> { consumer.accept(head); return peek(tail, consumer).prepend(head); }); } 


Stream.filter


述郚を満たす芁玠を残したす。 述郚が満たされおいる堎合にのみ頭を接着したす。
 public static <T> StreamEx<T> filter(StreamEx<T> input, Predicate<T> predicate) { return input.<T> headTail((head, tail) -> predicate.test(head) ? filter(tail, predicate).prepend(head) : filter(tail, predicate)); } 

Stream.distinct


ナニヌクなアむテムを残す。 明らかに远加のメモリが必芁になりたす。 単玔な実装では、フィルタヌ暙準たたは䞊蚘で宣蚀されたものを䜿甚したす。

 public static <T> StreamEx<T> distinct(StreamEx<T> input) { return input.headTail((head, tail) -> distinct(tail.filter(n -> !Objects.equals(head, n))).prepend(head)); } 

しかし、そのようなコヌドは䟝然ずしおスタックを䜿い果たし、テヌルストリヌムの最適化はありたせん。 さらに、各芁玠はフィルタヌチェヌンによっお線圢にチェックされたすが、最適化したいず思いたす。 これを行うには、HashSetパラメヌタヌを保持したす。

 private static <T> StreamEx<T> distinct(StreamEx<T> input, Set<T> observed) { return input.headTail((head, tail) -> observed.add(head) ? distinct(tail, observed).prepend(head) : distinct(tail, observed)); } 

芁玠が既にセットに含たれおいる堎合、 Set.addはfalse返すこずを忘れないでください。 この堎合、頭を刺さないでください。 そのような実装はスタックを䜿い果たしず、暙準のメモリよりも劣りたせん。 ここでは、実行するメ゜ッドを远加する䟡倀がありたす再垰関数では、実行するために別のパブリックメ゜ッドが必芁になるこずがよくありたす。

 public static <T> StreamEx<T> distinct(StreamEx<T> input) { return distinct(input, new HashSet<>()); } 

Stream.sorted


ストリヌムを䞊べ替えたす。 操䜜は特別です。゜ヌスが完党に読み取られるたで、結果に䜕も䞎えるこずはできたせん。 すべおをバッファリングする必芁がありたずえば、 ArrayList 、ここでは初めお2番目の匕数headTailを䜿甚したす。

 public static <T> StreamEx<T> sorted(StreamEx<T> input) { return sorted(input, new ArrayList<>()); } private static <T> StreamEx<T> sorted(StreamEx<T> input, List<T> buf) { return input.headTail((head, tail) -> { buf.add(head); return sorted(tail, buf); }, () -> { buf.sort(null); return buf.stream(); }); } 

゜ヌスストリヌム党䜓が終了したら、バッファを䞊べ替え、そこからストリヌムを返したす。 このようなsortedは暙準のものず同様に機胜し、䞊蚘のshuffleよりも優れおいるこずに泚意しおください。 たずえば、2぀の゜ヌトされたストリヌムを連結するず、最初のストリヌムを完党に読み取るたで、2番目のストリヌムは゜ヌトされたせん。 ちなみに、 buf.sort(null)をCollections.shuffle(buf)に眮き換えるこずで、あなたずshuffleはほが正垞に動䜜したす。 Collections.reverse(buf)を䜿甚するず、ストリヌムを反転できたす。

JDK-9では、これたでに2぀の新しい䞭間操䜜が远加されおいたす。 たた、それらを実珟したす。

Stream.takeWhile


述郚がfalseを返したらすぐにストリヌムをトリミングしたす。 制限のように芋えたす

 public static <T> StreamEx<T> takeWhile(StreamEx<T> input, Predicate<T> predicate) { return input.headTail((head, tail) -> predicate.test(head) ? takeWhile(tail, predicate).prepend(head) : null); } 

Stream.dropWhile


述郚がfalse返すたで、ストリヌムから芁玠をスロヌしfalse 。 skip䌌おいたす

 public static <T> StreamEx<T> dropWhile(StreamEx<T> input, Predicate<T> predicate) { return input.headTail((head, tail) -> predicate.test(head) ? dropWhile(tail, predicate) : tail.prepend(head)); } 

さお、車茪の再発明は退屈です。 Stream APIにはない新しい操䜜を実装しおみたしょう。

鏡


コンテンツをストリヌムの最埌に逆順で远加したす1、2、3からのストリヌムが1、2、3、3、2、1になりたす。 簡単に実行できたすが、テヌルの最適化は行われたせん。

 public static <T> StreamEx<T> mirror(StreamEx<T> input) { return input.headTail((head, tail) -> mirror(tail).append(head).prepend(head)); } 

末尟から、バッファが必芁です。

 public static <T> StreamEx<T> mirror(StreamEx<T> input) { return mirror(input, new ArrayDeque<>()); } private static <T> StreamEx<T> mirror(StreamEx<T> input, Deque<T> buf) { return input.headTail((head, tail) -> { buf.addFirst(head); return mirror(tail, buf).prepend(head); }, buf::stream); } 

䞡方の実装は、必芁以䞊のものを取りたせん mirror(StreamEx.of(1,2,3,4,5)).limit(3)は反射点に到達せず、゜ヌスから3぀の芁玠のみを枛算したす。

scanLeft


ストリヌムを順次倉曎し、特定の操䜜を実行したす。 たずえば、 scanLeft(StreamEx.of(1,2,3,4,5), Integer::sum)は芁玠を連続しお合蚈し、ストリヌムscanLeft(StreamEx.of(1,2,3,4,5), Integer::sum)を䜜成する必芁がありたす。

 public static <T> StreamEx<T> scanLeft(StreamEx<T> input, BinaryOperator<T> operator) { return input.headTail((head, tail) -> scanLeft(tail.mapFirst(cur -> operator.apply(head, cur)), operator).prepend(head)); } 

ここでは、すでにStreamExにあるmapFirstメ゜ッドを䜿甚したした。 しかし、もしなければ、再垰がなくおも簡単に曞くこずができたす。

 public static <T> StreamEx<T> mapFirst(StreamEx<T> input, UnaryOperator<T> operator) { return input.headTail((head, tail) -> tail.prepend(operator.apply(head))); } 

いずれの堎合も、mapFirstず既存のテヌルの䞡方でテヌルが最適化されたす。

takeWhileClosed


名前はあたり成功しないかもしれたせん。 堎合によっおは、述語を満たす芁玠だけでなく、それに違反する最初の芁玠も含たれるように、 takeWhileを倉曎するこずがありたす。 既存の操䜜ず通垞のtakeWhileを介しおこれを衚珟するのは普通ではありたせん。 headTailをheadTailず簡単です。

 public static <T> StreamEx<T> takeWhileClosed(StreamEx<T> input, Predicate<T> predicate) { return input.headTail((head, tail) -> predicate.test(head) ? takeWhileClosed(tail, predicate).prepend(head) : Stream.of(head)); } 

毎回


最初から始めお、指定された間隔たずえば、10分ごずでストリヌムから芁玠を取埗したす。 ここでskip操䜜ず組み合わせるず䟿利ですが、暙準のskipはテヌルが最適化されないため、オヌバヌラむドされたskipを䜿甚したす。

 public static <T> StreamEx<T> every(StreamEx<T> input, int n) { return input.headTail((head, tail) -> every(skip(tail, n - 1), n).prepend(head)); } 

カップル


指定された関数をそれらに適甚するこずにより、ストリヌムを芁玠の互いに玠なペアに分割したす芁玠の数が奇数の堎合、最埌の芁玠をスロヌしたす。 ここでは、 headTail 2回呌び出すず䟿利です。

 public static <T, R> StreamEx<R> couples(StreamEx<T> input, BiFunction<T, T, R> mapper) { return input.headTail((left, tail1) -> tail1.headTail((right, tail2) -> couples(tail2, mapper).prepend(mapper.apply(left, right)))); } 

pairMap


亀差するペアでも同じこずが必芁ですか 簡単です。再垰呌び出し䞭に適切な芁玠をストリヌムに返すだけです。

 public static <T, R> StreamEx<R> pairMap(StreamEx<T> input, BiFunction<T, T, R> mapper) { return input.headTail((left, tail1) -> tail1.headTail((right, tail2) -> pairMap(tail2.prepend(right), mapper).prepend(mapper.apply(left, right)))); } 

そのような操䜜は既にStreamExにあり、私はそれに぀いお曞きたした。 もちろん、 headTail()による実装ずは異なり、通垞は䞊列化されたす。

バッチ


さお、ペアで、私はわかりたす。 そしお、ストリヌムを固定長の断片にリストの圢匏で分割し、最埌に敎数以倖の断片を倱いたくない堎合はどうでしょうか たずえば、 batches(StreamEx(1,2,3,4,5,6,7), 3)は、リスト[1,2,3], [4,5,6], [7]からストリヌムを䜜成する必芁がありたす。 䞭間バッファを含む匕数はここで圹立ちたす

 public static <T> StreamEx<List<T>> batches(StreamEx<T> input, int size) { return batches(input, size, Collections.emptyList()); } private static <T> StreamEx<List<T>> batches(StreamEx<T> input, int size, List<T> cur) { return input.headTail((head, tail) -> cur.size() >= size ? batches(tail, size, Collections.singletonList(head)).prepend(cur) //         : batches(tail, size, StreamEx.of(cur).append(head).toList()), //     () -> Stream.of(cur)); } 

゜ヌスが䜿い果たされた堎合、最埌に蓄積されたバッファを() -> Stream.of(cur)を䜿甚しお結果に戻し、テヌルが倱われないようにしたす。 ここでは、実装StreamEx.of(cur).append(head).toList()矎しさのために、 StreamEx.of(cur).append(head).toList()を䜿甚しお新しいリストを䜜成するたびに、既存のリストを倉曎したせん。 ただし、パフォヌマンスが重芁な堎合は、倉曎可胜なリストを簡単に挿入できたす。

withIndices


ストリヌム内の芁玠のむンデックスを知る必芁がありたすか これが可胜です。 むンデックスず芁玠のペアのような特別な型を開始しないために、 BiFunction<Integer, T, R>型の抜象関数を䜿甚したす。これは、むンデックスず芁玠で必芁な凊理を実行できたす。

 public static <T, R> StreamEx<R> withIndices(StreamEx<T> input, BiFunction<Integer, T, R> mapper) { return withIndices(input, 0, mapper); } private static <T, R> StreamEx<R> withIndices(StreamEx<T> input, int idx, BiFunction<Integer, T, R> mapper) { return input.headTail((head, tail) -> withIndices(tail, idx + 1, mapper).prepend(mapper.apply(idx, head))); } 


支配者


より゚キゟチックなタスク指定された芁玠が「支配する」特定の芁玠に続く芁玠をスロヌしたす。 優䜍性は、2぀の芁玠から述語を定矩したす。 たずえば、 dominators(numbers, (a, b) -> a >= b)は、元の数倀のサブセットを増やしたす。 実装はすべおに䌌おいたすが、dropWhileをスキップする代わりに䜿甚されたす

 public static <T> StreamEx<T> dominators(StreamEx<T> input, BiPredicate<T, T> isDominator) { return input.headTail((head, tail) -> dominators(dropWhile(tail, e -> isDominator.test(head, e)), isDominator) .prepend(head)); } 


appendReduction


ストリヌムの最埌に別の芁玠を远加したす-䞎えられた操䜜での削枛の結果。 たずえば、 appendReduction(numbers, 0, Integer::sum) 、その芁玠の合蚈を数倀のストリヌムに远加したす。

 public static <T> StreamEx<T> appendReduction(StreamEx<T> input, T identity, BinaryOperator<T> op) { return input.headTail((head, tail) -> appendReduction(tail, op.apply(identity, head), op).prepend(head), () -> Stream.of(identity)); } 

い぀ものように、すべおが怠zyであり、テヌルが最適化されおいたす。

玠数


むしろ、孊習タスク。 ゚ラトステネスのふるいを䜜成したす。すでに芋たものに分割されおいる玠数をスロヌする玠数の怠zyなストリヌム

 public static StreamEx<Integer> sieve(StreamEx<Integer> input) { return sieve(StreamEx.iterate(2, x -> x+1)); } private static StreamEx<Integer> sieve(StreamEx<Integer> input) { return input.headTail((head, tail) -> sieve(tail.filter(n -> n % head != 0)).prepend(head)); } 

ここでは、関数型蚀語の同様のものももちろん最適化されおいたせんが、テヌルの最適化は取埗されたせん。 しかし、それは簡単に芋えたす。 暙準蚭定では、JVMはStackOverflowErrorでクラッシュするたで、最倧200,000以䞊の玠数を発行したす。

他の䟿利な操䜜を考え出すこずができたす。 たずえば、ストリヌムの内容を指定された回数だけルヌプで繰り返したす。 たたは、2぀の異なるフィルタヌでフィルタヌ凊理しおストリヌムを耇補したす同時に、2番目のフィルタヌが通過しなかったメモリに保存しないでください。 実行䞭のりィンドりを䜜成できたすバッチに䌌おいたすが、重耇しおいたす。 実際、䜕を思い぀いたずしおも、これをheadTailで非垞に短時間で実装するこずができたした私のテストはこちらです 。 いずれにせよ、私にずっお、headTailはIteratorやSpliterator曞くよりも明らかに短く理解しやすいSpliterator 。 私が理解しおいるように、関数型プログラミングの䞖界では、そのようなこずは圓たり前です。 Javaでそれが可胜であるこずは玠晎らしいこずです。

喜んでプログラム

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


All Articles