はじめに
Java 8でラムダ式とStream APIが導入されたとき、合わせて関数型インターフェースという概念が登場しました。
List<String> names = List.of("田中", "佐藤", "鈴木");
names.stream()
.filter(name -> name.length() == 2)
.forEach(System.out::println);
filter() や forEach() に渡しているラムダ式は、実は関数型インターフェースを実装したものです。
この記事では、java.util.function パッケージに定義されている代表的な4つの関数型インターフェース(Function / Predicate / Consumer / Supplier)を整理します。ラムダ式やStream APIをより深く理解したい方、Java Goldを目指している方にも役立つ内容です。
関数型インターフェースとは?
抽象メソッドが1つだけのインターフェースのことです。@FunctionalInterface アノテーションをつけることで、コンパイラがその制約を強制してくれます。
@FunctionalInterface
interface MyFunction {
int execute(int a, int b); // 抽象メソッドは1つだけ
}
ラムダ式は関数型インターフェースを簡潔に実装する構文です。
// 匿名クラス
MyFunction add = new MyFunction() {
@Override
public int execute(int a, int b) { return a + b; }
};
// ラムダ式(同じ意味)
MyFunction add = (a, b) -> a + b;
毎回自分で定義しなくても済むよう、Javaが標準で用意しているのが java.util.function パッケージです。
4つの代表的な関数型インターフェース
| インターフェース | 引数 | 戻り値 | 位置付け |
|---|---|---|---|
Function<T, R> |
T(あり) | R(あり) | 変換・加工 |
Predicate<T> |
T(あり) | boolean | 条件判定 |
Consumer<T> |
T(あり) | void(なし) | 消費・副作用 |
Supplier<T> |
なし | T(あり) | 生成・供給 |
それぞれ詳しく見ていきましょう。
Function<T, R> — 値を受け取って別の値に変換する
シグネチャ
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
-
T:引数の型 -
R:戻り値の型 - メソッド:
apply()
使い方
// String を受け取って Integer を返す
Function<String, Integer> strToLength = str -> str.length();
System.out.println(strToLength.apply("Hello")); // 5
System.out.println(strToLength.apply("田中")); // 2
// Integer を受け取って String を返す
Function<Integer, String> intToStr = num -> "番号: " + num;
System.out.println(intToStr.apply(42)); // 番号: 42
Stream APIでの使用例
List<String> names = List.of("田中", "佐藤", "鈴木");
// map() は Function<T, R> を受け取る
List<Integer> lengths = names.stream()
.map(name -> name.length()) // Function<String, Integer>
.collect(Collectors.toList());
System.out.println(lengths); // [2, 2, 2]
andThen() で関数をつなぐ
Function<String, Integer> toLength = str -> str.length();
Function<Integer, String> toMessage = len -> "文字数は " + len + " です";
// andThen で連結
Function<String, String> combined = toLength.andThen(toMessage);
System.out.println(combined.apply("Hello")); // 文字数は 5 です
Predicate<T> — 条件を判定してbooleanを返す
シグネチャ
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
-
T:引数の型 - 戻り値:
boolean - メソッド:
test()
使い方
// 文字数が2文字かどうかを判定
Predicate<String> isTwoChars = str -> str.length() == 2;
System.out.println(isTwoChars.test("田中")); // true
System.out.println(isTwoChars.test("佐々木")); // false
// 数値が正かどうかを判定
Predicate<Integer> isPositive = num -> num > 0;
System.out.println(isPositive.test(5)); // true
System.out.println(isPositive.test(-3)); // false
Stream APIでの使用例
List<String> names = List.of("田中", "佐々木", "鈴木", "伊藤");
// filter() は Predicate<T> を受け取る
List<String> twoCharNames = names.stream()
.filter(name -> name.length() == 2) // Predicate<String>
.collect(Collectors.toList());
System.out.println(twoCharNames); // [田中, 鈴木, 伊藤]
and() / or() / negate() で条件を組み合わせる
Predicate<String> isTwoChars = str -> str.length() == 2;
Predicate<String> startsWith田 = str -> str.startsWith("田");
// AND条件
Predicate<String> combined = isTwoChars.and(startsWith田);
System.out.println(combined.test("田中")); // true
System.out.println(combined.test("田辺三菱")); // false(2文字ではない)
// 否定
Predicate<String> notTwoChars = isTwoChars.negate();
System.out.println(notTwoChars.test("佐々木")); // true
Consumer<T> — 値を受け取って何かするが値を返さない
シグネチャ
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
-
T:引数の型 - 戻り値:
void(なし) - メソッド:
accept()
「消費する(consume)」という名前の通り、値を受け取って何らかの処理(ログ出力・DB保存など)をしますが、値を返しません。
使い方
Consumer<String> printName = name -> System.out.println("こんにちは、" + name + "さん");
printName.accept("田中"); // こんにちは、田中さん
printName.accept("佐藤"); // こんにちは、佐藤さん
// リストに追加するConsumer
List<String> result = new ArrayList<>();
Consumer<String> addToList = item -> result.add(item);
addToList.accept("Apple");
addToList.accept("Banana");
System.out.println(result); // [Apple, Banana]
Stream APIでの使用例
List<String> names = List.of("田中", "佐藤", "鈴木");
// forEach() は Consumer<T> を受け取る
names.forEach(name -> System.out.println(name)); // Consumer<String>
// メソッド参照で書くとさらに短く
names.forEach(System.out::println);
Supplier<T> — 引数なしで値を生成して返す
シグネチャ
@FunctionalInterface
public interface Supplier<T> {
T get();
}
- 引数:なし
- 戻り値:
T - メソッド:
get()
「供給する(supply)」という名前の通り、引数を受け取らずに値を生成して返します。
使い方
// 現在時刻を返すSupplier
Supplier<LocalDateTime> nowSupplier = () -> LocalDateTime.now();
System.out.println(nowSupplier.get()); // 現在時刻が出力される
System.out.println(nowSupplier.get()); // 呼ぶたびに現在時刻を生成
// 新しいListを生成するSupplier
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
List<String> list1 = listSupplier.get(); // 新しいArrayList
List<String> list2 = listSupplier.get(); // また新しいArrayList
OptionalのorElseGet()での使用例
Optional<String> optional = Optional.empty();
// orElseGet() は Supplier<T> を受け取る
String value = optional.orElseGet(() -> "デフォルト値"); // Supplier<String>
System.out.println(value); // デフォルト値
orElse() と orElseGet() の違いは、orElseGet() は値が必要なときだけ Supplier を呼び出すため、生成コストが高いオブジェクトに向いています。
まとめ
| インターフェース | メソッド | 引数 | 戻り値 | 主な使用場面 |
|---|---|---|---|---|
Function<T, R> |
apply() |
T | R |
map() での変換 |
Predicate<T> |
test() |
T | boolean |
filter() での絞り込み |
Consumer<T> |
accept() |
T | void |
forEach() での処理 |
Supplier<T> |
get() |
なし | T |
orElseGet() での生成 |
Stream APIのメソッドがどの関数型インターフェースを受け取っているかを意識すると、ラムダ式の書き方が自然と身につきます。Java Goldでも頻出のテーマなので、apply() / test() / accept() / get() のメソッド名とセットで覚えておきましょう。