メソッド参照とは、「すでにあるメソッドを呼ぶだけ」のラムダ式を、クラス名::メソッド名 の形で短く書く記法です。 s -> s.length() は String::length、s -> Integer.parseInt(s) は Integer::parseInt と書けます。

この記事では、メソッド参照の4種類と配列コンストラクタ参照を「書き方・等価なラムダ・合う関数型インターフェース」の対応表にまとめ、すべて動かした結果を載せています。あわせて、Integer::toString や String::valueOf で出る「参照はあいまいです」のエラーの再現と直し方、メソッド参照はその場で評価され、ラムダは呼び出し時に評価されるという違いを、null で例外が出る時点の差で確かめます。

確認環境:Ubuntu 24.04 / OpenJDK 21.0.10(java・javac)。コンパイルエラーは javac -J-Duser.language=ja で日本語のメッセージを表示しています。

メソッド参照の早見表(4種類+配列)

種類 書き方 例 等価なラムダ 合う関数型インターフェースの例
1. static メソッド参照 クラス名::staticメソッド Integer::parseInt s -> Integer.parseInt(s) Function<String, Integer>
2. 特定のオブジェクトのインスタンスメソッド参照 変数::メソッド prefix::concat name -> prefix.concat(name) Function<String, String>
3. 任意のオブジェクトのインスタンスメソッド参照 クラス名::インスタンスメソッド String::length s -> s.length() Function<String, Integer>
3'. 同上(引数が2つ) クラス名::インスタンスメソッド String::equalsIgnoreCase (a, b) -> a.equalsIgnoreCase(b) BiFunction<String, String, Boolean>
4. コンストラクタ参照 クラス名::new ArrayList::new () -> new ArrayList<>() Supplier<List<String>>
5. 配列コンストラクタ参照 型[]::new int[]::new n -> new int[n] IntFunction<int[]>

4種類という分け方は、Oracle の Java チュートリアル(Method References)の表「Kinds of Method References」に沿っています。配列コンストラクタ参照は、Java 言語仕様 15.13 の構文に ArrayType :: new として載っている形です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaメソッド参照(::)の使い方|4種類とラムダ対応表」

4種類と配列コンストラクタ参照を動かす

早見表の例を、ラムダとメソッド参照の両方で書いて結果を比べます。

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Kinds {
    public static void main(String[] args) {
        // 1. static メソッド参照: クラス名::staticメソッド
        Function<String, Integer> parse1 = s -> Integer.parseInt(s);
        Function<String, Integer> parse2 = Integer::parseInt;
        System.out.println("1. " + parse1.apply("42") + " / " + parse2.apply("42"));

        // 2. 特定のオブジェクトのインスタンスメソッド参照: 変数::メソッド
        String prefix = "Hello, ";
        Function<String, String> greet1 = name -> prefix.concat(name);
        Function<String, String> greet2 = prefix::concat;
        System.out.println("2. " + greet1.apply("Taro") + " / " + greet2.apply("Taro"));

        // 3. 任意のオブジェクトのインスタンスメソッド参照: クラス名::インスタンスメソッド
        //    第1引数がレシーバー(メソッドを呼ばれる側)になる
        Function<String, Integer> len1 = s -> s.length();
        Function<String, Integer> len2 = String::length;
        BiFunction<String, String, Boolean> eq1 = (a, b) -> a.equalsIgnoreCase(b);
        BiFunction<String, String, Boolean> eq2 = String::equalsIgnoreCase;
        System.out.println("3. " + len1.apply("apple") + " / " + len2.apply("apple")
                + " / " + eq1.apply("Java", "JAVA") + " / " + eq2.apply("Java", "JAVA"));

        // 4. コンストラクタ参照: クラス名::new
        Supplier<List<String>> list1 = () -> new ArrayList<>();
        Supplier<List<String>> list2 = ArrayList::new;
        Function<String, StringBuilder> sb1 = s -> new StringBuilder(s);
        Function<String, StringBuilder> sb2 = StringBuilder::new;
        System.out.println("4. " + list1.get() + " / " + list2.get()
                + " / " + sb1.apply("ab").reverse() + " / " + sb2.apply("ab").reverse());

        // 5. 配列コンストラクタ参照: 型[]::new
        IntFunction<int[]> arr1 = n -> new int[n];
        IntFunction<int[]> arr2 = int[]::new;
        String[] names = Stream.of("b", "a", "c").sorted().toArray(String[]::new);
        System.out.println("5. " + arr1.apply(3).length + " / " + Arrays.toString(arr2.apply(3))
                + " / " + Arrays.toString(names));
    }
}
java Kinds.java

