254 Views
June 05, 25
スライド概要
JJUG CCC 2025 Spring 登壇資料
Java, Spring Boot, React, Vue.js など
こわく ない! Javaの関数型インターフェースの 基礎と活用法 2025-06-07 JJUG CCC 2025 Spring 株式会社サンアーチ Takeshi Miyajima
自己紹介 Takeshi Miyajima 宮島 健 株式会社サンアーチ 業務システム開発 / 運用ツール開発 Java / Spring Boot / React / Vue / etc 2
今日伝えたいこと • 関数型インターフェース・ラムダ式は難しくない • 使いこなすと簡潔で柔軟なコードが書ける • 考えに慣れると、実装アイデアも広がる なんとなく苦手意識のある人に、 やってみよう!と思ってもらえたらうれしい 3
関数型インターフェース 1 4
関数型インターフェースとは
ラムダ式やメソッド参照の受け皿となるインターフェース
関数型
インターフェース
greet(name -> "Hello, " + name);
ラムダ式
public static void greet(
Function<String, String> greeter) {
String message = greeter.apply("太郎");
System.out.println(message);
}
定義
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
「抽象メソッド1つだけ」ルール
ラムダ式の受け皿型
5
関数型インターフェースを学ぶ意義 1
処理を渡すのにラムダ式が使えるため、短く直感的に書ける
// 無名クラスの場合
greet(new Function<String, String>() {
@Override
public String apply(String name) {
return "Hello, " + name;
}
});
// ラムダ式の場合
greet(name -> "Hello, " + name);
6
関数型インターフェースを学ぶ意義 2
処理を渡すのが簡単なため、構造と処理の分離がしやすい
→ 構造に対する共通操作を再利用しやすくなる
// ループ処理を毎回書く必要がある
List<User> users = new ArrayList<>();
for (User user : allUsers) {
if (user.getAge() => 20) {
users.add(user);
}
}
// 「絞り込み条件」だけを変更する
List<User> users = allUsers
.stream()
.filter(user -> user.getAge() => 20)
.toList();
7
関数型インターフェースを学ぶ意義 3 「命令型」から「宣言型」への転換 →「どうやるか」ではなく「なにをやるか」に視点が変わる 命令型:処理手順を伝える 1. 結果格納用のリストを作っておく 2. ユーザー情報を全件ループ処理 1. 20歳以上の条件に該当するものがあれば、 結果格納用のリストに追加する 3. 結果格納用のリストを返却する 宣言型:リクエストを伝える ユーザー情報から、 20歳以上のユーザーを抽出して リストとして返却して 8
関数型インターフェースを学ぶ意義 4 さまざまな言語で同じようなものがある → 考え方を他言語へも流用可能 JavaScript numbers.map(x => x * 2); Python list(map(lambda x: x * 2, numbers)) C# numbers.Select(x => x * 2).ToList() 9
関数型インターフェースを学ぶ意義 5 様々なAPIで関数型インターフェースが使われるようになってる → ある程度慣れていないと困るシチュエーションが発生する 特に非同期・リアクティブプログラミングでは必須 関数型インターフェース前提のAPI Stream API / Optional / CompletableFuture Spring WebFlux / Spring R2DBC / Spring Security / etc 10
関数型インターフェースに苦手意識を感じる理由 1 宣言型の考え方になじめていない • どう動くのかがイメージできないことが不安 • IDEでのデバッグがやりづらい • そもそも “処理を渡す” というイメージが沸かない 中身を詳細に理解しようとせず、 抽象的にイメージするのが大事 Stream Filter 11
関数型インターフェースに苦手意識を感じる理由 2 型定義が複雑でとっつきにくい • インターフェース名が抽象度が高すぎる • ジェネリクス型が複雑怪奇になりがち <R> St ream<R > map( Functi on<? s ) uper T ,? ?? extend s R> m apper やってみるとそんなに難しくない! どんどん使って慣れるのが一番! 12
覚えておくべき関数型インターフェース java.util.functionパッケージのうち、覚えるべきは以下のもののみ。 インターフェース 引数 戻り値 代表メソッド イメージ Runnable なし なし void run() 処理の実行 Supplier<T> なし あり T get() 値の生成 Consumer<T> あり なし void accept(T t) 値の利用 Function<T, R> あり あり R apply(T t) 値の変換 Predicate<T> あり boolean boolean test(T t) 値の判定 13
関数型インターフェースを使う 2 14
例1) リスト操作 関数型インターフェースなし ユーザー情報を全件取得してほしい public List<User> findAllUsers() { return userRepository.findAll(); } 15
例1) リスト操作
関数型インターフェースなし
20歳以上のユーザーに絞り込んでほしい
public List<User> findUsers() {
List<User> users = new ArrayList<>();
for (User user : userRepository.findAll()) {
if (user.getAge() >= 20) {
users.add(user);
}
}
return users;
}
16
例1) リスト操作
関数型インターフェースなし
ユーザー名と年齢の昇順に並べかえてほしい
// さっきのコードに追加
users.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
if (u1.getName().equals(u2.getName())) {
return Integer.compare(u1.getAge(), u2.getAge());
} else {
return u1.getName().compareTo(u2.getName());
}
}
});
return users;
17
例1) リスト操作
関数型インターフェースなし
ユーザーIDだけ取得してほしい
// さっきのコードに追加
List<Long> userIds = new ArrayList<>();
for (User user : users) {
userIds.add(user.getId());
}
return userIds;
18
例1) リスト操作
関数型インターフェースなし
ここまでの処理のまとめ
public List<Long> findUserIds() {
List<User> users = new ArrayList<>();
for (User user : userRepository.findAll()) {
if (user.getAge() >= 20) {
users.add(user);
}
}
絞り込み
users.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
if (u1.getName().equals(u2.getName())) {
return Integer.compare(u1.getAge(), u2.getAge());
} else {
return u1.getName().compareTo(u2.getName());
}
}
});
ソート
List<Long> userIds = new ArrayList<>();
for (User user : users) {
userIds.add(user.getId());
}
変換
return userIdss;
}
19
例1) リスト操作 関数型インターフェースあり ユーザー情報を全件取得してほしい public List<User> findAllUsers() { return userRepository.findAll(); } 20
例1) リスト操作
関数型インターフェースあり
20歳以上のユーザーに絞り込んでほしい
public List<User> findUsers() {
return userRepository.findAll()
.stream()
.filter(user -> user.getAge() >= 20)
.toList();
}
21
例1) リスト操作
関数型インターフェースあり
ユーザー名と年齢の昇順に並べかえてほしい
public List<User> findUsers() {
return userRepository.findAll()
.stream()
.filter(user -> user.getAge() >= 20)
.sorted(
Comparator.comparing(User::getName)
.thenComparing(User::getAge))
.toList();
}
22
例1) リスト操作
関数型インターフェースあり
ユーザーIDだけ取得してほしい
public List<Long> findAllUserIds() {
return userRepository.findAll()
.stream()
.filter(user -> user.getAge() >= 20)
.sorted(
Comparator.comparing(User::getName)
.thenComparing(User::getAge))
.map(User::getId)
.toList();
}
23
例1) リスト操作
同じことを実現するのに、ここまで差がある
関数型インターフェースなし
public List<Long> findAllUserIds() {
List<User> users = new ArrayList<>();
for (User user : userRepository.findAll()) {
if (user.getAge() >= 20) {
users.add(user);
}
}
users.sort(new Comparator<User>() {
@Override
public int compare(User u1, User u2) {
if (u1.getName().equals(u2.getName())) {
return Integer.compare(u1.getAge(), u2.getAge());
} else {
return u1.getName().compareTo(u2.getName());
}
}
});
関数型インターフェースあり
public List<Long> findAllUserIds() {
return userRepository.findAll()
.stream()
.filter(user -> user.getAge() >= 20)
.sorted(
Comparator.comparing(User::getName)
.thenComparing(User::getAge))
.map(User::getId)
.toList();
}
List<Long> userIds = new ArrayList<>();
for (User user : users) {
userIds.add(user.getId());
}
return users;
}
24
例2) 乱数生成を入れ替え可能にする 処理内で乱数や現在日時を使用すると、テストがやりづらい プロダクトコード テストコード public class TodoService { public void addTodo(String title) { todoRepository.save(new Todo( UUID.randomUUID().toString(), title )); } } @Test void testAddTodo() { todoService.addTodo("Test Todo"); verify(todoRepository).save(new Todo( "???", "Test Todo" )); } 25
例2) 乱数生成を入れ替え可能にする
乱数生成をSupplierに変えて、入れ替え可能にする
プロダクトコード
public class TodoService {
private Supplier<String> idSupplier = () -> UUID.randomUUID().toString();
void setIdSupplier(Supplier<String> idSupplier) {
this.idSupplier = idSupplier;
}
public String addTodo(String title) {
todoRepository.save(new Todo(idSupplier.get(), title));
}
}
26
例2) 乱数生成を入れ替え可能にする
テスト時には固定値を生成するSupplierに入れ替えてテストできる
テストコード
@Test
void testAddTodo() {
todoService.setIdSupplier(() -> "test-id");
todoService.addTodo("Test Todo");
verify(todoRepository).save(new Todo("test-id", "Test Todo"));
}
27
例3) 独自関数型インターフェースを作る
標準の関数型インターフェースでは、Exceptionをスローできないため、
StreamやOptionalの処理でIO操作はやりづらい
try-catchが必要
filePaths.stream()
.flatMap(path -> Files.lines(path))
.filter(line -> line.contains(keyword))
.forEach(System.out::println);
例外java.io.IOExceptionは報告
されません。スローするには、
捕捉または宣言する必要があり
filePaths.stream()
.flatMap(path -> {
try {
return Files.lines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.filter(line -> line.contains(keyword))
.forEach(System.out::println);
ます
28
例3) 独自関数型インターフェースを作る
独自関数型インターフェースとユーティリティ関数を定義する
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
29
例3) 独自関数型インターフェースを作る これにより、Exceptionが発生する処理もStreamに組み込めるようになる ※構文として書きやすくなるだけで、例外が発生しなくなるわけではないため、注意 try-catchが不要 filePaths.stream() .flatMap(ThrowingFunction.wrap(path -> Files.lines(path))) .filter(line -> line.contains(keyword)) .forEach(System.out::println); 30
まとめ 3 31
今日伝えたかったこと • 関数型インターフェース・ラムダ式は難しくない • 使いこなすと簡潔で柔軟なコードが書ける • 考えに慣れると、実装アイデアも広がる 便利なところから小さく使っていこう! 32
ご静聴ありがとうございました 33
APPENDIX 34
参考リンク • Java言語仕様 ‒ Functional Interface https://docs.oracle.com/javase/specs/jls/se24/html/jls-9.html#jls-9.8 • JavaDoc ‒ @FunctionalInterface https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/FunctionalInterface.html 35
関数型インターフェースの派生パターン • 引数が複数 • • 演算子 (引数と戻り値が同じ型) • • • BiConsumer<T, U>, BiFunction<T, U, R>, BiPredicate<T, U> UnaryOperator<T>, BinaryOperator<T> 引数または戻り値がプリミティブ型(プリミティブ特化型) • ボクシング/アンボクシングによるパフォーマンス劣化対策 • IntConsumer, LongConsumer, DoubleConsumer, ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T>, etc • 様々なパターンで大量に存在する java.util.functionのJavaDocに派生型に対する説明あり • https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/function/package-summary.html 36
複数引数の関数型インターフェースのサンプル
@FunctionalInterface
public interface BiFunction<T, U ,R> {
R apply(T t, U u);
}
public static void greet(
BiFunction<String, Integer, String> greeter) {
String message = greeter.apply("太郎", 3);
System.out.println(message);
}
ラムダ式では、引数部分を (name, times) のように書く
greet((name, times) -> "Hello, " + name.repeat(times));
// => Hello, 太郎太郎太郎
37
for文とStreamのパフォーマンス比較 Slide 15〜24 のサンプルコードで実行速度を比較 ※1 検証環境:Macbook Air M4 8コアCPU / Temurin JDK 24.0.1+9 検証パターン 1,000件 ms/op 100万件 ms/op for文 0.040 0.001 260.097 11.429 Stream API 0.034 0.001 227.922 5.154 Stream API (Parallel) 0.075 0.005 58.125 3.536 Stream API (Virtual Threads) ※2 0.214 0.049 431.461 20.469 ※1 JMHにて以下のパラメーターで計測 Warmup: iterations=3, time=2s / Measurement: iterations=5, time=2s / Fork: 1 ※2 ユーザーIDの変換処理のみ Gatherers.mapConcurrent() を使用して Virtual Threads を利用 38
EOF 39