javaの関数型プログラミング: コレクションの使用 -...

9
ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015 36 //functional programming / 2回シリーズの第1回の記事では、ラムダ式を活用してJavaの関数スタイ ル・プログラミングが持つ可能性を引き出す方法について説明しました。 ラムダ式を使うと、表現力が高く、可変性やエラーが少ない簡潔なコードを作 成できるようになります。最終回となる今回は、このことをさらに追求するととも に、注意すべき点についても考察します。今回の記事でお分かりいただけると思 いますが、ラムダ式を使うとたいへん簡潔なコードを書くことができます。その ため安易な使用は、コードの重複につながる可能性があります。重複のあるコー ドは、メンテナンス性が低く品質の悪いコードです。コード変更が必要になると、 複数箇所のコードを探して変更しなければなりません。 重複の回避は、パフォーマンスの改善にもつながります。特定の処理に関連 するコードを1箇所にまとめておけば、その処理のパフォーマンス特性の分析は 簡単になり、パフォーマンス強化のための変更も1箇所で済むことになります。 ラムダ式の再利用 ラムダ式の安易な使用は、重複したコードの作成につながります。まずはそ の実例を見た上で、それを回避する方法について考えてみましょう。ここでは、 friendseditorscomrades という3つの名前のコレクションがあるものとします。 final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott"); final List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike"); final List<String> comrades = Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach"); それぞれのコレクションに対して、ある文字で始まる名前をフィルタリングす ることを考えてみます。まずは、 filter() メソッドを使用した単純なアプローチを実 行します。 final long countFriendsStartN = friends.stream() .filter(name -> name.startsWith("N")) .count(); final long countEditorsStartN = editors.stream() .filter(name -> name.startsWith("N")) .count(); final long countComradesStartN = comrades.stream() .filter(name -> name.startsWith("N")) .count(); ラムダ式でコードは簡潔に書かれていますが、重複コードが多くなっていま す。これでは、ラムダ式の簡単な修正でも、複数箇所の変更が必要になるため、 効率的とは言えません。ただ、幸いなことに、ラムダ式はオブジェクトと同じよう に変数に格納して再利用できます。 ラムダ式ベースの関数ルーチンからコードの「匂い」を取り除く パート2 Javaの関数型プログラミング: コレクションの使用 VENKAT SUBRAMANIAM Venkat Subramaniam 博士: 受賞 歴を持つ 著者。Agile Developerの 創業者であり、 ヒューストン 大学の教授を 務めている。 国際会議で定 期的に講演を 行っており、 JavaOne Rock Star Awardを 複数回受賞。

Upload: others

Post on 26-Jul-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

36

//functional programming /

全2回シリーズの第1回の記事では、ラムダ式を活用してJavaの関数スタイル・プログラミングが持つ可能性を引き出す方法について説明しました。

ラムダ式を使うと、表現力が高く、可変性やエラーが少ない簡潔なコードを作成できるようになります。最終回となる今回は、このことをさらに追求するとともに、注意すべき点についても考察します。今回の記事でお分かりいただけると思いますが、ラムダ式を使うとたいへん簡潔なコードを書くことができます。そのため安易な使用は、コードの重複につながる可能性があります。重複のあるコードは、メンテナンス性が低く品質の悪いコードです。コード変更が必要になると、複数箇所のコードを探して変更しなければなりません。

重複の回避は、パフォーマンスの改善にもつながります。特定の処理に関連するコードを1箇所にまとめておけば、その処理のパフォーマンス特性の分析は

簡単になり、パフォーマンス強化のための変更も1箇所で済むことになります。

ラムダ式の再利用ラムダ式の安易な使用は、重複したコードの作成につながります。まずはその実例を見た上で、それを回避する方法について考えてみましょう。ここでは、friends、editors、comradesという3つの名前のコレクションがあるものとします。

final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scott");