実行結果:

1. 42 / 42
2. Hello, Taro / Hello, Taro
3. 5 / 5 / true / true
4. [] / [] / ba / ba
5. 3 / [0, 0, 0] / [a, b, c]

どの行も、ラムダとメソッド参照で同じ結果になっています。

2番と3番の見分け方::: の左が「変数」か「クラス名」か

つまずきやすいのは、2番と3番の違いです。

種類 :: の左 メソッドを呼ばれる相手(レシーバー) 関数の引数
2. prefix::concat 変数(オブジェクト) その変数に入っているオブジェクト。作った時点で決まる すべてメソッドの引数になる
3. String::concat クラス名 関数の第1引数 第2引数以降がメソッドの引数になる

prefix::concat は「prefix に対して concat を呼ぶ」なので、関数の引数は1つ(つなげる文字列)です。String::concat は「渡された1つ目の文字列に対して、2つ目を concat する」なので、関数の引数は2つになります。クラス名::インスタンスメソッドの形では、関数の引数がメソッドの引数より1つ多いと覚えると整理できます。

1番と3番は書き方が同じ

Integer::parseInt(static)と String::length(インスタンス)は、どちらも クラス名::メソッド名 です。コンパイラは、そのメソッドが static かどうかと、受け取る関数型インターフェースの引数の数から、どちらの意味かを判断します。この仕組みが、後半の「参照はあいまいです」というエラーの理由になります。

ラムダからメソッド参照への書き換え対応表

Stream でよく書くラムダを、メソッド参照に書き換えた対応表です。

やりたいこと ラムダ メソッド参照 種類
null を除く filter(s -> s != null) filter(Objects::nonNull) 1
前後の空白を取る map(s -> s.trim()) map(String::trim) 3
空文字を除く filter(s -> !s.isBlank()) filter(Predicate.not(String::isBlank)) 3
大文字小文字を無視して並べる sorted((a, b) -> a.compareToIgnoreCase(b)) sorted(String::compareToIgnoreCase) 3'
項目を取り出す map(i -> i.name()) map(Item::name) 3
項目で並べる sorted((a, b) -> Integer.compare(a.stock(), b.stock())) sorted(Comparator.comparingInt(Item::stock)) 3
合計する reduce(0, (a, b) -> a + b) reduce(0, Integer::sum) 1
最大を求める reduce((a, b) -> Math.max(a, b)) reduce(Math::max) 1
Map にする toMap(i -> i.name(), i -> i.stock()) toMap(Item::name, Item::stock) 3
文字列にする map(i -> String.valueOf(i.stock())) map(Item::stock).map(String::valueOf) 3 と 1
1件ずつ出力する forEach(s -> System.out.println(s)) forEach(System.out::println) 2

