メソッド参照を一言でいうと
メソッド参照(::)は、
「すでに存在するメソッドやコンストラクタを、“ラムダ式の代わりに”そのまま関数として渡すための短い書き方」
です。
例えば、
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();
Javamap(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.out は PrintStream 型のインスタンスです。
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;
Javalogger::log は、「この logger の log メソッドを使ってね」という意味になります。
任意のインスタンスのメソッド参照:クラス名::メソッド名
初心者が一番混乱しやすいのがこれです。
ClassName::instanceMethodName
Javaさっきの「インスタンス版」と何が違うかというと、
- さっきは「特定のインスタンス(
loggerやSystem.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);
JavaString::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);
JavaString::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; // コンストラクタ参照
JavaFunction<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()IntUnaryOperator→i -> ???ではなく、これは無理そうに見えるが、適合しないのでこれはコンパイル不可例
(なので正しくは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 パターンと、それぞれに対応するラムダの形をセットで覚えることです。
