java8 Gold ストリームAPI②

【資格】java8-Oracle Gold

おはようございます。

今回は、

java 8 gold資格試験の出題範囲からストリームAPIについて解説します!

はじめに

ストリームAPIはデータの集合を効率よく操作するための仕組みです。

ストリームという呼び名はデータをパイプラインのような処理群に流して、その中でひとつ一つのデータに対して処理が行われていく、、、

という流れのイメージから着けられたようです。

ここで注意したいのが、ここで言うStreamAPIは、java7までのInputStreamやPrintStreamとは全く異なるものなので、整理して覚えておきましょう。

本単元はjava8で追加された機能の中で、試験としてはかなり重要視されている部分です。

必然的に覚えるべき情報は増えますが、慣れると結構楽しい分野ですので、効率よく勉強を進めていきましょう。



【対象者】本記事は、Oracle Certified Java Programmer, Gold SE 8 認定資格試験の出題分野の解説をいたします。特に以下のような方には読んでいただければ幸いです。

  • 当該試験を受ける方
  • java 11 Goldの試験を受ける方でStreamAPIの復習をしたい方
  • 受験の有無にかかわらず、StreamAPIの使い方をおさらいしたい方
  • 普通にド忘れしたプログラマーの方

出題範囲の確認

公式ページの出題内容は以下の通りです。

【Java ストリーム API】

  1. 基本バージョンの map() メソッドを含む peek() および map() メソッドを使用してオブジェクトからデータを抽出する
  2. findFirst、findAny、anyMatch、allMatch、noneMatch などの検索メソッドを使用してデータを検索する
  3. Optional クラスを使用する
  4. ストリームのデータ・メソッドと計算メソッドを使用するコードを作成する
  5. ストリーム API を使用してコレクションをソートする
  6. collect メソッドを使用してコレクションに結果を保存する。Collectors クラスを使用してデータをグループ化/パーティション化する
  7. flatMap() メソッドを使用する