final List<String> editors = Arrays.asList("Brian", "Jackie", "John", "Mike");

final List<String> comrades = Arrays.asList("Kate", "Ken", "Nick", "Paula", "Zach");

それぞれのコレクションに対して、ある文字で始まる名前をフィルタリングすることを考えてみます。まずは、filter()メソッドを使用した単純なアプローチを実行します。

final long countFriendsStartN = friends.stream() .filter(name -> name.startsWith("N")) .count();

final long countEditorsStartN = editors.stream() .filter(name -> name.startsWith("N")) .count();

final long countComradesStartN = comrades.stream() .filter(name -> name.startsWith("N")) .count();

ラムダ式でコードは簡潔に書かれていますが、重複コードが多くなっています。これでは、ラムダ式の簡単な修正でも、複数箇所の変更が必要になるため、効率的とは言えません。ただ、幸いなことに、ラムダ式はオブジェクトと同じように変数に格納して再利用できます。

ラムダ式ベースの関数ルーチンからコードの「匂い」を取り除く

パート2

Javaの関数型プログラミング: コレクションの使用

VENKAT SUBRAMANIAM

Venkat Subramaniam博士:受賞歴を持つ著者。Agile Developerの創業者であり、ヒューストン大学の教授を務めている。国際会議で定期的に講演を行っており、JavaOne Rock Star Awardを複数回受賞。

Page 2: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

37

//functional programming / 先ほどの例では、filter()メソッドにラムダ式を渡していますが、このメソッドは本来、

java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つまり、どういうことかというと、filter()メソッドに渡されたラムダ式をもとに、JavaコンパイラがPredicateのtest()メソッドの実装を合成しているのです。このように引数定義の場所でJavaにメソッドを合成してもらってもよいのですが、より明示的な手法を用いることもできます。今回の例では、Predicate型への明示的な参照にラムダ式を格納し、それを関数に渡すことができます。これが重複を排除するもっとも簡単な方法です。

では、DRY(Don’t Repeat Yourself、「重複の排除」)原則に従い、先ほどのコードをリファクタリングします。

final Predicate<String> startsWithN = name -> name.startsWith("N");

final long countFriendsStartN = friends.stream() .filter(startsWithN) .count();final long countEditorsStartN = editors.stream() .filter(startsWithN) .count();final long countComradesStartN = comrades.stream() .filter(startsWithN) .count();

ラムダ式を何度も重複して書く代わりに、1度だけ式を記述してPredicate型のstartsWithNという参照に格納しています。filterメソッドは3回呼び出されていますが、Javaコンパイラは変数に格納されたラムダ式を、まるでPredicateインスタンスであるかのように扱っています。

変数を新たに使用することで、紛れ込んでいた重複コードを取り除くことができました。しかし、次の例では、再び重複が紛れ込んできます。その対応には、さらに強力な手法が必要になります。

構文スコープとクロージャの使用開発者の中には、ラムダ式を使用すると重複が生まれ、コードの品質が低下しかねないと誤解する方もいます。事実はまったく逆で、より複雑なコードを書く場合でも、品質に妥協せずラムダ式で簡潔なコードを書くことができます。このセクションでは、この点について説明します。

先ほどの例で、ラムダ式を再利用することができました。しかし、別の文字でマッチングを行おうとすると、またすぐに重複が紛れ込んできます。それでは、最初にその問題を確認した上で、構文スコープとクロージャを使用して問題を解決します。ラムダ式の重複:ここでは、friendsコレクション内の名前から、「N」または「B」で始まる名前を選択してみます。先ほどの例を踏襲すると、次のようなコードを書きたくなるのではないでしょうか。

final Predicate<String> startsWithN = name -> name.startsWith("N");final Predicate<String> startsWithB = name -> name.startsWith("B");

final long countFriendsStartN = friends.stream() .filter(startsWithN) .count();final long countFriendsStartB = friends.stream() .filter(startsWithB) .count();