表の左右が同じ結果になることを、次のコードで確かめました。各行の先頭の true は、ラムダ版とメソッド参照版の結果が等しいことを表します。

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Idioms {
    record Item(String name, int stock) {}

    public static void main(String[] args) {
        List<String> words = Arrays.asList(" apple ", "kiwi", null, "", "Banana");
        List<Item> items = List.of(new Item("ペン", 30), new Item("ノート", 12), new Item("付箋", 0));

        // 左がラムダ、右がメソッド参照。結果が同じなら true
        check("null を除く", words.stream().filter(s -> s != null).toList(),
                              words.stream().filter(Objects::nonNull).toList());
        List<String> nonNull = words.stream().filter(Objects::nonNull).toList();
        check("前後の空白を取る", nonNull.stream().map(s -> s.trim()).toList(),
                                  nonNull.stream().map(String::trim).toList());
        check("空文字を除く", nonNull.stream().filter(s -> !s.isBlank()).toList(),
                              nonNull.stream().filter(Predicate.not(String::isBlank)).toList());
        check("大文字小文字を無視して並べる", nonNull.stream().sorted((a, b) -> a.compareToIgnoreCase(b)).toList(),
                                              nonNull.stream().sorted(String::compareToIgnoreCase).toList());
        check("項目を取り出す", items.stream().map(i -> i.name()).toList(),
                                items.stream().map(Item::name).toList());
        check("項目で並べる", items.stream().sorted((a, b) -> Integer.compare(a.stock(), b.stock())).toList(),
                              items.stream().sorted(Comparator.comparingInt(Item::stock)).toList());
        check("合計する", items.stream().map(i -> i.stock()).reduce(0, (a, b) -> a + b),
                          items.stream().map(Item::stock).reduce(0, Integer::sum));
        check("int にして合計", items.stream().mapToInt(i -> i.stock()).sum(),
                                items.stream().mapToInt(Item::stock).sum());
        check("最大を求める", items.stream().map(i -> i.stock()).reduce((a, b) -> Math.max(a, b)),
                              items.stream().map(Item::stock).reduce(Math::max));
        check("Map にする", items.stream().collect(Collectors.toMap(i -> i.name(), i -> i.stock())),
                            items.stream().collect(Collectors.toMap(Item::name, Item::stock)));
        check("文字列にする", items.stream().map(i -> String.valueOf(i.stock())).toList(),
                              items.stream().map(Item::stock).map(String::valueOf).toList());

        // 出力するだけの処理
        items.stream().map(Item::name).forEach(System.out::println);
    }

    static void check(String title, Object byLambda, Object byRef) {
        System.out.println(byLambda.equals(byRef) + "  " + title + " → " + byRef);
    }
}

実行結果:

true  null を除く → [ apple , kiwi, , Banana]
true  前後の空白を取る → [apple, kiwi, , Banana]
true  空文字を除く → [ apple , kiwi, Banana]
true  大文字小文字を無視して並べる → [,  apple , Banana, kiwi]
true  項目を取り出す → [ペン, ノート, 付箋]
true  項目で並べる → [Item[name=付箋, stock=0], Item[name=ノート, stock=12], Item[name=ペン, stock=30]]
true  合計する → 42
true  int にして合計 → 42
true  最大を求める → Optional[30]
true  Map にする → {ノート=12, 付箋=0, ペン=30}
true  文字列にする → [30, 12, 0]
ペン
ノート
付箋

!s.isBlank() のように否定が入るラムダは、そのままではメソッド参照にできません。Predicate.not(String::isBlank) で包むと書けます。引数を加工してから渡す、2つのメソッドを続けて呼ぶ、といったラムダはメソッド参照にならないので、その場合はラムダのまま書くか、map(Item::stock).map(String::valueOf) のように段階を分けます。

コンストラクタ参照と this・super

コンストラクタ参照は、受け取る関数型インターフェースの引数に合わせて、呼ばれるコンストラクタが決まります。

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class Ctor {
    record Item(String name, int stock) {
        Item() { this("(名前なし)", 0); }
        Item(String name) { this(name, 0); }
    }

    static class Base {
        String describe() { return "Base"; }
    }

    static class Child extends Base {
        @Override String describe() { return "Child"; }
        String tag(String s) { return "[" + s + "]"; }

        void show() {
            Supplier<String> mine = this::describe;     // 自分のメソッド
            Supplier<String> parent = super::describe;  // 親クラスのメソッド
            Function<String, String> tagger = this::tag;
            System.out.println("this::describe=" + mine.get() + " / super::describe=" + parent.get() + " / this::tag=" + tagger.apply("x"));
        }
    }

    public static void main(String[] args) {
        // 同じ Item::new でも、受け取る型で呼ばれるコンストラクタが変わる
        Supplier<Item> c0 = Item::new;
        Function<String, Item> c1 = Item::new;
        BiFunction<String, Integer, Item> c2 = Item::new;
        System.out.println(c0.get());
        System.out.println(c1.apply("ペン"));
        System.out.println(c2.apply("ノート", 12));

        // Stream と組み合わせる
        List<Item> items = Stream.of("ペン", "ノート").map(Item::new).toList();
        System.out.println(items);
        Item[] array = items.stream().toArray(Item[]::new);
        System.out.println(array.length + "件の配列 / 型: " + array.getClass().getSimpleName());

        // コレクションを指定して集める
        TreeSet<String> set = Stream.of("b", "a", "b").collect(Collectors.toCollection(TreeSet::new));
        Map<Integer, List<String>> byLength = new HashMap<>();
        for (String s : List.of("ab", "cd", "efg")) {
            byLength.computeIfAbsent(s.length(), ArrayList::new).add(s);
        }
        System.out.println(set + " / " + byLength);

        new Child().show();
    }
}

