プログラミング

Javaの関数型インターフェイス徹底解説:ラムダ式・メソッド参照・Stream APIでの活用術

この記事は約9分で読めます。

はじめに

Javaは、もともとオブジェクト指向プログラミング言語として設計されましたが、Java 8の登場により関数型プログラミングの要素が加わり、より柔軟で表現力のあるコードを書けるようになりました。その中でも重要な要素の一つが関数型インターフェイスです。

本記事では、関数型インターフェイスについて詳しく解説し、ラムダ式やメソッド参照を用いた表現方法、さらにStream APIでの活用方法を紹介します。

関数型インターフェイスとは?

基本的な概要

関数型インターフェイスとは、抽象メソッドを1つだけ持つインターフェイスのことです。@FunctionalInterfaceアノテーションを使用することで、意図的に関数型インターフェイスとして定義でき、複数の抽象メソッドを持たないことをコンパイラが保証してくれます。

代表的な関数型インターフェイス

Javaには標準で以下のような関数型インターフェイスが用意されています。

以下でサンプルコードを見ながら、それぞれのインターフェースに関して解説します。

Consumer<T>

  • 役割T型の引数を受け取り、結果を返さず(voidを返す)、処理を行います。
  • 用途何らかの操作(ログ出力、データ表示など)を実行する場合に使用されます。

以下は、文字列を出力する例です。accept() メソッドで処理を実行します。

import java.util.function.Consumer;

public class ConsumerExample {
    public static void main(String[] args) {
        Consumer<String> printer = message -> System.out.println(message);
        printer.accept("Hello, Consumer!"); // 出力: Hello, Consumer!
    }
}

Predicate<T>

  • 役割T型の引数を受け取り、条件を評価し、true または false を返します。
  • 用途条件判定やフィルタ処理に使用されます。

以下は、整数が偶数かどうかを判定する例です。test() メソッドで条件を評価します。

import java.util.function.Predicate;

public class PredicateExample {
    public static void main(String[] args) {
        Predicate<Integer> isEven = number -> number % 2 == 0;
        System.out.println(isEven.test(4)); // 出力: true
        System.out.println(isEven.test(5)); // 出力: false
    }
}

Function<T, R>

  • 役割T型の引数を受け取り、R型の結果を返します。
  • 用途入力を変換する処理(文字列を数値に変換するなど)に使用されます。

以下は、文字列の長さを返す例です。apply() メソッドで処理を実行します。

import java.util.function.Function;

public class FunctionExample {
    public static void main(String[] args) {
        Function<String, Integer> stringLength = str -> str.length();
        System.out.println(stringLength.apply("Java")); // 出力: 4
        System.out.println(stringLength.apply("Hello, Function!")); // 出力: 17
    }
}

Supplier<T>

  • 役割: 引数を受け取らず、T型の結果を生成して返します。
  • 用途計算結果やデフォルト値を生成する場合に使用されます。

以下は、現在の日付を返す例です。get() メソッドで結果を取得します。

import java.time.LocalDate;
import java.util.function.Supplier;

public class SupplierExample {
    public static void main(String[] args) {
        Supplier<LocalDate> currentDateSupplier = () -> LocalDate.now();
        System.out.println(currentDateSupplier.get());
        // 出力: 現在の日付(例: 2025-01-18)
    }
}

※LocalDate型でなくSupplier<LocalDate>型として、他のメソッドに渡すことにより、Supplier.get()を実行した時点での現在日付が取得できるため、より正確な日付をメソッド内で扱うことができます。このような手法を遅延生成と呼び、値が実際に必要になるまで生成しないことを意味します。。

この関数型インターフェースを表現する方法として、「ラムダ式」と「メソッド参照」があります。上のサンプルコードでも扱ってきたラムダ式から解説します。

ラムダ式での表現方法

ラムダ式とは?

ラムダ式は、匿名クラスを簡潔に記述する方法です。特に関数型インターフェイスを簡単に実装するために使用されます。

サンプルコード

以下は、Consumerを使ったラムダ式の例です:

Consumer<String> printer = message -> System.out.println(message);
printer.accept("Hello, Java!");

メソッド参照の使い方

メソッド参照の基本

メソッド参照は、既存メソッドを利用して、ラムダ式をさらに簡潔に記述できる構文です。以下の3種類があります。

  • クラスの静的メソッド参照(ClassName::methodName)
  • インスタンスメソッド参照(instance::methodName)
  • コンストラクタ参照(ClassName::new)

サンプルコード

次のシナリオを考えます。

  • 商品名(name)を元に、複数のProductオブジェクトをリストに変換したいとします。
  • 商品情報(Product)を保持するクラスがあります。

この場合、コンストラクタ参照を使うことで簡潔に実装できます。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Product {
    private String name;

    // コンストラクタ
    public Product(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Product{name='" + name + "'}";
    }
}

public class ConstructorReferenceExample {
    public static void main(String[] args) {
        // 商品名のリスト
        List<String> productNames = Arrays.asList("Laptop", "Smartphone", "Tablet");

        // コンストラクタ参照を使ってProductオブジェクトを生成
        List<Product> products = productNames.stream()
                .map(Product::new) // コンストラクタ参照
                .collect(Collectors.toList());

        // 結果を出力
        products.forEach(System.out::println);

        // 出力結果
        // Product{name='Laptop'}
        // Product{name='Smartphone'}
        // Product{name='Tablet'}
    }
}

Stream APIでの活用

Stream APIとは?

Stream APIはJava 8で導入されたコレクション処理のための新しい方法です。近年のJava開発においては、必ずと言っていいほど利用されるもので、関数型インターフェイスが頻繁に使用されます。

実践的な例

以下は、Stream APIで関数型インターフェイスを活用したコード例です。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
       .filter(n -> n % 2 == 0)       // Predicate
       .map(n -> n * n)               // Function
       .forEach(System.out::println); // Consumer

ポイント

  • filter
    条件を満たす要素を抽出(Predicateをラムダ式で表現して使用)。
  • map
    要素を変換(Functionをラムダ式で表現して使用)。
  • forEach
    各要素に処理を適用(Consumerをラムダ式で表現して使用)。

Stream APIに関しては、filterに関しての記事mapに関しての記事を参考にしてみてください。

まとめ

本記事では、Java 8で導入された関数型インターフェイスについて、基礎から応用までを詳しく解説しました。Consumer<T>Predicate<T>といった基本的なインターフェイスの使い方から、ラムダ式やメソッド参照を用いた簡潔な記述方法、さらにはStream APIでの活用例までを紹介しました。

インターフェイス役割主要メソッド
Consumer<T>処理を実行accept(T t)UIの更新やログ出力
Predicate<T>条件を評価test(T t)フィルタ処理
Function<T, R>データ変換apply(T t)文字列を数値に変換する
Supplier<T>値を生成get()現在の日付やランダム値を生成する

関数型インターフェイスは、Javaコードを簡潔で効率的に記述するための強力なツールです。本記事で紹介した内容を活用し、より洗練されたJavaプログラミングに挑戦してみてください!