最初のPredicateは名前が「N」で始まるかどうか、2番目は「B」で始まるかどうかをチェックするものです。この2つのインスタンスを1回ずつ渡してfilter()メソッドを2回呼び出しています。これは一見合理的ですが、2つのPredicateは使っている文字が異なるだけで、単なる重複に他なりません。では、この重複はどのように排除すればよいでしょうか。

サンプルでは、関数を関数に渡す方法、関数の中に関数を作成する方法、関数から関数を返す方法を示しました。

Page 3: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

38

//functional programming /

構文スコープによる重複の排除:1つ目の方法として考えられるのは、文字をパラメータとして関数に渡し、その関数をfilter()メソッドに引数として渡す方法です。これは妥当な考え方ですが、filter()メソッドには任意の関数を渡すことはできません。filter()メソッドが受け取るのは、評価対象となるコレクションの1要素をパラメータとし、boolean型の結果を返す関数、つまりPredicateだけです。

比較を行うには、後で使用するために文字をキャッシュし、パラメータ(この例ではname)を受け取るまで保持しておく変数が必要になります。その目的に沿って関数を作成してみます。

public static Predicate<String> checkIfStartsWith(final String letter) { return name -> name.startsWith(letter);}

ここでは、パラメータとしてString型のletterを受け取るstatic関数checkIfStartsWith()を定義しています。この関数が返すPredicateは、後々に行う評価に備えてfilter()メソッドに渡すことができます。checkIfStartsWith()は、結果として関数を返しています。

checkIfStartsWith()が返すPredicateは、今まで見てきたラムダ式とは違います。return name -> name.startsWith(letter)のnameは、ラムダ式に渡されたパラメータであることは分かるでしょう。しかし、変数letterには何が入るのでしょうか。letterはこの匿名関数のスコープ内で定義されていないため、Javaはラムダ式の定義スコープまで変数letterを探しに行きます。これが、構文スコープと呼ばれるものです。構文スコープは、あるコンテキストで提供された値を後ほど別のコンテキストで使用するためにキャッシュしておく強力なテクニックです。このラムダ式は、定義のスコープを囲い込んでいるため、クロージャ(閉包)とも呼ばれています。

構文スコープにはいくつかの制限があることを覚えておくとよいでしょう。その1つは、ラムダ式の内部からは、ローカルのfinal変数、または外側のスコープで実質的なfinalであるローカル変数のみにアクセスできる点です。ラムダ式は、定義された直後に呼出される可能性もあれば、遅延呼出しや複数のスレッドから呼出される可能性もあります。競合状態を避けるため、アクセス対象となる外側のスコープのローカル変数は、一度初期化した後は変更できなくなっています。変更しようとすると、コンパイル・エラーが発生します。もちろん、final修飾子を付けた変数はこの条件を満たしますが、必ずしもそうする必要はありません。その代わりに、Javaは次の2つの条件を確認します。まず、アクセス対象の変数は、ラムダ式が定義される前に、外側のメソッドで初期化されている必要があります。次に、それらの変数の値が1度も変更されないことです。つまり、finalと明示していなくても、実質的にfinalであるということです。

なお、ステートレスなラムダ式はランタイムでは定数となりますが、ローカル状態をキャプ

チャするラムダ式を使用すると、評価の際に追加コストが必要となる点にも留意してください。以上の制限事項を考慮した上で、filter()メソッドの呼出しと、checkIfStartsWith()が返すラム

ダ式の使用方法について見てみます。

final long countFriendsStartN = friends.stream() .filter(checkIfStartsWith("N")) .count();final long countFriendsStartB = friends.stream() .filter(checkIfStartsWith("B")) .count();

filter()メソッドの呼出し時、最初に目的の文字を渡してcheckIfStartsWith()メソッドを呼び出しています。その結果、ラムダ式が即座に返され、それがfilter()メソッドに渡されます。