実行結果:

Item[name=(名前なし), stock=0]
Item[name=ペン, stock=0]
Item[name=ノート, stock=12]
[Item[name=ペン, stock=0], Item[name=ノート, stock=0]]
2件の配列 / 型: Item[]
[a, b] / {2=[ab, cd], 3=[efg]}
this::describe=Child / super::describe=Base / this::tag=[x]
書き方 受け取る型 呼ばれるもの
Item::new Supplier<Item> 引数なしのコンストラクタ
Item::new Function<String, Item> 引数1つのコンストラクタ
Item::new BiFunction<String, Integer, Item> 引数2つのコンストラクタ
Item[]::new IntFunction<Item[]> 指定した長さの配列を作る
this::describe Supplier<String> 自分のメソッド(オーバーライド後)
super::describe Supplier<String> 親クラスのメソッド

stream.toArray(Item[]::new) と書くと、Object の配列ではなく Item の配列として受け取れます。出力でも型が Item[] になっています。

computeIfAbsent に ArrayList::new を渡すときの動き

上のコードの byLength.computeIfAbsent(s.length(), ArrayList::new) は正しく動いていますが、1点知っておきたいことがあります。computeIfAbsent は「キーを受け取って値を返す関数」を求めるので、ArrayList::new は キー(ここでは Integer)を引数に取るコンストラクタ、つまり初期容量を指定する ArrayList(int) として解決されます。キーが小さな正の整数ならそのまま動きます。試したところ、キーが -1 のときは実行時に IllegalArgumentException: Illegal Capacity: -1 になり、キーが String の Map では「コンストラクタ参照が無効です」というコンパイルエラーになりました。どちらも k -> new ArrayList<>() とラムダで書くと、意図どおり空のリストが作られます。

メソッド参照とラムダの違い:評価される時点

見た目が短くなるだけでなく、動きが変わる場面が1つあります。:: の左側に書いた式が、いつ評価されるかです。

import java.util.function.*;

public class Timing {
    static String name = "apple";

    static String label() {
        System.out.println("    (label() が呼ばれた)");
        return "ID-";
    }

    public static void main(String[] args) {
        // ---- 実験1:null のときに例外が出る時点 ----
        String text = null;

        System.out.println("1-a. ラムダを作る");
        Supplier<Integer> lambda = () -> text.length();
        System.out.println("     作成できた。get() を呼ぶ");
        try {
            lambda.get();
        } catch (NullPointerException e) {
            System.out.println("     get() で NullPointerException");
        }

        System.out.println("1-b. メソッド参照を作る");
        try {
            Supplier<Integer> ref = text::length;
            System.out.println("     作成できた");
            ref.get();
        } catch (NullPointerException e) {
            System.out.println("     作成した時点で NullPointerException");
        }

        // ---- 実験2::: の左側の式が評価される回数 ----
        System.out.println("2-a. ラムダ");
        Function<String, String> f1 = s -> label().concat(s);
        System.out.println("     作成後。2回呼ぶ");
        System.out.println("     " + f1.apply("1") + " " + f1.apply("2"));

        System.out.println("2-b. メソッド参照");
        Function<String, String> f2 = label()::concat;
        System.out.println("     作成後。2回呼ぶ");
        System.out.println("     " + f2.apply("1") + " " + f2.apply("2"));

        // ---- 実験3:作成後にフィールドを書き換える ----
        Supplier<Integer> byLambda = () -> name.length();
        Supplier<Integer> byRef = name::length;
        name = "watermelon";
        System.out.println("3.   ラムダ: " + byLambda.get() + " / メソッド参照: " + byRef.get());
    }
}

