数分でひざの上でJava Stream APIを書く

ストリームAPIは、Javaプログラマーの間で急速に人気を博した素晴らしいものです。 単純な操作のマップ、フィルター、forEach、collectのチェーンを介した単一行処理のデータ収集は、非常に便利であることが証明されました。 もちろん、キーと値のペアでの操作も害はありませんが、悲しいかな。



全体として、これがどのように機能するかはほぼ理解できますが、「これをどのように書くでしょうか?」という質問に対する答えは、多くの場合、このまたはそのテクノロジーの内部メカニズムを理解するのに役立ちます。 偶然にも、私自身がこの質問に答えたのは、Stream API、この自転車の発明の歴史、そして急いであなたと共有することです。



私は落ち着いてIDEコンポーネントを作成しましたが、世界は変化していました-javascriptがUI開発領域を引き継いでいました。 そして捕獲された。 好むと好まざるとにかかわらず、絶対にすべてのマシンでの高品質のランタイムは強力な議論です。 何もすることはありません、私はそれを理解しなければなりませんでした。 Javaスクリプトでは、ユーザーコードは単一のスレッドで実行されるため、長時間実行される操作はすべて非同期です。 そして、ビジネスロジックがこのような操作のシーケンスを想定している場合、最初の操作にコールバックを送信する必要があります。コールバックは2番目を起動し、コールバックは3番目を実行します。 一般的に、それはひどく読み、痛々しいほどサポートされています、js開発者はどうにかしてそれと一緒に生き、回避策を考えます、私が出会ったパズルオプションの1つはジェネレーターの使用です。 だから私はそれらについて知りました。



新しいプログラミングに甘やかされていないJavistsにとって、ジェネレーターとは、多くの現代言語に存在する言語構成体であり、値を何度も返すことができる関数のように見えることです。 このような「関数」を使用して、イテレーターは、それが発行する値から組み立てることができます。 インタビューでイテレータが競うように頼んだ人は私に同意するでしょう-ジェネレータの助けを借りて、これは完全に些細な仕事になります。



それで、Java開発者が何らかのジェネレータを作成したい場合、彼はどうしますか? 次のようになりました。



public interface Generator<T> { void generate(GeneratorContext<T> context); } public interface GeneratorContext<T> { void emit(T value); }
      
      





考え方は明確で、生成(...)メソッドが生成に関与し、特定のコンテキストがパラメーターとして渡され、その発行(...)メソッドを連続して呼び出すことにより、複数の値を返すことができます。



確かに、このジェネレーターによって生成されたデータはエンティティを形成します。これをDatasetと呼びましょう。



 public class Dataset<T> { private final Generator<T> generator; private Dataset(Generator<T> generator) { this.generator = generator; } }
      
      





また、データセットがある場合は、各要素で何かを実行できると便利です。 そこか何かを入力します。 forEachメソッドをDatasetクラスに追加します。



  public void forEach(Consumer<T> consumer) { generator.generate(value -> consumer.accept(value)); }
      
      





私たちは、emitメソッドを呼び出すたびに、生成された値をコンシューマに渡すジェネレータコンテキストを形成し、生成を開始しました。



どこかからデータセットのインスタンスを取得するために残り、あなたはそれを経験することができます。 コレクションからジェネレータを作成し、データセットにラップするファクトリメソッドを追加します。



  public static <T> Dataset<T> of(Collection<T> collection) { return new Dataset<>(generatorContext -> collection.forEach(item -> generatorContext.emit(item)) ); }
      
      





古き良きループでも同じこと:



  public static <T> Dataset<T> of(Collection<T> collection) { return new Dataset<>(generatorContext -> { for (T item : collection) { generatorContext.emit(item); } }); }
      
      





それらは単にコレクションを実行し、各要素を放出しました。 すでに実行できます:



  Dataset.of(Arrays.asList("foo", "bar")).forEach(System.out::println);
      
      





結論:

foo

バー


実際、これはインタビューで最も人気のあるタスクです。データセットに別のコレクションの要素を追加します。 メソッドを追加:



  public Dataset<T> union(Collection<T> collection) { return new Dataset<>(generatorContext -> { generator.generate(generatorContext); collection.forEach(item -> generatorContext.emit(item)); }); }
      
      





最初に現在のデータセットのすべての値を出力し、次にアタッチされたコレクションのすべての値を出力するジェネレーターを使用して、新しいデータセットを作成しました。



フィルタリング:



  public Dataset<T> filter(Predicate<T> predicate) { return new Dataset<>(generatorContext -> generator.generate(value -> { if (predicate.test(value)) { generatorContext.emit(value); } })); }
      
      





ここでは、現在のジェネレーターから値を受け取るようなジェネレーターを使用して新しいデータセットを作成しましたが、テストに合格したもののみがさらに発光します。



最後に、データセットの各要素を変換します。



  public <R> Dataset<R> map(Function<T, R> function) { return new Dataset<>(generatorContext -> generator.generate( value -> generatorContext.emit(function.apply(value)) )); }
      
      





新しいデータセットを作成しました。各データセットの要素は、このデータセットの要素を変換して生成されます。 ただし、このデータセットの要素自体は存在しないため、生成する必要があります。 はい、これらは遅延計算です。



今、すべてを一緒に始めます。



  Dataset.of(Arrays.asList("", "", "", "")) .union(Arrays.asList("", "", "")) .filter(s -> s.length() > 4) .map(s -> s + ", length=" + s.length()) .forEach(System.out::println);
      
      





結論:

高速道路、長さ= 5

吸い込まれた、長さ= 6

乾燥、長さ= 5


リファクタリングの時間です。 最初に目を引くのは、GeneratorContextインターフェースを標準のConsumerに置き換えることができることです。 交換してください。 以前はGeneratorContextでConsumerをラップする必要があったため、コードは一部の場所で削減されます。



ここでは、Datasetとjava.util.stream.Streamの間の特定の類似性を停止し、注意を払う価値があります。これは、GeneratorとJavaプラットフォームの神秘的なSpliteratorとの関係についての考えを示唆しています。



自転車の準備ができました。 Stream APIの内部メカニズム、Spliteratorの役割、および遅延計算を整理する方法も少し明確になることを願っています。



PS。 Gitレポはこちらです。



All Articles