高階関数(この例ではcheckIfStartsWith()メソッド)を作成し、構文スコープを使用することでコードの重複を排除できました。名前が別の文字で始まるかどうかを確認するために、比較を繰り返す必要は無くなります。 スコープを制限するためのリファクタリング:先ほどの例では、staticメソッドを使用しました。しかし、これも「匂い」がするコードです。毎回変数をキャッシュするstaticメソッドでクラスを汚染したくはないでしょう。関数のスコープは必要な範囲に制限する方が望ましいのです。そこで、Functionインタフェースを使用します。

final Function<String, Predicate<String>> startsWithLetter = (String letter) -> { Predicate<String> checkStarts = (String name) -> name.startsWith(letter); return checkStarts;};

このラムダ式は、staticメソッドcheckIfStartsWith()の代わりとして使えます。関

Page 4: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

39

//functional programming /

数内部に記述できるので、使用箇所の手前に置くこともできます。startsWithLetter変数には、Stringを受け取ってPredicateを返すFunctionが格納されます。

この書き方は先ほどのstaticメソッドに比べると表現がかなり冗長ですが、後ほどリファクタリングを行って簡潔なコードにします。現実的な意味においては、この関数はStringを受け取ってPredicateを返すので、staticメソッドと同等です。明示的にPredicateのインスタンスを作成して返す代わりに、ラムダ式を使って書き換えることができます。

final Function<String, Predicate<String>> startsWithLetter = (String letter) -> (String name) -> name.startsWith(letter);

これで少しばかりきれいになりましたが、型情報を削除してJavaコンパイラにコンテキストに基づく型推論を行わせると、さらにもう1段階簡素化を進めることができます。次のコードがさらに簡素化したものです。

final Function<String, Predicate<String>> startsWithLetter = letter -> name -> name.startsWith(letter);

この簡潔な構文に慣れるには、多少の努力が必要でしょう。よく分からないと思ったら、しばらく横に置いても差し支えありません。これで、リファクタリングが完了しました。次に、これをcheckIfStartsWith()の代わりに使ってみます。

final long countFriendsStartN = friends.stream() .filter(startsWithLetter.apply("N")) .count();final long countFriendsStartB = friends.stream() .filter(startsWithLetter.apply("B")) .count();

このセクションでは、いくつかの高階関数に着目しつつ、一巡りして最初の内容に戻ってきました。サンプルでは、関数を関数に渡す方法、関数の中に関数を作成する方法、関数から関数を返す方法を示しました。ラムダ式の活用によって実現できる簡潔性と再利用性もお分かりいただけたのではないかと思います。

ところで、このセクションで頻繁に利用したFunctionとPredicateの違いは何でしょうか。Predicate<T>は、T型のパラメータを1つ受け取り、何かをチェックした結果としてboolean型を返します。候補値を渡して真か偽かの判定を行いたい場合は、いつでも利用できます。filter()のように候補となる要素を評価するメソッドは、パラメータとしてPredicateを受け取ります。

一方のFunction <T, R>は、T型のパラメータを受け取り、R型の結果を返す関数を表します。これは、常にbooleanを返すPredicateよりも汎用的です。ある入力値を別の値に変換したければ、どこでもFunctionを利用できます。そう考えると、map()メソッドのパラメータにFunctionを指定できるのは理にかなっていると言えるでしょう。

コレクションから複数の要素を選択するのは簡単でした。次は、コレクションから1つだけ要素を取り出す方法について見てゆきましょう。

Page 5: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

40

//functional programming /

要素の取出しコレクションから1つの要素を取り出すというのは、複数の要素を取り出すより簡単だと感じるのではないでしょうか。しかし、いくつかの厄介な問題が出てきます。それでは、よく使うアプローチが抱える厄介な問題を確認した上で、ラムダ式を使ってその問題を解決してみましょう。

ここでは、指定の文字で始まる要素を探し、それを表示するメソッドを作成します。

