Java | Java 詳細・モダン文法:ラムダ式・関数型 – メソッド参照

Java Java
スポンサーリンク

メソッド参照を一言でいうと

メソッド参照(::)は、

「すでに存在するメソッドやコンストラクタを、“ラムダ式の代わりに”そのまま関数として渡すための短い書き方」

です。

例えば、

names.forEach(s -> System.out.println(s));
Java

は完全にこう書き換えられます。

names.forEach(System.out::println);
Java

println というメソッドを、そのまま Consumer<String> として渡している」
というイメージを持ってください。

ラムダ式で書くとちょっとクドいところを、
「もうメソッドあるじゃん、それそのまま使おうよ」とスッキリさせるための記法です。


メソッド参照の基本パターン

静的メソッド参照:クラス名::staticメソッド名

最初に押さえるべきはこれです。

ClassName::staticMethodName
Java

例として、「文字列を Integer に変換する」Function<String, Integer> を考えます。

ラムダ式で書くと:

Function<String, Integer> f = s -> Integer.parseInt(s);
Java

メソッド参照で書くと:

Function<String, Integer> f = Integer::parseInt;
Java

この 2 つは「完全に同じ意味」です。

Integer::parseInt は、

s -> Integer.parseInt(s)
Java

というラムダの省略形だと理解してください。

Stream と組み合わせるとこうなります。

List<String> list = List.of("1", "2", "3");

List<Integer> ints = list.stream()
                         .map(Integer::parseInt)   // String -> Integer
                         .toList();
Java

map(Integer::parseInt) は、「各要素 s に Integer.parseInt(s) を適用する」という意味です。

ここで重要なのは、

「メソッド参照は、対応するラムダ式と“1 対 1 で置き換え可能”」

だという感覚です。


特定のインスタンスのメソッド参照:インスタンス変数::メソッド名

次に、すでに持っているインスタンスのメソッドを指すパターンです。

instanceRef::instanceMethodName
Java

例:

import java.util.function.Consumer;

public class InstanceMethodRefExample {
    public static void main(String[] args) {
        Consumer<String> c1 = s -> System.out.println(s);
        Consumer<String> c2 = System.out::println;  // System.out は PrintStream のインスタンス

        c1.accept("hello1");
        c2.accept("hello2");
    }
}
Java

ここで System.outPrintStream 型のインスタンスです。

System.out::println は、

s -> System.out.println(s)
Java

というラムダの短縮形です。

自分で作ったオブジェクトでも同じです。

class Logger {
    void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
}

Logger logger = new Logger();

// ラムダ版
Consumer<String> c1 = s -> logger.log(s);

// メソッド参照版
Consumer<String> c2 = logger::log;
Java

logger::log は、「この logger の log メソッドを使ってね」という意味になります。


任意のインスタンスのメソッド参照:クラス名::メソッド名

初心者が一番混乱しやすいのがこれです。

ClassName::instanceMethodName
Java

さっきの「インスタンス版」と何が違うかというと、

  • さっきは「特定のインスタンス(loggerSystem.out)のメソッド」
  • 今度は「引数として渡されるインスタンスに対して呼び出すメソッド」

です。

例を見た方が早いです。

List<String> names = List.of("alice", "bob", "charlie");

// ラムダ版
names.stream()
     .map(s -> s.toUpperCase())
     .forEach(System.out::println);

// メソッド参照版
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);
Java

String::toUpperCase は、

s -> s.toUpperCase()
Java

というラムダの短縮形です。

つまり、「map が渡してくる各要素 s に対して s.toUpperCase() を呼ぶ」という意味を
String::toUpperCase と短く書いているだけです。

もう少し別の例:

List<String> list = List.of("b", "aaa", "cc");

list.stream()
    .sorted(String::compareTo)  // s1.compareTo(s2)
    .forEach(System.out::println);
Java

String::compareTo は、

(s1, s2) -> s1.compareTo(s2)
Java

というラムダと対応しています。

このパターンは、「クラス名::メソッド名」となっていても、
実際には「引数で渡ってくるインスタンスに対するメソッド呼び出し」として動く点がポイントです。


コンストラクタ参照:クラス名::new

最後の主要パターンがこれです。

ClassName::new
Java

コンストラクタを関数として渡したいときに使います。

例:「引数なしコンストラクタでオブジェクトを作る Supplier<User>

class User {
    String name;
    User() {
        this.name = "no-name";
    }
}

