まだまだ夜は寒い季節ですが、新年度とともに Java の世界にも一足早く新しい風が吹いてきました。 Java5 でジェネリクスが導入されたときも結構インパクトがありましたが、Java8 でのラムダ式はそれ以上のインパクトがあり、プログラミングのパラダイム・シフトを余儀なくされることでしょう。
そうは言ってもラムダ式、関数オブジェクトは現代的なプログラミング言語では大抵サポートされている機能でもあるので、今更感の強い人もいることでしょう。 そういう方でも、多言語で実現されてた機能が Java8 ではどのように使うのか?ってことは確認しておく価値はあると思います。 ってことでラムダ式、関数オブジェクトに関して基本的な事項を Java8 のコードで見ていきまする。
内容
参考
- OpenJDK: Project Lambda
- Java 8 vs Scala: a Feature Comparison
- Project Lambda in Java SE 8
- java.util.function パッケージ
ラムダ式
拙者は「ラムダ式」とか「関数型プログラミング」とかに関しては初心者なので、ラムダ式の定義とかに関しては深入りしないことにして、記法・文法だけに焦点をあてます。 Java8 でのラムダ式は Scala のものとほとんど同じ様です。 ちょっとした違いは、引数と本体を分ける矢印が「=>」ではなく「->」になっているくらいです(引数の型を省略した場合)。
基本的な記法
まずは関数リテラルの記法を見ていきましょう。 int 値1つをを引数にとって、int 値を返す関数は
(int i) -> i * 2;
と書きます。 矢印「->」の前に関数の引数を「(int i)」のように書き、矢印の後に関数の処理内容を書きます(「i * 2」の部分)。 これは無名クラスを使うと次のように書いたのと同じです:
new AnonymousFunction1(){ // AnonymousFunction1 というクラスってないけどね public int apply(int i){ return i * 2; } }
無名クラスの場合に比べて、クラス名、メソッド名、返り値の型、return キーワードなど、あれこれいろいろ省略して、かなり簡略化して書けるようになってます。 引数が2つの場合
(int i0, int i1) -> i0 + i1;
となり、これは
new AnonymousFunction2(){ public int apply(int i0, int i1){ return i0 + i1; } }
と同じです。 引数が3つ以上の場合は説明が不要でしょう。 引数がない場合は
() -> Math.PI * 2.0;
のように矢印「->」の前に「()」を付けます。 関数の処理を複数行にわたって書きたい場合*1は
(int i) -> { int prod = 1; for(int n = 0; n < 5; n++) prod *= i; return prod; };
のように、処理部分を中括弧「{}」で囲みます*2。 この場合は return キーワードは省略できません。
型推論
さて、上記の基本的な記法では引数の型を真面目に書いてましたが、大抵の場合は型推論(type inference)*3の機能によって型宣言を省略することができます。 例えば int 値2つをとり int 値を返す以下のような IntOperator インターフェース
@FunctionalInterface interface IntOperator{ int apply(int i0, int i1); }
があった場合、この型の変数にラムダ式(によって作った関数オブジェクト)を代入するなら
IntOperator op = (i0, i1) -> i0 + i1;
のように、引数の型を省略することが出来ます。 引数が1つの場合は括弧「()」も省略できて
@FunctionalInterface interface IntFunction{ int apply(int i); } IntFunction f = i -> i * 2;
だけで OK です。
クロージャとしてのラムダ式
ラムダ式はローカル変数にそのままアクセスできるという意味でクロージャ(Closure 閉包)としての機能を持つことが一般的です。 まぁ言葉はともかく使い方は簡単。
int scale = 10; IntFunction f = i -> i * scale; System.out.println(f.apply(2)); // 「20」
ここでは scale というローカル変数をラムダ式の中で参照しています。 特に不自然なところはないですね。 ただ、ラムダ式の代わりに無名関数で同じことをしようとすると、今まで(Java7 まで)はローカル変数に final 宣言をつけていなければなりませんでした(「final int scale = 10;」とする)。 この辺りの齟齬をなくすために、Java8 からは無名関数の場合でも final でないローカル変数にアクセスできるようになりました(ただし、実質的に final な場合に限られます*4):
int scale = 10 IntFunction f = new IntFunction(){ @Override public int apply(int i) { return i * scale; // final でない変数でも無名クラス内で使えるようになったヨ。 } }; System.out.println(f.apply(3)); // 「30」
ラムダ式が導入された後にこの機能を使うことがあるかどうかは疑問ですがね*5。
関数オブジェクト
次は関数をオブジェクトとして扱う方法を見ていきます。
ラムダ式を既存の型に代入する
Java8 では抽象メソッドが1つだけのインターフェースは「関数インターフェース」と見做され、引数のシグニチャと返り値の型が一致すればラムダ式を代入することができます。 例えば java.lang.Runnable インターフェースは
@FunctionalInterface interface Runnable{ void run(); }
という型宣言を持つので、
Runnable r = () -> System.out.println("Runnable by lambda expression.");
という代入ができます。 型が合わなければコンパイラがきちんとコンパイル・エラーを出してくれます。 わぁい。 Java8 からは Runnable に @FunctionalInterface アノテーションが付けられてますが、これは関数インターフェースと見做されるための必須条件ではありません。 インターフェースにこのアノテーションを付けておくと、そのインターフェースが関数インターフェースとみなせない場合(抽象メソッドが2つ以上あるなど)にコンパイル・エラーを出してくれます。
ラムダ式で生成したオブジェクトを関数オブジェクトとして扱いたい場合、見て関数とわかる型に代入したいこともよくあります。 ただ、個々人が独自に Function インターフェースみたいなのを定義していくのも何なので、Java8 から java.util.function パッケージが導入され、関数っぽいインターフェースがあれこれまとめて定義されています。 大雑把にはこちらの記事参照。
メソッドの参照
関数オブジェクトを取得するには、ラムダ式を使う以外に既存のメソッドをオブジェクトとして取得する方法もあります。 これにはコロンを2つ並べた演算子「::」を使います*6。 「::」の使い方は微妙に違う2通りがあります:
違いを意識する必要があるような、ないようなって感じですが。
具体例を見ていきましょう。 まずはオブジェクトに対して「::」演算子を使う場合。 このときはインスタンス・メソッドの参照を取得します。 例えばラムダ式で書いたとき
Function<Integer, Character> im0 = i -> "Lambda".charAt(i); assert im0.apply(3) == 'b';
となるのと同じ関数オブジェクトをメソッドの参照で書いてみましょう。 ちなみに Function<String, Integer> は String オブジェクト1つを引数にとって Integer オブジェクトを返す関数オブジェクトです。 「::」演算子を使った場合
Function<Integer, Character> im1 = "Lambda"::charAt; assert im1.apply(3) == 'b';
となります。 参照するメソッドには「()」をつけません。 まぁ、これは問題ないかと思います。
次はクラス(名)に「::」を使う場合。 クラス・メソッド(static メソッド、静的メソッド)の参照を取得するのは特に問題はないと思います:
Function<String, Double> sm = Double::parseDouble; assert sm.apply("2.0") == 2.0;
コンストラクタの参照を取得する場合は、メソッド名の代わりには「new」を用います:
Function<char[], String> c = String::new; assert c.apply(new char[]{ 'l', 'a', 'm', 'b', 'd', 'a'}) == "lambda";
最後は、第1引数のインスタンス・メソッドを参照する場合。 String を引数にして length() メソッドの返り値を返す関数オブジェクトを取得してみましょう。 ラムダ式で書くと
Function<String, Integer> f = s -> s.length(); assert f.apply("Lambda") == 6;
です。 これを「::」を使って書くと
Function<String, Integer> mc = String::length; assert mc.apply("Lambda") == 6;
参照したメソッドを実際に持っているのは、メソッド(今の場合 apply() メソッド)を呼び出す際に渡した(第1)引数 "Lambda" になります。 呼び出すメソッドに引数を渡したい場合は以下のようになります:
BiFunction<String, Integer, Character> mc3 = String::charAt; assert mc3.apply("Lambda", 2) == 'm';
BiFunction<String, Integer, Character> (2変数関数)は「Character apply(String, Integer)」というメソッドを持ちます。 このメソッド参照をこれはラムダ式で書くと
BiFunction<String, Integer, Character> mc2 = (s, i) -> s.charAt(i); assert mc2.apply("Lambda", 2) == 'm';
第2引数(以降)は参照したメソッドの引数として渡されます。 どうでしょう? 2つの場合の違い分かったでしょうか?
メソッドのデフォルト実装
Java8 からはインターフェースに定義したメソッドにデフォルト実装を書くことができるようになりました。 このメソッドは抽象メソッドに数えられないので、関数インターフェースが抽象メソッドが1つしか持てないという制約があってもいろいろなインターフェースを関数インターフェースとして扱うことができるようになります。 デフォルトメソッドは、単にインターフェースのメソッドに「default キーワード」を付けておくだけです。 メソッドの実装は具象メソッドと同じように書けます。 例えばこんな感じです:
@FunctionalInterface interface BDReducer{ BigDecimal reduce(BigDecimal arg0, BigDecimal arg1); default BigDecimal reduce(BigDecimal arg0, BigDecimal arg1, BigDecimal arg2){ return reduce(reduce(arg0, arg1), arg2); } }
2引数の reduce() は抽象メソッド、3引数の reduce() はデフォルト実装を持ったメソッドです。 @FunctionalInterface アノテーションは、BDReducer が関数インターフェースに適合していることを(コンパイル時に)検証するためにつけてます。 なくても OK。 このインターフェースは例えば以下のように使います:
BigDecimal one = new BigDecimal(1), two = new BigDecimal(2), three = new BigDecimal(3); BDReducer a = BigDecimal::add; assert a.reduce(one, two).intValue() == 3; // 1 + 2 assert a.reduce(one, two, three).intValue() == 6; // 1 + 2 + 3 BDReducer m = BigDecimal::multiply; assert m.reduce(one, two).intValue() == 2; // 1 * 2 assert m.reduce(one, two, three).intValue() == 6; // 1 * 2 * 3
インターフェースにメソッド実装を許すと多重継承の問題が発生しますが、ここでは割愛。
高階関数
関数をオブジェクトとして扱えるようになると、その自然な帰結として関数を引数とする関数、すなわち高階関数 (higher-order function) が使えるようになります。 その最も簡単な例はコレクションの各要素におなじ処理を施すというメソッドであり、関数オブジェクトをサポートするプログラミング言語では、その言語内のコレクションにいろいろな高階関数が定義されているのが普通です。 ここでは Java8 で Java のコレクション・フレームワークに追加された機能を簡単に見ていきます。 また、独自に高階関数を定義する仕方も簡単に触れます(まぁ、普通の関数の定義なんですけど)。
Java Collection Framework API
Java8 ではコレクション・フレームワークのインターフェース群に高階関数が使えるような API 拡張があれこれ施されています。 ただ、Java7 までに存在したインターフェースに高階関数を追加しているというより、高階関数を多く持つ新たな型*7 java.util.stream.Stream インターフェースを定義し、Collection インターフェースに Stream オブジェクトを生成するメソッドを追加する、というアプローチをとっているようです。 まぁ、今後 Collection インターフェース(とそのサブタイプ)にも色々と高階関数が定義されていくのかも知れませんが。 ちなみに、java.util.Map インターフェースには、高階関数は定義されていますが、Stream オブジェクトを生成するメソッドは定義されていないようです。 entrySet() メソッドなどで Set オブジェクトを取得してから Stream オブジェクトにすればいいだけですけど。
既存の Collection オブジェクトから Stream オブジェクトを取得するメソッドは2つ定義されています
- stream() : Stream<E>
- parallelStream() : Stream<E>
parallelStream() は並列処理可能な Stream オブジェクトを返します(そのままw)。 ちなみに、どちらもデフォルト・メソッドとして定義されています。
ちなみに、Iterable<E> インターフェースには forEach(Consumer) メソッド、Iterator<E> インターフェースには forEachRemaining(Consumer) メソッドという高階関数が定義されています(どちらもデフォルト・メソッド)。 Consumer<E> インターフェースは引数 E、返り値 void の関数インターフェースです。 Map インターフェースにも同様のメソッドが定義されています(引数が Consumer ではなく BiConsumer ですが)。 まぁ、forEach() メソッドがあれば大抵の高階関数は定義できる*8かと思うので、このメソッド超重要。
まぁ、具体的にコード見た方が分かりやすいでしょう。 まずは既存のコレクションの forEach() メソッド。
List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6); list.forEach(System.out::print); // 「0123456」
2行目では PrintStream 型のSystem.out オブジェクトについて print() メソッドの参照を取得し、それを List#forEach() メソッドに渡しています。 まぁ、なんてことないですね。 次は Stream オブジェクトを使うコード。
List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6); Stream<Integer> stream = list.stream(); Stream<Integer> even = stream.filter(i -> i % 2 == 0); // 偶数だけを取り出す even.forEach(System.out::print); // 「0246」
偶数だけを取り出して表示しています。 3行目ではラムダ式で関数オブジェクトを生成しています。 まぁ、通常はメソッドを数珠繋ぎに呼び出して使う方が自然:
List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6); list.stream().filter(i -> i % 2 == 0).forEach(System.out::print); // 「0246」
おぉー、すっきり。 Java もだいぶ見易くなったもんだ。 次はもうちょっと複雑(demonai?)コード。 文字列の List を長さでソートしています:
List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure"); lang.sort((s0, s1) -> s0.length() - s1.length()); lang.forEach(System.out::println);
2行目ではラムダ式によって Comparator オブジェクトを生成しています。 関数型言語と違ってメソッドが呼び出された List の要素自体が並び替えられているのに注意。
高階関数を定義する
最後に、簡単な高階関数を作って終わりにしましょう。 高階関数はいきなりキチンとした実装を作ろうとしても混乱して時間を無駄にすることが多いので(拙者だけ?)、具体的なメソッド実装から順々に汎化していった方が無難かと。
ここでは、String の List に対して、各要素に何らかの変換を施して表示する関数を作ってみます。 手始めに文字列の長さを表示する関数を書いてみると
List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure"); printlnForEach(lang) public static void printlnForEach(List<String> list){ list.stream().map(String::length).forEach(System.out::println); }
Stream#map() メソッドを使って String の Stream から int (Integer) の Stream に変換しています。 変換部分を関数オブジェクトにして、メソッドの引数に追加すると
List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure"); printlnForEach(lang, String::length) public static void printlnForEach(List<String> list, Function<String, Integer> mapper){ list.stream().map(mapper).forEach(System.out::println); }
今の場合、printlnForEach() メソッドの最後で forEach() に渡している println() メソッドは任意の型のオブジェクトを渡せるので、printlnForEach() メソッドの第2引数は Function<String, ?> で充分ですね。 また、List<String> と Function<String, ?> の String は型パラメータとして抽出しておきましょう。
List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure"); printlnForEach(lang, String::length) public static <T> void printlnForEach(List<T> list, Function<T, ?> mapper){ list.stream().map(mapper).forEach(System.out::println); }
この printlnForEach() メソッドを使えば
List<String> lang = Arrays.asList("Java", "Groovy", "Scala", "Clojure"); printlnForEach(lang, s -> s.charAt(0));
として書く文字列の先頭文字だけを抜き出して表示させることが出来ます。 String 以外の List でも処理できますが本日はこの辺で。
まとめ
ザッと Java8 Project Lambda の概要を見てきましたが、ほとんどの機能は既存の関数型言語で既に実装されているものばかりとも言えますが、逆に言えば多言語で出来ていたことが Java でも出来るようになったとも言えます。 シンタックスは Scala のものによく似ているようですが、Groovy のものとは結構違いますね。 完全に Java の文法を捨てた Scala に似ていて、Java の痕跡が残るような文法にしている Groovy と全く異なるという、皮肉なところもある Java8 のラムダ式ですが、Groovy の今後はどうなっちゃうんでしょー*9。 まぁ、それはともかく、本記事が Java8 のラムダ式の理解に役立って無上の喜びでございまする。
Happy, April 1st!
追記
「::」演算子を使ってクラス・メソッド、コンストラクタの参照を取得する部分を追記しました。
追記2
Java8 のラムダ式で参照できる外部変数は実質的に final な場合だ、って部分を追記しました。 「Java8のlambda構文がどのようにクロージャーではないか」参照のこと。
- 作者: Martin Odersky,Lex Spoon,Bill Venners,羽生田栄一,水島宏太,長尾高弘
- 出版社/メーカー: インプレスジャパン
- 発売日: 2011/09/27
- メディア: 単行本(ソフトカバー)
- 購入: 12人 クリック: 235回
- この商品を含むブログ (42件) を見る
- 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
- 出版社/メーカー: 技術評論社
- 発売日: 2011/07/06
- メディア: 単行本(ソフトカバー)
- 購入: 6人 クリック: 392回
- この商品を含むブログ (153件) を見る