public static void pickName( final List<String> names, final String startingLetter) { String foundName = null; for(String name : names) { if(name.startsWith(startingLetter)) { foundName = name; break; } } System.out.print( String.format( "A name starting with %s:", startingLetter));

if(foundName != null) { System.out.println(foundName); } else { System.out.println("No name found"); }}

このメソッドの「匂い」は、ゴミ収集車と同じくらい強烈です。最初にfoundName変数を作成し、nullで初期化しています。この点が最初の悪臭の発生源になっています。この処理によってnullチェックが必須になり、その処理を忘れると、NullPointerExceptionなどの望まない結果になってしまいます。次に、外部イテレータを使って全要素をループしますが、目的の要素を見つけた場合には、ループを脱出しなければなりません。このプリミティブ型への執着、命令型スタイル、可変性がもう1つの悪臭の発生源です。ループを脱出した後も、応答をチェックして適切な結果を表示しなくてはなりません。単純な処理の割に、コードは複雑です。

ここで、もう一度考察してみます。行いたいことは、最初に一致した要素を取り出すことと、要素が存在しない場合も安全に対応することです。それを踏まえ、ラムダ式を使ってpickName()

メソッドを書き換えてみます。

public static void pickName( final List<String> names, final String startingLetter) { final Optional<String> foundName = names.stream() .filter(name -> name.startsWith(startingLetter)) .findFirst(); System.out.println( String.format( "A name starting with %s:%s", startingLetter, foundName.orElse("No name found")));}

いくつかのJDKライブラリの強力な機能が集まって、この簡潔さを実現しています。まず、filter()メソッドを使用して、目的のパターンに一致するすべての要素を取得します。次に、

StreamクラスのfindFirst()メソッドを使って、コレクションの最初の値を取り出します。このメソッドは、特別なOptionalオブジェクトを返します。これは、Java公式のnull消臭剤と言えるようなクラスです。

Op t i o n a lクラスは、結果が存在しない可能性 が 想 定 さ れ る 場 合 に 便 利 で す。偶 発 的 なNullPointerExceptionの発生を防ぐとともに、コードの読者に「結果が存在しない」可能性があることを明示的に示すことができます。オブジェクトが存在するかどうかはisPresent()メソッドで問い合わせることができ、get()メソッドで現在の値を取得できます。もう1つの方法として、一風変わった名前を持つorElse()メソッドを使用して、存在しないインスタンスの代替値を指定することもできます。先ほどのコードでは、この方法を使っています。

プログラミングにおいては、コレクションは一般的なものです。ラムダ式のおかげで、Javaのコレクションは今まで以上に簡単かつシンプルに利用できるようになりました。古く回りくどいメソッドによってではなく、美しく簡潔なコードによって一般的なコレクション操作を行えるようになっています。

Page 6: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

41

//functional programming /

次に、これまでの例と同じく、friendsコレクションに対してpickName()関数を実行してみます。pickName(friends, "N");pickName(friends, "Z");

このコードは、最初に一致した要素が存在する場合、それを取り出します。存在しない場合は、適切なメッセージを表示します。

A name starting with N:NateA name starting with Z:No name found

findFirst()メソッドとOptionalクラスを組み合わることで、コードの量が減り、「匂い」もかなり軽減されました。なお、Optionalにはこの例とは違う使用法もあります。たとえば、存在しないインスタンスに別の値を割り当てるのではなく、値が存在する場合のみOptionalにコード・ブロックやラムダ式を実行させることができます。次の例をご覧ください。

foundName.ifPresent( name -> System.out.println("Hello " + name));

最初に一致した名前を取り出す方法として、命令型スタイルのコードより関数型スタイルの流れるようなコードのほうがはるかに美しくなっています。それでいて、この流れるようなコードは、命令型のコードよりも作業量が増えているでしょうか。そんなことはありません。関数型スタイルのメソッドは、必要最低限の作業のみを実行する賢いものです。