実行結果:

1-a. ラムダを作る
     作成できた。get() を呼ぶ
     get() で NullPointerException
1-b. メソッド参照を作る
     作成した時点で NullPointerException
2-a. ラムダ
     作成後。2回呼ぶ
    (label() が呼ばれた)
    (label() が呼ばれた)
     ID-1 ID-2
2-b. メソッド参照
    (label() が呼ばれた)
     作成後。2回呼ぶ
     ID-1 ID-2
3.   ラムダ: 10 / メソッド参照: 5

3つの実験からわかることをまとめます。

実験 ラムダ メソッド参照(変数::メソッド)
1. 変数が null 作成はできる。呼び出した時点で NullPointerException 作成した時点で NullPointerException
2. :: の左がメソッド呼び出し 呼び出すたびに label() が実行される(2回) 作成時に1回だけ label() が実行される
3. 作成後にフィールドを書き換え 呼び出し時の値(watermelon の10文字)を見る 作成時の値(apple の5文字)を覚えている

Java 言語仕様 15.13.3 には、式::メソッド の形のメソッド参照では、まず :: の左側の式が評価され、その結果が null なら NullPointerException になる、と書かれています。ラムダの本体は、呼び出されるまで実行されません。

どちらを選ぶかの目安は次のとおりです。

状況 向いている書き方 理由
対象のオブジェクトが決まっていて変わらない メソッド参照 作成時に null を検知でき、早く気づける
呼び出すたびに最新の値を見たい ラムダ 呼び出し時にフィールドや式を評価する
対象がまだ null で、後から設定される ラムダ 作成時には評価されない
:: の左が重い処理(生成や検索) メソッド参照 1回だけ実行される

なお、String::length のような「クラス名::メソッド」の形(1番・3番・4番)は、:: の左がクラス名なので、作成時に評価される式がありません。この違いが出るのは、2番の「変数::メソッド」「式::メソッド」の形です。

「参照はあいまいです」を再現して直す

メソッド参照でよく出会うコンパイルエラーが「参照はあいまいです」です。2つのパターンがあります。

パターン1:static とインスタンスの両方に当てはまる(Integer::toString)

import java.util.function.*;

public class A1 {
    public static void main(String[] args) {
        Function<Integer, String> f = Integer::toString;
    }
}
A1.java:5: エラー: 不適合な型: メソッド参照が無効です
        Function<Integer, String> f = Integer::toString;
                                      ^
    toStringの参照はあいまいです
      Integerのメソッド toString(int)とIntegerのメソッド toString()の両方が一致します
エラー1個

Integer クラスには、引数なしのインスタンスメソッド toString() と、int を1つ取る static メソッド toString(int) があります。Function<Integer, String> に対して Integer::toString と書くと、次の2通りに読めてしまいます。

読み方 等価なラムダ 種類
引数の Integer に対して toString() を呼ぶ i -> i.toString() 3(インスタンス)
引数を static の toString(int) に渡す i -> Integer.toString(i) 1(static)

前の章で見たとおり、1番と3番は書き方が同じなので、両方が当てはまるとコンパイラは選べません。

パターン2:渡す先のメソッドがオーバーロードされている(String::valueOf)

import java.util.concurrent.*;
import java.util.function.*;
import java.util.*;
import java.util.stream.*;

public class A2 {
    static void run(Function<Integer, String> f) { System.out.println("Function: " + f.apply(1)); }
    static void run(IntFunction<String> f) { System.out.println("IntFunction: " + f.apply(1)); }
    public static void main(String[] args) {
        run(String::valueOf);
    }
}
A2.java:10: エラー: runの参照はあいまいです
        run(String::valueOf);
        ^
  A2のメソッド run(Function<Integer,String>)とA2のメソッド run(IntFunction<String>)の両方が一致します
エラー1個

こちらは、エラーになっているのが valueOf ではなく run のほうである点が違います。String.valueOf には Object 用・int 用など多くのオーバーロードがあるため、String::valueOf は Function<Integer, String> にも IntFunction<String> にもなれます。その結果、どちらの run を呼ぶのかが決まりません。