Supplier<User> s1 = () -> new User();  // ラムダ
Supplier<User> s2 = User::new;        // コンストラクタ参照
Java

この 2 つも完全に同じ意味です。

User::new は、「引数なしコンストラクタ new User() を呼ぶ処理」を表す Supplier<User> になります。

引数付きコンストラクタも同様です。

class User {
    String name;
    User(String name) {
        this.name = name;
    }
}

Function<String, User> f1 = s -> new User(s);  // ラムダ
Function<String, User> f2 = User::new;        // コンストラクタ参照
Java

Function<String, User> は「String -> User」。
User::new は「s -> new User(s)」というラムダに対応します。

Stream での利用例:

List<String> names = List.of("Alice", "Bob");

List<User> users = names.stream()
                        .map(User::new)  // new User(name)
                        .toList();
Java

これで name から User への変換を、コンストラクタ参照で表現できます。


メソッド参照と関数型インターフェースの関係

「ラムダと同じく、“どのインターフェースに代入するか”で意味が決まる」

メソッド参照もラムダ式と同じく、それ単体では「型」がありません。

代入先や引数の「関数型インターフェース」によって、
「どのシグネチャとして使われるか」が決まります。

例えば String::length は、状況によってこう解釈され得ます。

Function<String, Integer> f = String::length;
IntUnaryOperator op = String::length;
ToIntFunction<String> g = String::length;
Java

それぞれ、

  • Function<String, Integer>s -> s.length()
  • IntUnaryOperatori -> ??? ではなく、これは無理そうに見えるが、適合しないのでこれはコンパイル不可例
    (なので正しくは IntUnaryOperator には適合しない)
  • ToIntFunction<String>s -> s.length()

のように、「どのインターフェースの抽象メソッドに対応するか」が重要です。

ここで押さえたいのは、

「メソッド参照はあくまで、“あるメソッドを、そのシグネチャに合う関数型インターフェースの実装として使う”記法」

だということです。


「いつメソッド参照にするべきか」の感覚

“ラムダの中身がそのまま 1 メソッド呼び出しだけ”なら、メソッド参照候補

基本的な指針として、

x -> SomeClass.someMethod(x)
Java

obj -> obj.instanceMethod()
Java

のように、「ラムダの中身が“引数をそのまま 1 回メソッドに渡しているだけ”」の場合は、
メソッド参照に置き換えると読みやすくなりやすいです。

例えば:

names.forEach(s -> System.out.println(s));
// ↓
names.forEach(System.out::println);

list.stream()
    .map(s -> s.trim())
// ↓
    .map(String::trim);
Java

一方で、ラムダの中で条件分岐がある、複数ステートメントがある、値を組み合わせている、など
「単純な 1 メソッド呼び出し以上のことをしている」場合は、
無理にメソッド参照にせず、そのままラムダで書いたほうが意図が伝わりやすいです。


代表的なパターンをまとめて頭に刻む

よく使うものを、ラムダとの対応で並べておきます。

静的メソッド

// ラムダ
s -> Integer.parseInt(s)
// メソッド参照
Integer::parseInt
Java

特定インスタンスのメソッド

PrintStream out = System.out;

// ラムダ
s -> out.println(s)
// メソッド参照
out::println
Java

任意インスタンスのメソッド

// ラムダ
s -> s.toUpperCase()
// メソッド参照
String::toUpperCase
Java
// ラムダ
(s1, s2) -> s1.compareTo(s2)
// メソッド参照
String::compareTo
Java

コンストラクタ

// ラムダ
() -> new User()
// メソッド参照
User::new
Java
// ラムダ
s -> new User(s)
// メソッド参照
User::new
Java

この「ラムダ ↔ メソッド参照」の変換が、頭の中で自然にできるようになると、
既存コードを読むのも、自分で書くのも一気に楽になります。


まとめ:メソッド参照を自分の言葉で整理する

メソッド参照(::)をあなたの言葉でまとめるなら、

「既にあるメソッドやコンストラクタを、“中身がそのまま 1 メソッド呼び出しだけのラムダ”として、短く・読みやすく書くための記法」

です。

特に大事なのは、

静的メソッド:ClassName::staticMethod
特定インスタンス:instanceRef::method
任意インスタンス:ClassName::instanceMethod(実際には引数インスタンスに対する呼び出し)
コンストラクタ:ClassName::new

という 4 パターンと、それぞれに対応するラムダの形をセットで覚えることです。

タイトルとURLをコピーしました