JDKには、最初に一致する要素を検索するうえで役に立つ便利な機能が他にもいくつか存在しています。次は、ラムダ式を使ってコレクションから1つの結果を計算する方法について見てみましょう。

コレクションの単一値への集約ここまでで、一致する要素の取り出し、特定の要素の選択、コレクションの変換など、コレクションを操作するいくつかのテクニックを見てきました。この3つの操作には、1つの

共通する点があります。それは、コレクションの個々の要素に対して独立した処理を行っていることです。つまり、要素同士を比較したり、計算した値をある要素から隣の要素に引き継ぐというような処理は必要ありませんでした。このセクションでは、要素同士を比較して、コレクション全体にわたって計算状態を引き継がせる方法を説明します。

まずは基本的な操作から始めて、それから洗練されたコードを作成します。最初の例では、名前のコレクションであるfriendsの値を読み出し、合計文字数を計算します。

System.out.println( "Total number of characters in all names:" + friends.stream() .mapToInt(name -> name.length()) .sum());

合計文字数を計算するためには、それぞれの名前の長さが必要です。これは、mapToInt()メソッドによって簡単に計算できます。名前を長さに変換した後、最終ステップとしてすべての長さを合計します。これは、組込みのsum()メソッドで計算できます。出力結果は、次のようになります。

Total number of characters in all names:26

ここでは、map(マッピング)操作の一種(他にmapToInt()、mapToDouble()などがあり、IntStream、DoubleStreamなどのそれぞれの型用のストリームを作成できます)であるmapToInt()メソッドを使用し、次に長さを合計値にreduce(集約)しています。

sum()メソッドを使用する代わりに、最長の長さを求めるmax()、最短の長さを求めるmin()、長さでソートを行うsorted()、長さの平均を求めるaverage()など、さまざまなメソッドが使用できます。

先ほどの例にはある秘密が隠されています。それは、人気上昇中のMapReduceパターンです。この例では、map()メソッドがspread操作であり、sum()メソッドは汎用的なreduce操作の特別なケースにあたります。実際、JDKでのsum()メソッドの実装にはreduce()メソッドが使用されています。では、reduce操作の一般的な形態を見てみましょう。

しかし、新しく追加されたjoin()メソッドがなければ、今まで実現してきた簡潔で美しいコードが台無しになったかもしれません。このシンプルなメソッドは非常に使い勝手がよいため、JDKでもっともよく使用される機能の1つにまでなっています。

Page 7: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

42

//functional programming /

例として、指定された名前コレクションを読み取り、一番長い名前を表示してみます。最長の名前が複数ある場合は、最初に見つけたものを表示します。実現方法として、まず最長の長さを調べ、次にその長さの要素を取り出すというものが考えられます。しかし、この方法ではリストを2回走査する必要があります。これは効率的とは言えません。このような場合、reduce()メソッドが役立ちます。

reduce()メソッドを使用すると、2つの要素を比較し、その結果をさらにコレクションの残りの要素と比較できます。コレクションを処理する高階関数がここまでに出てきましたが、そうした関数と同様に、reduce()メソッドもコレクションの反復処理を行います。しかも、ラムダ式が返す計算結果を引き継げます。例を見た方が分かりやすいでしょう。

final Optional<String> aLongName = friends.stream() .reduce((name1, name2) -> name1.length() >= name2.length() ? name1 : name2); aLongName.ifPresent(name -> System.out.println( String.format("A longest name:%s", name)));

reduce()メソッドに渡すラムダ式には、name1とname2という2つのパラメータがあり、長さに応じていずれかを返します。reduce()メソッド自体は、行いたいこととは無関係です。行いたいことは渡されるラムダ式に記述されており、reduce()メソッドから分離されています。つまりこれは、Strategyパターンを持つ軽量アプリケーションです。