参考(https://education.oracle.com/ja/java-se-8-programmer-ii/pexam_1Z0-809)


今回も引き続き残りのクラスを解説するとともに、上記の内容の意味するところを読み解いていければと思います!

学習の流れ

  • 基礎、1,2(前半)
  • 3~7(後半)

今回は後半です。是非内容を整理して学習してみてください。

それでは、内容どうぞッ!

内容

(3)Optional クラスを使用する

【概要】

java.util.Optional は、値がnulである可能性を明示することを目的として
java8で実装されたクラスです。(今更ですが、ストリームの要素には”Optionalオブジェクトに包んで”と言う条件は付きますが、nullを格納することはできます)

ストリーム専用の型というよりは、他の用途で値をラッピングして使っても大丈夫です。「このオブジェクトはnullかもしれないよ」と示したい時、使うことができます。

下にも載せていますが、

ofNullable ()と言うメソッドでは

引数に指定されたT型オブジェクトがnullの場合は空のOptional<T>オブジェクトを返します。

このように、中身がnullか?に着目して動作するメソッドが実装されているクラスです。

メソッド

ちょっと全部出ることはないと思いますが、さらっと名前と動作ぐらいは見ておいていただければと思います。

定義動作
static <T> Optional<T> empty()空のOptional<T>インスタンスを返す
boolean equals()①Optional型 ②両方のインスタンスに値が存在しない、もしくは存在する値がequals()判定で等しい。といった条件でちゃんとOverrideされています。
Optional<T> filter(Predicate<? super T> p)値が存在する時に、それが引数の条件に合えば要素を返し、そうでなければ空のOptionalインスタンスを返します。
T get()普通に要素を返します
int hashCode()Orverrideちゃんとされています。
void ifPresent(Consumer<? super T> c)値が存在するときは引数のコンシューマの動作を実施。なければ何もしない。
boolean isPresent()値があればtrue,なければfalse
<U> Optional<U> map(Function<? super T,? extends U> f)前回説明したmapメソッドです。Optionalクラスの実装だったわけです。
static <T> Optional<T> of(T t)前回説明したofメソッドです。ただ、ここで注意なのが引数がnullだとダメです。NullPointerExceptionになります。
static <T> Optional<T> ofNullable(T t)引数の値がnullでなければそれを要素としたOptionalオブジェクト、引数がnullなら空のOptionalオブジェクトを返します。上のメソッドとの違いは結構大事です。
T orElse(T other)そのOptionalオブジェクトに値があればその値を、なければ引数のotherを返します。
T orElseGet(Supplier<?extends T> other)そのOptionalオブジェクトに値があればその値を、なければ引数のラムダ式の処理結果の返り値を返します。
<X extends Throwable > T orElseThrow(orElseGet(Supplier<?extends T> ex)そのOptionalオブジェクトに値があればその値を、なければ引数のラムダ式の処理結果が作成した例外をスローします。

(4)データ・メソッドと計算メソッドを使用する

foreEach(Consumer<? super T> c) 終端

forEach()メソッドは、引数に入ってきた関数型インタフェースインスタンス、またはラムダ式の操作を、各要素に実施するメソッドです。

例えば、MapやListのようなコレクションクラスの要素全てを標準出力する、と言った操作が出来るので確認やサンプルでよく使われるメソッドです。

count()

count()を始め、これらのメソッドはリダクションと言われます。
要素が複数ある中で、それらの結果を集計して一つのものを返す、という動作です。

名前の通り、このメソッドはlong型(型に注意)で要素の数を返すメソッドです。

max()

最大の要素を返すメソッドになります。基準としては、
Comparatorのラムダ式を入れることによって、そこに書かれた比較基準で大喬&小喬関係を決めます。

戻り値の型はOptional型です。

min()

maxと同様、比較基準をComparatorインスタンスやラムダ式を入れてあげることによって設定し、それに従って最小の要素を返すものになります。

こちらも、戻り値の型はOptonal型になります。

ほかにも

リダクション処理は、各インタフェースに対して専用メソッドがあります。

Streamインタフェースであれば、上記で述べた3メソッドですし、
IntStreamインタフェースであれば、以下のようなものがあります。

  • long conut()
  • OptionalDouble average()
  • OptionalInt max()
  • OpiontalInt min()
  • int sum()

注意なのが、直接値を返すのではなく、Optional~型の値を返すものに関しては、
getInt() などのゲッタメソッドで取得しないと値そのものは手に入らないことです。

まぁ、eclipseで実装する時に困るかと言われたらそうはならないと思いますが、実際試験に出るときは戻り値の型をよく覚えておきましょう。

reduce()

これは、リダクション操作の中でも最も自由な、独自リダクション処理を作ることのできるメソッドです。例えばIntStreamインタフェースに用意されているものだと、以下のものがあります。

  • OptionalInt reduce(IntBinaryOperator op)
  • int reduce(int firstValue ,IntBinaryOperator op)

使い方の違いですが、一つ目のものは分かりやすいです。2つの引数が入ってきた時に何をするか記述したラムダ式を入れてやれば、最初の二つの処理結果とその次、さらにその処理結果とその次の要素、、、と言った形で処理を進めてくれます。

引数が2つのreduceに関しては、
だいたい似たようなことをしてintの値を作っていく処理になるのですが、その処理結果に対して、初期値を設定することができます。つまり、最終的に処理の結果と第一引数が足し合わされたものが返り値となるのです。

(5)コレクションをソートする

sorted() 中間操作

ストリームの要素を並び替えてくれるメソッドです。2種類あり、以下のような違いがあります。

Stream<T> sorted()自然順序で並び替える
Stream<T> sorted(Comparator<? super T> c)引数の比較基準に従って並び替える

comparing()

上記のsortedに関して、Comparatorをクラスごとにいちいち作らなくても、
選択したキーとなる値について並び替えるためのComparatorを提供してくれます。comparingが引数が1つのものと2つのもの、comparingIntなどのプリミティブ型のものがあります。

(6)collect メソッド/Collectors クラス

可変リダクション

登場人物

  • 可変コンテナ:要素を入れておく入れ物。コレクションクラスや配列、StringBuilderオブジェクトもこれに入ります。中に何らかの要素を持って、それを操作できる、というのが特徴です。概念的なものなので、実装上それを定義するコードはありません。
  • コレクタ:可変コンテナにいろいろ操作をしつつしまう人
  • 可変リダクション操作用メソッド:コレクタが使う、

可変リダクションというのは、概念的な説明をすると、
何らかの可変コンテナに要素を格納する操作
という操作になります。ストリームAPIで具体的に実現できることには、

  • ストリームの要素をそのままリストに出力する
  • ストリームの要素から最大要素や、一定の範囲に当たるものを所定のコレクションや配列に要素として出力する
  • ストリームの要素を任意のルールに基づいて入れ子構造のリストの形にして出力する

と言ったことが考えられます。(ここは、結構筆者の偏見と想像力と知見の欠如がある部分です、、、もっと正しい方法で活用することができている方はコメントで入れ知恵をしていただけたらありがたいです。。。)

ここが結構最初分かりにくい概念かもしれません。分量が増えすぎるので、
今後さらに詳細と分かりやすい解説を別記事で追加していきたいと思います。

コレクタ

上記の、可変コンテナに対して可変リダクション操作を実現するオブジェクトがこの
コレクタ(収集器)というものになります。
散らかった部屋に置いてあるものを棚に並べる掃除人、をイメージしてください。
もしくは、求職者を必要な部署に配置する人事担当です笑
~~~~~
また、コレクタもどれをどこにどうやって、どんな操作をして棚にしまうべきかを
決めてあげる必要があります。

設計者が入力として入れる可変リダクション操作は、
掃除人が片付けの時に従うルール、というイメージで覚えてください。

例えばストリームの要素をListに格納したいような場合にも、こうした
「仕事をする人」
「仕事のルール」
が必要であることがご理解いただけると思います。

そして、これを実現するのがjava.util.stream.Collector<A,T,R>インタフェースです。
Collectorインタフェース の中で、可変リダクション操作は以下の操作で実現されます。
また、一つ一つの操作にはCollectorインタフェース内のメソッドが紐づいています。

1. 前処理:結果を格納する新しい可変コンテナを作成する操作に関わる。

2. 蓄積:可変コンテナに値を格納するためのリダクション処理に関わる。

3. 結合:2つの部分的な結果を受け取ってマージする処理に関わる。並列処理でのみ使う

4. 後処理:中間結果となる可変コンテナの要素に処理を施して最終結果の可変コンテナに入れる操作に関わる。
操作メソッド概要
前処理Supplier<A> supplier()可変コンテナ作成を行うSupplierオブジェクトを返す
蓄積BiConsumer<A,T> accumulator()可変コンテナに値を格納するリダクション操作を実装しているBiConsumerオブジェクトを返す
結合BinaryOperator<A> combiner()2つの部分的な結果をマージする処理を実装するオブジェクトを返す。(2つのプロセスの結果を足し合わせる、など)
後処理Function<A,R> finisher() 中間コンテナ要素に処理をしつつ、最終コンテナ要素に入れる操作を実装するFunctionオブジェクトを返す

collectメソッド

混同しないように注意ですが、collectメソッドはCollectorインタフェースではなく、Streamインタフェースのメソッドです。
Collectorインタフェース とのつながりとしては、
collectメソッドの引数に先ほどのCollectorインタフェース のオブジェクトが入ります。

collectメソッドにはオーバーロード含め2種類あります。

一つ目が、前の節で説明したような、
各種操作を内包するCollectorオブジェクトを引数として取得するもの、

二つ目が、
示すようなCollectorインタフェースを取らずに、
上で述べた前処理に始まる操作を行うオブジェクトを直接引数として取得するものです。

何が違うの?と思われるでしょうが、両者は大体同じことができるので、使い方であると思っています。
ご想像がつくかもしれませんが、自由度高く作るなら、最初から二つ目のメソッドを使った方がCollectorインタフェースに包む手間がない分楽です


それでは、一つ目のメソッドのメリットって何? となると思います。
これは、Collectorのオブジェクトは、標準で用意されているものがあります笑
こういうことです。大体の目的に沿ってやりたいことを作ってくれてあるので、

「標準ライブラリにあるような基本操作なら自分で書かずに
 標準のCollectorオブジェクト使ってね
」ということです。
これを取得したい場合は、Collectorsクラスを使いましょう(「~s」ってクラス多いですよね笑 基本操作用オブジェクトはこういうクラスで提供する、という思想のようです)

以下のメソッドは、すべてCollectorオブジェクトを返します。

メソッド名Collectorオブジェクトに書かれている操作
counting要素の数をカウントします。
groupingBy指定されたFunnctionに従って要素をグループ化し、結果をMapに格納して返します。
joining入力要素を検出順に連結して一つのString文字列を作ります。
maxBy指定されたComparatorに基づいてOptionalとして記述された最大要素を生成します。
minBy指定されたComparatorに基づいてOptionalとして記述された最小要素を生成します。
reducing指定されたBinaryOperatorを使って、入力要素のリダクションを行います。
toList入力要素を新しいListに格納します。
コレクションの自由度が高いtoCollectionメソッドもあります。
toSet入力要素を新しいSetに格納します。
toMap入力要素を新しいMapに格納します。オーバーロード含めて3種類、
並列処理用のConcurrentMapメソッドが4種類あります。
summarizing~系後ろにプリミティブ型の名前が入ります。
入力要素に対する、統計的なサマリー結果を返します。
summing~系後ろにプリミティブ型の名前が入ります。
入力要素の合計値を返します。
partitioningByPredicateで決めた基準に従ってtrue,falseがキーでバリューがListになっているMapの要素として振り分けてくれます。2つあります。
averaging系後ろにプリミティブ型の名前が入ります。
入力要素の平均値を返します。

collectメソッドとreduceメソッド

reduceメソッドの違いを見てみましょう。
公式リファレンスの解説から、

以下のシグニチャを持つcollectメソッドでは、

<R> R collect(Supplier<R> supplier,
              BiConsumer<R,? super T> accumulator,
              BiConsumer<R,R> combiner)

内部でこのような操作が起こっています。(combiner並列処理で使うものなので今回は無視してください。)

R result = supplier.get(); //要素の入れ先である、supplierオブジェクト(List,Setといった入れ物)
for (T element : this stream)
    accumulator.accept(result, element); //accumulatorを使用して、supplierを更新
return result;

そして、以下のシグニチャを持つreduceメソッドでは、

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

内部操作は以下のようなものになります。

U result = identity; //要素の初期値である、identityオブジェクト
                                //(intやdoubleなどの演算が行われるような値)
 for (T element : this stream)
     result = accumulator.apply(result, element) //accumulatorを使用して、identifyを初期値とした演算
 return result;

気にしていただきたいのは、accumulatorを使用してやっている操作が、

  • collectでは
    • voidのメソッドでコレクションなどに要素の値を入れる更新
    • 最後に返すのは更新後のコンテナたるオブジェクト
  • reduceでは入力値の型が引数のメソッドで
    • 入力値の型を戻り値とするメソッドで初期値に要素の値を使った計算を行う(加減乗除など)
    • 最後に返すのは計算結果

と言ったように、
collect()の結果は複雑な構造を許容するコレクションなどのオブジェクトなのに対し、
reduce()の結果は単一の値、オブジェクトです。

内部に複数のデータを持つ物を扱うのが可変リダクション
単一の値を更新して返すのが通常のリダクション

というイメージを持っていただければ、あとはコードを見て実装やセオリーを知るだけかと思います。

(7)flatMap() メソッドを使用する

flatMap()

ストリームの要素にリストや配列が含まれている場合、
それらをすべて同列のストリーム要素に落とし込んでくれる便利メソッドです。

merged()

java se8でjava.util.Mapインタフェースに追加されたdefaultメソッドです。
返り値はバリューの型となっています。
引数に(キー,バリュー,BiFunction)をとり、キーとバリューが存在する場合にはバリューを返し、逆にキーが存在しなかったり、バリューがnullだと、返り値にBiFunctionの戻り値を返す、というものになっています。

まとめ

今回はjava 8 Gold試験対策として、ストリームAPIの分野について解説しました。

長い話になりましたが、お付き合いいただきありがとうございます。

前回も言いましたが、

この分野は非常に重要度の高い分野です。ですので、

手を動かすこと、腰を据えて暗記をしてみる、などあの手この手で定着を図っていただければと思います!

それでは、次回またお会いしましょう〜。

コメント

タイトルとURLをコピーしました