ラムダ式を理解するための前置き
関数オブジェクト
JavaSE7まででは、変数に代入できるのは第一級オブジェクト(データ、インスタンスなど)のみであるとされていたが、
JavaSE8以降では、関数も変数に代入できるようになった。
関数にとって、名前はあってもなくても良いものである。
入力(引数)と出力(返り値)さえあれば関数の役割は果たせる。
例えば…
===============================
職場の上司に、
「この手順書に沿って折り紙折っといて~、午前中に100個作っておくように!」
と指示され、手順書と折り紙100枚を渡された(どんな仕事やねん)
しかし、手順書には折り方が載ってあるだけで、
何が出来上がるのかとか、手順書の名前なんてものはない。
それでも、手順に沿って折り紙をしていった結果、
なにか良くわからないけど100個分完成した
===============================
…みたいな感じである。
プログラミングにおける関数も同様で、
「名前なんか無くても入力・出力さえあれば関数でいいよね~」
って考えでJavaSE8からは変数に関数が代入できるようになった。
変数に代入される関数のことを、関数オブジェクトといったりもする。
変数への関数オブジェクト代入の例
import java.util.function.*;
public class Sample {
private static int sum(int x, int y) {
return x + y;
}
public static void main(String[] args) {
IntBinaryOperator func = Sample::sum;
int x = func.applyAsInt(10, 20);
System.out.println(x);//result: 30
}
}
ざっくり説明すると、Sampleクラスにあるstaticなsumメソッドへの参照を、IntBinaryOperatorインターフェース)のfuncに代入することで、
IntBinaryOperatorインターフェースで予め定義されているapplyAsIntメソッドはsumメソッドの機能としてオーバーライドされ、
func.applyAsIntはsumの機能を提供するようになった、という感じ。
なお、今回のように関数オブジェクトを変数に代入できるのには条件がある。
================================
*1.変数(左側)の条件…抽象メソッドを1つしか有していない関数型インターフェースである(1)こと
2.関数オブジェクト(右側)の条件…関数型インターフェースのメソッドと関数オブジェクトの「戻り値の型・引数の数・引数の型」が一致していること
(*1)SAMインターフェース(Single Abstract Method Interface)という
================================
実際、↑のコードでも条件は満たす。
IntBinaryOperator
というのをリファレンスで調べてみると、
- これは関数型インタフェースなので、ラムダ式またはメソッド参照の代入先として使用できます。
-
int applyAsInt(int left, int right)
これ1つのみメソッドをもつ
との記載がされている。
今回の関数オブジェクトint sum(int x, int y)と比較をしても条件は満たしている。
IntBinaryOperatorの他にも関数型インターフェースは以下のようなものがある
関数型インターフェース | メソッド |
---|---|
int IntToDoubleFunction(int, int) | applyAsDouble |
void IntConsumer(int) | accept |
int IntSupplier() | getAsInt |
boolean IntPredicate(boolean) | test |
ラムダ式
ラムダ式とは、関数を必要なタイミングで即座に利用できるようにするための構文のこと
先ほどの関数オブジェクト代入コードを、ラムダ式を用いて表すと以下のようになる
import java.util.function.*;
public class Sample2 {
public static void main(String[] args) {
IntBinaryOperator func = (int x, int y) -> {
return x + y;
};
int x = func.applyAsInt(10, 20);
System.out.println(x);// result: 30
}
}
非常にシンプルなコードになったが、実態は先ほどと変わらず、
引数x, yの合計値を戻り値として返す関数への参照を、IntBinaryOperatorインターフェースのfunc変数に代入している。
また、以下のように、条件を満たせばもっと簡略化することも可能である。
import java.util.function.*;
public class Sample3 {
public static void main(String[] args) {
IntBinaryOperator func = (x, y) -> x + y;
int x = func.applyAsInt(10, 20);
System.out.println(x);// result: 30
}
}
ラムダ式の実践的な使い方の具体例
StreamAPI
void forEach(Consumer)
関数型インターフェースConsumerの変数を引数に取り、要素の数だけ処理を繰り返す
import java.util.ArrayList;
import java.util.List;
public class RamdaSample2 {
public static void main(String[] args) {
int[] nums = { 9, -3, 1, 0, 2 };
List<Integer> numsList = new ArrayList<>();
for (int n : nums)
numsList.add(n);
numsList.stream().forEach((i) -> {
System.out.println(i);
});
}
}
//9
//-3
//1
//0
//2
ちなみに、よく使う関数型インターフェースには以下のようなものがある
(T:引数, R:戻り値)
関数型インターフェース | メソッド |
---|---|
Functions<T, R> | R apply(T) |
Consumer<T> | void accept(T) |
Supplier | T get() |
Predicate<T> | boolean test(T) |
おわりに
関数オブジェクトの項目だけでも読んでおけば、
ラムダ式がなぜあんな意味不明な構造になっているかのかは何となくわかるはず