このラムダ式は、JDKの関数インタフェースBinaryOperatorのapply()メソッドに準拠したもので、reduce()メソッドが受け取ることができるのはこのタイプのパラメータです。それでは、reduce()メソッドを実行して、friendsリストにある2つの最長の名前のうち最初のものが取り出されるかどうかを確認します。

A longest name:Brian

reduce()メソッドは、コレクションの反復処理を行う際に、まずリストの最初の2つの要素を使用してラムダ式を呼び出します。そのラムダ式の実行結果は、以降の呼出しで使用されます。2回目の呼出しでは、name1は先ほどのラムダ式の結果となり、name2はコレクションの3番目の要素になります。あとは、コレクションの残りの要素についてラムダ式の呼出しが繰り返されます。最後の呼出しの結果が、reduce()メソッドの結果として返されます。

reduce()を実行するリストは空である可能性があるため、reduce()メソッドの結果はOptionalです。その場合、最長の名前は存在しません。リストの要素が1つだけの場合は、reduce()はその要素を返し、ラムダ式は呼び出されません。

この例から、reduce()メソッドはコレクションの中から1個の要素を返すか、または何も返さないかのいずれかであることが分かるでしょう。デフォルトの値や基準値を設定したい場合は、オーバーロードされたreduce()メソッドの追加パラメータとしてその値を指定します。たとえば、「Steve」より短い名前は取り出したくない場合、次のようにreduce()メソッドを呼び出します。

final String steveOrLonger = friends.stream() .reduce("Steve", (name1, name2) -> name1.length() >= name2.length() ? name1 : name2);

指定された基準値(この例では「Steve」)よりも長い名前があればそれが取り出されますが、無かった場合は基準値自身が返されます。この形式のreduce()はOptionalを返しません。コレクションが空でもデフォルト値が返されるため、値が存在しないケースを心配をする必要はありません。

それでは、最後のまとめに入る前に、もう1つの基本的なコレクション操作について考えてみましょう。一見難しそうに見える要素の連結です。

要素の連結ここまでは、コレクションの要素を選択する方法と、コレクションの反復処理や変換を行う方法について学んできました。しかし、新しく追加されたjoin()関数がなければ、コレクションの連結という平凡な操作が、今まで実現してきた簡潔で美しいコードを台無しにしたかもしれません。このシンプルなメソッドは非常に使い勝手がよいため、JDKでもっともよく使用される機能の1つにまでなっています。それでは、この機能を使ってコレクションの値をカンマ区切りリストで表示する方法を見てみましょう。

Page 8: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

43

//functional programming /

今までと同じく、friendsリストを対象にします。古いJDKライブラリだけでカンマ区切りの名前のリストを表示するには、どうすればよいでしょうか。

必要な処理は、リストに対する反復と各要素の表示です。Java 5で拡張されたfor構文は原始的なforループより優れているので、そこから始めることにしましょう。

for(String name : friends) { System.out.print(name + ", ");}System.out.println();

シンプルなコードですね。実行すると、次のように表示されます。

Brian, Nate, Neal, Raju, Sara, Scott,

問題にお気づきでしょうか。末尾に、余分なカンマがついています。この最後のカンマを消すには、どうすればよいでしょう。残念ながら、このループでは細かい制御はできず、最後の要素かどうかを簡単に判定する方法はありません。そのため、従来型のループを使う方法に戻ってこの問題に対応します。

for(int i = 0; i < friends.size() - 1; i++) { System.out.print(friends.get(i) + ", ");}

if(friends.size() > 0) System.out.println( friends.get(friends.size() - 1));

きちんと結果が出力されるでしょうか。

Brian, Nate, Neal, Raju, Sara, Scott

結果は上々です。しかし、コードのほうはそうではありません。では、最新のJavaの力を借りるとしましょう。