直し方

import java.util.List;
import java.util.function.*;
import java.util.stream.*;

public class AmbFix {
    static void run(Function<Integer, String> f) { System.out.println("Function 版: " + f.apply(1)); }
    static void run(IntFunction<String> f) { System.out.println("IntFunction 版: " + f.apply(1)); }

    public static void main(String[] args) {
        // Integer::toString の代わりに書ける形
        Function<Integer, String> a = i -> i.toString();   // ラムダで書く
        Function<Integer, String> b = String::valueOf;     // 別のメソッドを使う
        Function<Integer, String> c = Object::toString;    // 親クラスのメソッドを指す
        System.out.println(a.apply(10) + " " + b.apply(20) + " " + c.apply(30));
        System.out.println(Stream.of(1, 2, 3).map(String::valueOf).collect(Collectors.joining("-")));

        // 受け取る側がオーバーロードされているときは、どちらの型かを示す
        run((Function<Integer, String>) String::valueOf);  // キャストで示す
        run((IntFunction<String>) String::valueOf);
        run((Integer i) -> String.valueOf(i));             // 引数の型を書いたラムダで示す
        run((int i) -> String.valueOf(i));

        // String::valueOf は、受け取る型に合わせて呼ばれるメソッドが変わる
        Function<Object, String> o = String::valueOf;      // valueOf(Object)
        Function<char[], String> ch = String::valueOf;     // valueOf(char[])
        IntFunction<String> in = String::valueOf;          // valueOf(int)
        DoubleFunction<String> d = String::valueOf;        // valueOf(double)
        System.out.println(o.apply(List.of(1)) + " " + ch.apply(new char[] {'a', 'b'}) + " " + in.apply(65) + " " + d.apply(65));
    }
}

実行結果:

10 20 30
1-2-3
Function 版: 1
IntFunction 版: 1
Function 版: 1
IntFunction 版: 1
[1] ab 65 65.0
パターン 直し方 書き方
1 ラムダで書いて、どちらの意味かを示す i -> i.toString()
1 あいまいにならない別のメソッドを使う String::valueOf
1 親クラスのメソッドとして指す Object::toString
2 キャストで受け取る型を示す (Function<Integer, String>) String::valueOf
2 引数の型を書いたラムダにする (Integer i) -> String.valueOf(i)

出力の最後の行は、同じ String::valueOf が、受け取る型によって valueOf(Object)・valueOf(char[])・valueOf(int)・valueOf(double) に解決されることを示しています。char[] を渡した場合は中身の文字列 ab になり、double では 65.0 になっています。メソッド参照は、代入先の型が決まって初めて「どのメソッドか」が決まります。

そのほかのコンパイルエラーと原因

引数の数や戻り値の型が合わないときのメッセージも確認しておきます。

import java.util.function.*;

public class A4 {
    public static void main(String[] args) {
        Supplier<Integer> s = String::length;
        Function<String, Integer> f = String::size;
        Function<String, String> g = String::length;
        BiFunction<String, String, Integer> h = String::length;
    }
}
A4.java:5: エラー: 不適合な型: メソッド参照が無効です
        Supplier<Integer> s = String::length;
                              ^
    非バインド検索で予期しないインスタンスメソッド length()が見つかりました
A4.java:6: エラー: メソッド参照が無効です
        Function<String, Integer> f = String::size;
                                      ^
  シンボルを見つけられません
    シンボル:   メソッド size(String)
    場所: クラス String
A4.java:7: エラー: 不適合な型: メソッド参照の戻り型が不正です
        Function<String, String> g = String::length;
                                     ^
    intをStringに変換できません:
A4.java:8: エラー: 不適合な型: メソッド参照が無効です
        BiFunction<String, String, Integer> h = String::length;
                                                ^
    クラス Stringのメソッド lengthは指定された型に適用できません
      期待値: 引数がありません
      検出値:    String,String
      理由: 実引数リストと仮引数リストの長さが異なります
