Java Goldでは、ラムダ式、メソッド参照、Stream APIとあわせて、java.util.function パッケージの関数型インターフェースが頻出します。
単に名前を暗記するよりも、次の3点で整理すると理解しやすくなります。
- 引数を取るか
- 戻り値を返すか
- 戻り値の型が同じか、別の型か
この記事では、Java Goldで重要な関数型インターフェースを、公式APIの内容をもとに整理します。
まず結論:型の形で覚える
最初に、以下の対応を押さえるのが重要です。
| インターフェース | ラムダ式の形 | 意味 |
|---|---|---|
Supplier<T> |
<mark>() -> T</mark> |
値を供給する |
Consumer<T> |
<mark>T -> void</mark> |
値を受け取って処理する |
Predicate<T> |
<mark>T -> boolean</mark> |
条件判定する |
Function<T, R> |
<mark>T -> R</mark> |
値を変換する |
UnaryOperator<T> |
<mark>T -> T</mark> |
同じ型で変換する |
BiConsumer<T, U> |
<mark>(T, U) -> void</mark> |
2つの値を受け取って処理する |
BiPredicate<T, U> |
<mark>(T, U) -> boolean</mark> |
2つの値で条件判定する |
BiFunction<T, U, R> |
<mark>(T, U) -> R</mark> |
2つの値から1つの結果を返す |
BinaryOperator<T> |
<mark>(T, T) -> T</mark> |
同じ型2つから同じ型を返す |
Runnable |
<mark>() -> void</mark> |
処理だけ実行する |
Callable<V> |
<mark>() -> V</mark> |
結果を返すタスク |
関数型インターフェースとは
関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェースです。
ラムダ式やメソッド参照は、この関数型インターフェースの実装として扱われます。
@FunctionalInterface
interface Greeting {
void hello(String name);
}
Greeting greeting = name -> System.out.println("Hello, " + name);
greeting.hello("Alice");
重要ポイント
@FunctionalInterface は必須ではありません。
ただし、付けておくと抽象メソッドが2つ以上になった場合にコンパイルエラーになるため、安全です。
@FunctionalInterface
interface Sample {
void execute();
// defaultメソッドは抽象メソッドに数えない
default void log() {
System.out.println("log");
}
// staticメソッドも抽象メソッドに数えない
static void info() {
System.out.println("info");
}
}
Java Goldポイント
default メソッドや static メソッドがあっても、抽象メソッドが1つなら関数型インターフェースです。
主要な関数型インターフェース一覧
| パッケージ | インターフェース | 抽象メソッド | 形 | Java Gold重要度 |
|---|---|---|---|---|
java.util.function |
Supplier<T> |
T get() |
() -> T |
高 |
java.util.function |
Consumer<T> |
void accept(T) |
T -> void |
高 |
java.util.function |
BiConsumer<T, U> |
void accept(T, U) |
(T, U) -> void |
中 |
java.util.function |
Predicate<T> |
boolean test(T) |
T -> boolean |
高 |
java.util.function |
BiPredicate<T, U> |
boolean test(T, U) |
(T, U) -> boolean |
中 |
java.util.function |
Function<T, R> |
R apply(T) |
T -> R |
高 |
java.util.function |
BiFunction<T, U, R> |
R apply(T, U) |
(T, U) -> R |
高 |
java.util.function |
UnaryOperator<T> |
T apply(T) |
T -> T |
高 |
java.util.function |
BinaryOperator<T> |
T apply(T, T) |
(T, T) -> T |
高 |
java.lang |
Runnable |
void run() |
() -> void |
高 |
java.util.concurrent |
Callable<V> |
V call() |
() -> V |
高 |
1. Supplier
Supplier<T> は、引数なしで値を返す関数型インターフェースです。
T get()
基本例
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
Supplier<String> supplier = () -> "Hello";
System.out.println(supplier.get());
}
}
Hello
重要ポイント
Supplier<T> は「毎回新しい値を返す」とは限りません。
例えば、次のように毎回同じ値を返しても Supplier です。
Supplier<String> supplier = () -> "Java";
一方で、毎回違う値を返すこともできます。
Supplier<Double> random = () -> Math.random();
System.out.println(random.get());
System.out.println(random.get());
Java Goldポイント
Supplier<T> は 引数なし・戻り値あり です。
Supplier<String> s1 = () -> "Java"; // OK
これはNGです。
// 引数を取っているためNG
// Supplier<String> s2 = name -> "Hello, " + name;
2. Consumer
Consumer<T> は、引数を1つ受け取り、戻り値を返さない関数型インターフェースです。
void accept(T t)
基本例
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("Java");
}
}
Java
重要ポイント
Consumer は副作用を前提とした処理で使われます。
副作用とは、例えば以下のような処理です。
- コンソールに出力する
- ログを書く
- コレクションに追加する
- オブジェクトの状態を変更する
Consumer<String> printer = s -> System.out.println("value = " + s);
forEachでよく使う
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
}
}
forEach の引数は Consumer<T> と考えることができます。
Java Goldポイント
Consumer<T> は戻り値が void です。
ただし、次のようなコードはコンパイルできます。
Consumer<String> c = s -> s.length();
一見、s.length() は int を返すためNGに見えます。
しかし、メソッド呼び出し式は「式文」として扱えるため、戻り値を捨てる形で Consumer に代入できます。
一方、次はNGです。
// returnで値を返しているためNG
// Consumer<String> c = s -> { return s.length(); };
これもNGです。
// 単なる値リテラルはvoid互換ではないためNG
// Consumer<String> c = s -> 100;
3. BiConsumer
BiConsumer<T, U> は、引数を2つ受け取り、戻り値を返さない関数型インターフェースです。
void accept(T t, U u)
基本例
import java.util.function.BiConsumer;
public class Main {
public static void main(String[] args) {
BiConsumer<String, Integer> printer =
(name, age) -> System.out.println(name + " : " + age);
printer.accept("Alice", 20);
}
}
Alice : 20
Map#forEachでよく使う
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> scores = Map.of(
"Alice", 90,
"Bob", 80
);
scores.forEach((name, score) ->
System.out.println(name + " = " + score));
}
}
重要ポイント
Map#forEach はキーと値の2つを受け取るため、Consumer ではなく BiConsumer です。
map.forEach((key, value) -> { ... });
4. Predicate
Predicate<T> は、引数を1つ受け取り、booleanを返す関数型インターフェースです。
boolean test(T t)
基本例
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isPositive = x -> x > 0;
System.out.println(isPositive.test(10));
System.out.println(isPositive.test(-1));
}
}
true
false
Stream#filterでよく使う
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(-2, -1, 0, 1, 2);
numbers.stream()
.filter(x -> x > 0)
.forEach(System.out::println);
}
}
filter の引数は Predicate<T> です。
重要ポイント
Predicate<T> は必ずbooleanを返します。
Predicate<String> isEmpty = s -> s.isEmpty(); // OK
Predicate<String> isLong = s -> s.length() >= 5; // OK
これはNGです。
// intを返しているためNG
// Predicate<String> p = s -> s.length();
and / or / negate
Predicate には条件を組み合わせるメソッドがあります。
Predicate<Integer> isPositive = x -> x > 0;
Predicate<Integer> isEven = x -> x % 2 == 0;
Predicate<Integer> positiveAndEven = isPositive.and(isEven);
System.out.println(positiveAndEven.test(4)); // true
System.out.println(positiveAndEven.test(3)); // false
Java Goldポイント
and と or は短絡評価します。
Predicate<Integer> p1 = x -> x > 0;
Predicate<Integer> p2 = x -> {
System.out.println("p2 evaluated");
return x % 2 == 0;
};
System.out.println(p1.and(p2).test(-1));
この場合、p1 が false なので、p2 は評価されません。
5. BiPredicate
BiPredicate<T, U> は、引数を2つ受け取り、booleanを返す関数型インターフェースです。
boolean test(T t, U u)
基本例
import java.util.function.BiPredicate;
public class Main {
public static void main(String[] args) {
BiPredicate<String, String> same =
(a, b) -> a.equals(b);
System.out.println(same.test("Java", "Java"));
System.out.println(same.test("Java", "Gold"));
}
}
true
false
重要ポイント
BiPredicate は「2つの値で条件判定する」ときに使います。
BiPredicate<String, Integer> hasLength =
(str, length) -> str.length() == length;
System.out.println(hasLength.test("Java", 4)); // true
6. Function
Function<T, R> は、引数を1つ受け取り、結果を返す関数型インターフェースです。
R apply(T t)
基本例
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> length = s -> s.length();
System.out.println(length.apply("Java"));
}
}
4
Stream#mapでよく使う
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
names.stream()
.map(name -> name.length())
.forEach(System.out::println);
}
}
map の引数は Function<T, R> です。
重要ポイント
Function<T, R> は型変換・値の変換に使います。
Function<String, Integer> f = s -> s.length();
この場合、String を受け取り、Integer を返します。
String -> Integer
compose と andThen
Function には、関数を合成するメソッドがあります。
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, String> trim = s -> s.trim();
Function<String, Integer> length = s -> s.length();
System.out.println(trim.andThen(length).apply(" Java "));
}
}
4
andThen
trim.andThen(length)
これは、先に trim を実行し、その結果に length を実行します。
trim -> length
compose
length.compose(trim)
これは、先に trim を実行し、その結果に length を実行します。
trim -> length
Java Goldポイント
andThen と compose は実行順序が逆になりやすいので注意です。
f.andThen(g) // fの後にg
f.compose(g) // gの後にf
7. BiFunction
BiFunction<T, U, R> は、引数を2つ受け取り、1つの結果を返す関数型インターフェースです。
R apply(T t, U u)
基本例
import java.util.function.BiFunction;
public class Main {
public static void main(String[] args) {
BiFunction<Integer, Integer, Integer> add =
(a, b) -> a + b;
System.out.println(add.apply(3, 5));
}
}
8
重要ポイント
BiFunction<T, U, R> は型パラメータが3つあります。
BiFunction<T, U, R>
-
T:1つ目の引数 -
U:2つ目の引数 -
R:戻り値
例えば、次のように引数と戻り値の型がすべて異なっても構いません。
BiFunction<String, Integer, String> repeat =
(str, count) -> str.repeat(count);
System.out.println(repeat.apply("Java", 3));
JavaJavaJava
8. UnaryOperator
UnaryOperator<T> は、1つの引数を受け取り、同じ型の結果を返す関数型インターフェースです。
Function<T, T> の特殊版です。
T apply(T t)
基本例
import java.util.function.UnaryOperator;
public class Main {
public static void main(String[] args) {
UnaryOperator<Integer> increment = x -> x + 1;
System.out.println(increment.apply(10));
}
}
11
重要ポイント
UnaryOperator<T> は入力型と戻り値型が同じです。
UnaryOperator<String> upper = s -> s.toUpperCase(); // OK
UnaryOperator<Integer> plusOne = x -> x + 1; // OK
これはNGです。
// Stringを受け取ってintを返しているためNG
// UnaryOperator<String> length = s -> s.length();
この場合は Function<String, Integer> を使います。
Function<String, Integer> length = s -> s.length();
9. BinaryOperator
BinaryOperator<T> は、同じ型の引数を2つ受け取り、同じ型の結果を返す関数型インターフェースです。
BiFunction<T, T, T> の特殊版です。
T apply(T t1, T t2)
基本例
import java.util.function.BinaryOperator;
public class Main {
public static void main(String[] args) {
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println(add.apply(10, 20));
}
}
30
Stream#reduceでよく使う
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
Integer sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum);
}
}
15
重要ポイント
BinaryOperator<T> は、引数2つも戻り値もすべて同じ型です。
BinaryOperator<Integer> max = (a, b) -> a > b ? a : b;
BinaryOperator<String> concat = (a, b) -> a + b;
一方、引数や戻り値の型が異なる場合は BiFunction<T, U, R> を使います。
BiFunction<String, Integer, String> repeat =
(s, count) -> s.repeat(count);
10. Runnable
Runnable は、引数なし・戻り値なしで処理を実行する関数型インターフェースです。
void run()
Runnable は java.lang パッケージにあるため、importは不要です。
基本例
public class Main {
public static void main(String[] args) {
Runnable task = () -> System.out.println("running");
task.run();
}
}
running
Threadで使う例
public class Main {
public static void main(String[] args) {
Runnable task = () -> System.out.println("thread task");
Thread thread = new Thread(task);
thread.start();
}
}
重要ポイント
Runnable は戻り値を返せません。
Runnable r = () -> System.out.println("Hello"); // OK
これはNGです。
// RunnableはvoidなのでNG
// Runnable r = () -> "Hello";
また、Runnable#run() は throws Exception を宣言していません。
そのため、チェック例外をそのまま投げるラムダ式は書けません。
// InterruptedExceptionを処理していないためNG
// Runnable r = () -> Thread.sleep(1000);
書くなら、ラムダ式の中で例外処理をします。
Runnable r = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
11. Callable
Callable<V> は、引数なし・戻り値ありのタスクを表す関数型インターフェースです。
V call() throws Exception
基本例
import java.util.concurrent.Callable;
public class Main {
public static void main(String[] args) throws Exception {
Callable<String> task = () -> "result";
System.out.println(task.call());
}
}
result
ExecutorServiceで使う例
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<String> task = () -> "result";
Future<String> future = executor.submit(task);
System.out.println(future.get());
executor.shutdown();
}
}
重要ポイント
Callable は戻り値を返せて、チェック例外も投げられます。
Callable<String> c = () -> {
Thread.sleep(1000);
return "done";
};
Thread.sleep(1000) は InterruptedException を投げますが、Callable#call() は throws Exception を宣言しているため、このラムダ式は書けます。
Runnable / Callable / Supplier の違い
Java Goldでは、次の3つがかなり混同しやすいです。
| インターフェース | 引数 | 戻り値 | チェック例外 | 用途 |
|---|---|---|---|---|
Runnable |
なし | なし | そのまま投げられない | 処理だけ実行 |
Callable<V> |
なし | あり | 投げられる | 結果を返すタスク |
Supplier<T> |
なし | あり | そのまま投げられない | 値の供給 |
覚え方
Runnable : () -> void
Callable : () -> V throws Exception
Supplier : () -> T
Java Goldポイント
Callable と Supplier はどちらも「引数なし・戻り値あり」です。
ただし、Callable は java.util.concurrent パッケージにあり、非同期処理や Future と組み合わせて使われます。
Callable<String> callable = () -> "result";
Supplier<String> supplier = () -> "value";
Stream APIとの対応
Java Goldでは、Stream APIのメソッドがどの関数型インターフェースを受け取るかが重要です。
| Stream API | 対応する関数型インターフェース | 役割 |
|---|---|---|
filter |
Predicate<T> |
条件に合う要素を残す |
map |
Function<T, R> |
要素を変換する |
forEach |
Consumer<T> |
各要素に処理を行う |
peek |
Consumer<T> |
中間操作として要素を覗く |
reduce |
BinaryOperator<T> |
要素を1つにまとめる |
generate |
Supplier<T> |
値を生成する |
collect |
Supplier / BiConsumer / BiConsumer
|
可変コンテナに集約する |
filter:Predicate
list.stream()
.filter(x -> x > 0);
x -> x > 0 は Predicate<T> です。
T -> boolean
map:Function
list.stream()
.map(x -> x.toString());
x -> x.toString() は Function<T, R> です。
T -> R
forEach:Consumer
list.stream()
.forEach(x -> System.out.println(x));
x -> System.out.println(x) は Consumer<T> です。
T -> void
reduce:BinaryOperator
list.stream()
.reduce((a, b) -> a + b);
(a, b) -> a + b は BinaryOperator<T> です。
(T, T) -> T
generate:Supplier
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream.generate(() -> Math.random())
.limit(3)
.forEach(System.out::println);
}
}
() -> Math.random() は Supplier<T> です。
() -> T
重要ポイント
Stream.generate() は無限Streamを生成するため、通常は limit() と組み合わせます。
collectで登場する3つの関数型インターフェース
collect では、以下の3つが登場します。
<R> R collect(
Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner
)
例
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> result = List.of("A", "B", "C").stream()
.collect(
ArrayList::new,
ArrayList::add,
ArrayList::addAll
);
System.out.println(result);
}
}
[A, B, C]
役割
| 引数 | 関数型インターフェース | 役割 |
|---|---|---|
ArrayList::new |
Supplier<R> |
結果を入れる箱を作る |
ArrayList::add |
BiConsumer<R, T> |
箱に要素を追加する |
ArrayList::addAll |
BiConsumer<R, R> |
箱同士を結合する |
Java Goldポイント
collect は Supplier と BiConsumer が同時に出てくるため、かなり狙われやすいです。
プリミティブ特化型も押さえる
java.util.function には、int、long、double などに対応したプリミティブ特化型もあります。
これはオートボクシングを避けるために使われます。
代表例
| インターフェース | 形 | 意味 |
|---|---|---|
IntPredicate |
int -> boolean |
intを判定する |
IntConsumer |
int -> void |
intを処理する |
IntSupplier |
() -> int |
intを供給する |
IntFunction<R> |
int -> R |
intからRへ変換する |
ToIntFunction<T> |
T -> int |
Tからintへ変換する |
IntUnaryOperator |
int -> int |
intをintに変換する |
IntBinaryOperator |
(int, int) -> int |
int同士を処理してintを返す |
混同注意
IntFunction<R> と ToIntFunction<T> は逆です。
IntFunction<String> f1 = i -> "value = " + i;
int -> String
ToIntFunction<String> f2 = s -> s.length();
String -> int
Java Goldで狙われるコンパイル可否
1. Supplierに引数ありラムダはNG
Supplier<String> s1 = () -> "OK"; // OK
// Supplierは引数を取らないためNG
// Supplier<String> s2 = x -> "NG";
2. Predicateはbooleanを返す必要がある
Predicate<String> p1 = s -> s.isEmpty(); // OK
// intを返しているためNG
// Predicate<String> p2 = s -> s.length();
3. FunctionとUnaryOperatorの違い
Function<String, Integer> f = s -> s.length(); // OK
// UnaryOperator<String>はStringを返す必要があるためNG
// UnaryOperator<String> u = s -> s.length();
4. BinaryOperatorはすべて同じ型
BinaryOperator<Integer> b1 = (a, b) -> a + b; // OK
// 引数と戻り値の型がすべてStringでなければならないためNG
// BinaryOperator<String> b2 = (a, b) -> a.length() + b.length();
この場合は BiFunction<String, String, Integer> です。
BiFunction<String, String, Integer> b =
(a, c) -> a.length() + c.length();
5. Callableはチェック例外を投げられる
Callable<String> c = () -> {
Thread.sleep(1000);
return "OK";
};
これはOKです。
一方、Supplier ではそのまま書けません。
// Supplierのgetはthrows Exceptionを持たないためNG
// Supplier<String> s = () -> {
// Thread.sleep(1000);
// return "OK";
// };
メソッド参照との対応
関数型インターフェースは、ラムダ式だけでなくメソッド参照でも使えます。
Consumer
Consumer<String> printer = System.out::println;
printer.accept("Java");
String -> void
Function
Function<String, Integer> length = String::length;
System.out.println(length.apply("Java"));
String -> Integer
Supplier
Supplier<ArrayList<String>> supplier = ArrayList::new;
ArrayList<String> list = supplier.get();
() -> ArrayList<String>
BinaryOperator
BinaryOperator<Integer> max = Integer::max;
System.out.println(max.apply(10, 20));
(Integer, Integer) -> Integer
最後に:暗記用まとめ
基本4種類
| 名前 | 覚え方 |
|---|---|
Supplier |
供給する |
Consumer |
消費する |
Predicate |
判定する |
Function |
変換する |
Biが付くと引数が2つ
| 名前 | 形 |
|---|---|
Consumer<T> |
T -> void |
BiConsumer<T, U> |
(T, U) -> void |
Predicate<T> |
T -> boolean |
BiPredicate<T, U> |
(T, U) -> boolean |
Function<T, R> |
T -> R |
BiFunction<T, U, R> |
(T, U) -> R |
Operatorは同じ型
| 名前 | 形 |
|---|---|
UnaryOperator<T> |
T -> T |
BinaryOperator<T> |
(T, T) -> T |
Runnable / Callable / Supplier
| 名前 | 形 | 例外 |
|---|---|---|
Runnable |
() -> void |
チェック例外をそのまま投げられない |
Callable<V> |
() -> V |
チェック例外を投げられる |
Supplier<T> |
() -> T |
チェック例外をそのまま投げられない |
まとめ
Java Goldでは、関数型インターフェースを名前だけで覚えるのではなく、ラムダ式の形で覚えることが重要です。
特に、以下は頻出です。
filter -> Predicate<T>
map -> Function<T, R>
forEach -> Consumer<T>
reduce -> BinaryOperator<T>
generate -> Supplier<T>
collect -> Supplier + BiConsumer + BiConsumer
また、次の違いは混同しやすいため、重点的に押さえるとよいです。
Function<T, R> : T -> R
UnaryOperator<T> : T -> T
BiFunction<T,U,R> : (T, U) -> R
BinaryOperator<T> : (T, T) -> T
Runnable : () -> void
Callable<V> : () -> V throws Exception
Supplier<T> : () -> T
関数型インターフェースは、ラムダ式・メソッド参照・Stream APIの基礎になるため、抽象メソッド名とラムダ式の形をセットで覚えるのがおすすめです。
参考
- Oracle Java SE API Specification
-
java.util.functionパッケージ FunctionalInterfaceSupplierConsumerPredicateFunctionBiFunctionUnaryOperatorBinaryOperatorRunnableCallableStream