Java Lambda関数 Function、Consumer、Supplier、Predicate
Java 8から導入されたLambdaについて見ていきましょう。
Lambda関数とは?
Lambda関数は、プログラミング言語で使われる概念で、匿名関数(Anonymous functions)を指す用語です。ラムダは一時的にすぐ使用できるため、使い捨てできる関数と考えられます。
基本的な書き方
(引数) -> {処理}
()-> {処理}
(引数) -> {処理}
(引数) -> {return 0;}
引数と処理を->でつなぐことで、Lambdaを簡単に使用できます。以下のさまざまな例を通して、具体的に見ていきましょう。
基本形
Java 7以前のバージョンでは、スレッドコードを作成するとき、次のように書いていました。
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello world lambda");
}
}).start();
ラムダを使用すると、次のようにコードが簡潔になり、可読性も大きく向上します。
new Thread(()->{
System.out.println("Hello world lambda");
}).start();
ListでのLambda
通常のfor文は、forEach関数に渡すLambdaで置き換えることができます。
次のようなListがあるとします。
List<String> stringList = new ArrayList<String>();
stringList.add("apple");
stringList.add("orange");
stringList.add("strawberry");
Java 7以前のバージョンでは、次のようにfor文を使っていたはずです。
for (String string : stringList) {
System.out.println(string);
}
Java 8以降では、Lambdaを使って次のように書けます。
stringList.forEach(string -> System.out.println(string));
Lambdaでは引数や処理の記述を省略できるため、できるだけ省略した形で書くことができます。
省略ルールは次のとおりです。
- 戻り値が
voidであれば、returnを省略できます。 - 処理コードが1行であれば、中括弧
{ }なしで書くことができ、最後のセミコロン;も省略できます。 - 引数が1つであれば、括弧
()を省略できます。 - 引数のデータ型は省略できます。すべて省略するか、すべて記述するかの2通りがあります。
このほかにも省略可能なルールはありますが、ここではすべては触れません。
次のようにメソッド参照を使って書くこともできます。
stringList.forEach(System.out::println);
メソッド参照をLambdaと一緒に使うと、簡潔でわかりやすく書けます。ただし、多用しすぎるとかえって読みづらくなることもあるため、Objects::nonNullやXXXX::getIdなど、処理内容が明確な場合にだけ使うことをおすすめします。
配列でのLambda
配列でも上記と同じように書きたい場合がありますが、配列ではforEachを直接使用できないため、Listに変換してから実行する必要があります。
String[] stringArray = { "apple", "orange", "strawberry" };
Arrays.asList(stringArray).forEach(string -> System.out.println(string));
- 配列でLambdaを使用する場合は、
Arrays.stream()メソッドを使うこともできます。
MapでのLambda
Mapでは、そのままLambdaを適用できます。
次のようなMapがあるとします。
Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("apple", "red");
stringMap.put("orange", "orange");
stringMap.put("strawberry", "red");
Java 7以前のバージョンでは、次のようにfor文を使っていたはずです。
for (Entry<String, String> entry : stringMap.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
Java 8以降では、Lambdaを使って次のように書けます。
stringMap.forEach((key, value) -> {
System.out.println(key);
System.out.println(value);
});
関数型インターフェース
関数型インターフェース(Functional Interface)とは、抽象メソッドが1つだけ存在するインターフェースのことです。つまり、関数を第一級オブジェクトのように扱えるようにするアノテーションで、インターフェースに宣言して抽象メソッドを1つだけ持つよう制限する役割を持ちます。
まず、関数型インターフェースを作成します。
@FunctionalInterface
public interface Math {
int calc(int first, int second);
}
インターフェースメソッドを実装して実行してみましょう。
public class MathCalc {
public static void main(String[] args) {
Math plus = (first, second) -> first + second;
System.out.println(plus.calc(3, 2));
Math minus = (first, second) -> first - second;
System.out.println(plus.calc(3, 2));
}
}
実行結果:
5
5
4つの関数型インターフェース
Javaには、よく使われる関数型インターフェースがあらかじめ定義されて提供されています。主な関数型インターフェースは4つあります。
| 関数型インターフェース | 引数 | 戻り値 |
|---|---|---|
Supplier<T> |
X | O |
Consumer<T> |
O | X |
Function<T, R> |
O | O |
Predicate<T> |
O | Boolean |
Supplier <T>
- Supplierは英語で「供給者」を意味し、引数なしで戻り値だけを持つ関数型インターフェースです。
T get()を抽象メソッドとして持ちます。
Supplier関数型インターフェース
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
T get();
}
使用例
Supplier<String> supplier = () -> "Hello World!";
System.out.println(supplier.get());
実行結果:
Hello World!
Consumer<T>
- Consumerは英語で「消費者」を意味し、オブジェクト
Tを引数として受け取って使用し、戻り値は持たない関数型インターフェースです。 void accept(T t)を抽象メソッドとして持ちます。- また、
andThenという関数を提供しており、これを使うと1つの関数が終わった後に次のConsumerを連鎖的に利用できます。
Consumer関数型インターフェース
package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
使用例
Consumer<String> consumer = (str) -> System.out.println(str.split(" ")[0]);
consumer.andThen(System.out::println).accept("Hello World!");
実行結果:
Hello
Hello World!
上の例では、まずacceptで受け取ったConsumerを処理して「Hello」が表示され、次にandThenで受け取った2つ目のConsumerを処理して「Hello World!」が表示されました。関数型では、関数による値の代入や変更などがないため、最初のConsumerがsplitでデータを変更したように見えても、元のデータは維持されていることがわかります。
Function<T, R>
- Functionは英語で「関数」を意味し、オブジェクト
Tを引数として受け取って処理した後、Rとして返す関数型インターフェースです。 R apply(T t)を抽象メソッドとして持ちます。- FunctionはConsumerと同じく
andThenを提供しており、さらにcomposeも提供しています。 - 前述の
andThenが最初の関数を実行した後に次の関数を連鎖的に実行するよう接続するのに対し、composeは最初の関数を実行する前に先に関数を実行して連鎖的に接続する点が異なります。 - また、
identity関数が存在します。これは自分自身を返すstatic関数です。
Function関数型インターフェース
package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
使用例
Function<String, Integer> function = str -> str.length();
int length = function.apply("Hello World!");
System.out.println(length);
実行結果:
12
Predicate<T>
- Predicateは「(事実であると)断定する」という意味で、オブジェクト
Tを引数として受け取って処理した後、Booleanを返す関数型インターフェースです。 boolean test(T t)を抽象メソッドとして持っています。
Predicate関数型インターフェース
package java.util.function;
import java.util.Objects;
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
@SuppressWarnings("unchecked")
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
使用例
Predicate<String> predicate = (str) -> str.equals("Hello World!");
boolean test = predicate.test("Hello World!");
System.out.println(test);
実行結果:
true