エラー4個
行 メッセージの要点 原因 直し方
5 非バインド検索で予期しないインスタンスメソッド length()が見つかりました Supplier は引数0個。レシーバーになる引数が無い Function<String, Integer> で受けるか、text::length のように変数を指定する
6 シンボルを見つけられません String に size というメソッドは無い メソッド名を確かめる(String は length)
7 メソッド参照の戻り型が不正です intをStringに変換できません 戻り値の型が合わない 型引数を Integer にする
8 実引数リストと仮引数リストの長さが異なります length() は引数を取らないのに、2つ目の引数を渡す形になっている Function<String, Integer> にする

メソッド参照を var や Object で受けようとした場合のメッセージは次のとおりです。

public class A5 {
    public static void main(String[] args) {
        var v = String::valueOf;
        Object o = String::valueOf;
    }
}
A5.java:3: エラー: ローカル変数vの型を推論できません
        var v = String::valueOf;
            ^
  (メソッド参照には明示的なターゲット型が必要です)
A5.java:4: エラー: 不適合な型: Objectは機能インタフェースではありません
        Object o = String::valueOf;
                   ^
エラー2個

メソッド参照は、それだけでは型が決まりません。代入先や引数の型が関数型インターフェースであることが必要です。javac のメッセージでは、関数型インターフェースが「機能インタフェース」と表記されています。

ラムダとメソッド参照のどちらで書くか

状況 おすすめ 例
メソッドを1つ呼ぶだけ メソッド参照 map(Item::name)
引数を加工する・定数を足す ラムダ map(s -> s.substring(1))
否定したい Predicate.not +メソッド参照、またはラムダ filter(Predicate.not(String::isBlank))
「参照はあいまいです」が出る ラムダ map(i -> i.toString())
呼び出し時に対象を評価したい ラムダ () -> name.length()
クラス名が長く、ラムダのほうが短い ラムダ 読みやすいほうを選ぶ

メソッド参照は「何をするか」がメソッド名で伝わるのが利点です。短くならない場合や、引数の流れが見えたほうがわかりやすい場合は、ラムダのままで問題ありません。

よくある質問

Q. メソッド参照とラムダで、実行結果は変わりますか? A. メソッドを呼ぶだけの処理なら同じ結果になります。違いが出るのは「変数::メソッド」の形で、:: の左側が作成時に1回だけ評価される点です。変数が null なら、呼び出す前の作成時点で NullPointerException になります。

Q. System.out::println は4種類のどれですか? A. 2番の「特定のオブジェクトのインスタンスメソッド参照」です。System.out は PrintStream のオブジェクトが入った static フィールドで、そのオブジェクトの println を指しています。

Q. 引数が2つあるメソッドも参照にできますか? A. できます。String::equalsIgnoreCase を BiFunction<String, String, Boolean> で受けると、1つ目の引数がレシーバー、2つ目がメソッドの引数になります。Integer::sum のような static メソッドなら、2つの引数がそのまま渡されます。

Q. 配列を作るメソッド参照は、どこで使いますか? A. Stream の toArray に渡す使い方が代表的です。toArray(String[]::new) と書くと、String の配列として結果を受け取れます。

まとめ

  • メソッド参照は「メソッドを呼ぶだけのラムダ」の短い書き方。 static・特定のオブジェクト・任意のオブジェクト・コンストラクタの4種類に、配列コンストラクタ参照が加わる。
  • クラス名::インスタンスメソッド では、関数の第1引数がレシーバーになる。 関数の引数はメソッドの引数より1つ多い。
  • コンストラクタ参照は、受け取る型の引数に合わせてコンストラクタが選ばれる。 ArrayList::new を Function として渡すと ArrayList(int) になる。
  • 変数::メソッド は作成時に左側が評価される。 null なら作成時点で NullPointerException。ラムダは呼び出し時。
  • 「参照はあいまいです」は、static とインスタンスの両方に当てはまるか、渡す先がオーバーロードされているときに出る。 ラムダで書くか、キャストで型を示す。
  • メソッド参照は代入先の型で意味が決まる。 var や Object では受けられない。

参考・出典

  • The Java Tutorials「Method References」 https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
  • The Java Language Specification, Java SE 21 Edition「15.13 Method Reference Expressions」 https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.13

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaメソッド参照(::)の使い方|4種類とラムダ対応表」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?