最新のJavaでは、もうこの問題に悩まされることはありません。Java 8のStringJoinerクラスがこの問題をすべて解決してくれます。また、Stringクラスにもjoin()という便利なメソッドが追加されています。こういった機能によって、「匂う」コードを美しく1行で書き換えることができま

す。System.out.println(String.join(", ", friends));

念のため、コードと同じく結果も美しいことを確認しておきましょう。

Brian, Nate, Neal, Raju, Sara, Scott

Stringクラスのjoin()メソッドは、2番目の引数、varargs(可変引数)の値を連結し、最初の引数で区切られた文字列を作成します。この処理は、内部的にStringJoinerを呼び出すことで実現されています。この機能は、カンマで連結するだけのものではありません。この新しいメソッドとクラスを使うと、複数のパスを受け取って連結し、簡単にクラスパスを作成するようなこともできます。

また、リストの要素を単純に連結するだけでなく、要素の変換を行ってから連結することも可能です。要素の変換には、すでに説明したとおりmap()メソッドを使います。また、filter()などのメソッドを使うと、一部の要素のみを抜き出してから連結することもできます。カンマや任意の文字で区切られた要素を最終的に連結しますが、それはつまりreduce操作です。

reduce()メソッドで要素を連結して文字列を作成することもできますが、それには多少の労力が必要です。JDKには、collect()という便利なメソッドがあります。これはreduce操作の一種で、複数の値を1つにまとめる際に有効です。

collect()メソッドもreduce操作を行いますが、その実装(ターゲット)はコレクタに委譲されています。たとえば、変換した要素をArrayListに格納することができます。あるいは、現在の例で説明を続けるなら、変換した要素をカンマ区切りの文字列にまとめることができます。

System.out.println( friends.stream() .map(String::toUpperCase) .collect(joining(", ")));

Page 9: Javaの関数型プログラミング: コレクションの使用 - Oracle...java.util.function.Predicate関数インタフェースの参照を受け取ることになっています。つま

ORACLE.COM/JAVAMAGAZINE /////////////////// SEPTEMBER/OCTOBER 2015

44

//functional programming /

ここでは、変換したリストに対してcollect()メソッドを呼び出し、その結果をjoining()メソッドが返すコレクタに渡しています。joining()メソッドは、Collectorsユーティリティ・クラスの静的メソッドです。コレクタは、collect()メソッドから要素を受け取るシンク・オブジェクトで、要素を目的の形式、すなわちArrayList、Stringなどに保存します。

先ほどのコードからは、大文字に変換された名前がカンマ区切りで出力されます。

BRIAN, NATE, NEAL, RAJU, SARA, SCOTT

StringJoinerを使うと、連結形式を細かく制御できます。たとえば、必要に応じて接頭辞や接尾辞、挿入辞の文字列を指定できます。

まとめこの2回シリーズを通して見てきたように、ラムダ式と新しく追加されたクラスやメソッドによって、Javaプログラミングははるかに簡単で楽しいものになっています。

プログラミングにおいては、コレクションは一般的なものです。ラムダ式のおかげで、Javaのコレクションは今まで以上に簡単かつシンプルに利用できるようになりました。古く回りくどいメソッドによってではなく、美しく簡潔なコードによって一般的なコレクション操作を行えるようになっています。内部イテレータを使うと、可変性を排除しつつ手軽にコレクションの探索や変換を行えるようになり、簡単にコレクションから要素を選択することもできます。こういった機能を使えばコードの量は減少し、メンテナンス性は高まり、業務領域やアプリケーション・ロジックに重点を置いてコードを作成できます。もちろん、コーディングにおける初歩的な事項を処理するためのコードは少なくなります。</article>

本記事は、出版社のThe P r a gma t i c B oo k s h e l fのご厚意により、『Func t i o n a l Programming in Java: Harnessing the Power of Java 8 Lambda Expressions』から抜粋および要約したものです。

LEARN MORE• 関数型プログラミングの概要

• Cay HorstmannによるJava 8でのラムダ式の使用についての説明