Java : メソッド参照の基本

メソッド参照を使うと、クラスのメソッド1つを、関数型インタフェースのインスタンスとして参照および実行できるようになります。
これにより、リスナーやイベントなどのコールバック処理を、より柔軟に実装することができます。

機能としては、C#のデリゲートに近いですね。

本記事では、そんなメソッド参照の基本的な使い方をご紹介します。


はじめに

本記事は、ラムダ式や関数型インタフェースをある程度理解していることを前提としています。
もし不安のあるかたは、以下の記事も合わせてご参照ください。

概要

15.13. Method Reference Expressions
A method reference expression is used to refer to the invocation of a method without actually performing the invocation.

メソッド参照は、Java言語仕様に組み込まれた機能です。
他のプログラミング言語では、デリゲートや関数ポインタと呼ばれている機能に近いものです。

メソッド参照を使うと、通常のクラスやインタフェースの1メソッドを、関数型インタフェースのインスタンスとして参照および実行できるようになります。

簡単なコード例

まずは関数型インタフェースだけを使ってみましょう。

Streamの文字列を大文字に変換する例になります。
関数型インタフェースには、パラメータを1つと戻り値を返すFunction<T, R>を使います。

final Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
final Function<String, String> func = new Function<String, String>() {
    @Override
    public String apply(String s) {
        return s.toUpperCase();
    }
};

final var list = stream.map(func).toList();

System.out.println(list); // [AAA, BBB, CCC]

OKでしょうか。
次にラムダ式に置き換えてみましょう。

final Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
final Function<String, String> func = (s) -> s.toUpperCase();

final var list = stream.map(func).toList();

System.out.println(list); // [AAA, BBB, CCC]

これもOKですね。
次は、メソッド参照に置き換えてみます。

final Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
final Function<String, String> func = String::toUpperCase;

final var list = stream.map(func).toList();

System.out.println(list); // [AAA, BBB, CCC]

ちょっと見慣れない形になりました。
メソッド参照は、対象となるクラス…この例では Stringクラスと、そのメソッドを :: (コロン2つ)で指定します。

  • String::toUpperCase

func.applyを呼び出すことは、Stringの toUpperCase を呼び出すことと同じになります。

final Function<String, String> func = String::toUpperCase;

System.out.println(func.apply("aaa")); // "AAA"

// ↓と同じ意味です。
System.out.println("aaa".toUpperCase()); // "AAA"

つまり、func は toUpperCase メソッドを参照しているわけですね。

もしくは、インスタンス変数に :: を使うことでもメソッド参照が可能です。

その場合、関数型インタフェースは、対象となるインスタンスを受け取る必要はありません。
結果だけを返せばいいので、Supplier<T>に変更します。

final var str = "aaa";
final Supplier<String> supplier = str::toUpperCase;

System.out.println(supplier.get()); // "AAA"

// ↓と同じ意味です。
System.out.println(str.toUpperCase()); // "AAA"

標準で用意されている関数型インタフェース以外にも、自分で定義した関数型インタフェースも使えます。
2つのパラメータが必要となる例です。

@FunctionalInterface
public interface SampleFunc {

    String replace(String target, char oldChar, char newChar);
}

public void start() {

    final SampleFunc func = String::replace;

    System.out.println(func.replace("aaabbb", 'b', 'Z')); // "aaaZZZ"

    // ↓と同じ意味です。
    System.out.println("aaabbb".replace('b', 'Z')); // "aaaZZZ"
}

メリット

デリゲートとして使える

メソッド参照の1番の利点だと思います。

具体例で見てきましょう。

public class Foo {

    private FooListener listener;

    @FunctionalInterface
    interface FooListener {
        void onCompleted();
    }

    public void setListener(FooListener listener) {
        this.listener = listener;
    }

    public void execute() {
        System.out.println("Foo execute");
        listener.onCompleted();
    }
}
public class Bar {

    private BarListener listener;

    @FunctionalInterface
    interface BarListener {
        void onCompleted();
    }

    public void setListener(BarListener listener) {
        this.listener = listener;
    }

    public void execute() {
        System.out.println("Bar execute");
        listener.onCompleted();
    }
}

FooクラスとBarクラスは、ほぼ同じクラスです。

  1. setListener で listener を設定
  2. executeメソッドを呼び出し
  3. listenerのonCompletedが呼びだされる(コールバック)

というシーケンスになります。

次に、Sampleクラスで、Foo と Bar のコールバックを受け取りたいとします。

public class Sample implements Foo.FooListener, Bar.BarListener {
    @Override
    public void onCompleted() {
        System.out.println("onCompleted!");
    }

    public void start() {
        final var foo = new Foo();
        final var bar = new Bar();

        foo.setListener(this);
        bar.setListener(this);

        foo.execute();
        bar.execute();

        // 実行結果
        // ↓
        //Foo execute
        //onCompleted!
        //Bar execute
        //onCompleted!
    }
}

ここで問題になるのが、FooListenerとBarListenerがどちらも onCompleted という同じ名前のメソッドを持っていることです。

FooListener, BarListener の両方を implements してもコンパイルエラーにはなりません。
ただ、onCompleted の呼び出し元が FooなのかBarなのか判別できませんね。

もし判別したい場合、解決方法はいろいろとありますが、今回はメソッド参照を使ってみましょう。

まず implemenets から、それぞれの Listener は外します
そして、それぞれのコールバック用のメソッドとして onFooCompleted, onBarCompleted を用意します。

Foo と Bar の setListener では、メソッド参照として、onFooCompleted と onBarCompleted を指定します。
自身のメソッドを参照するときは this を使います。

public static class Sample {

    public void onFooCompleted() {
        System.out.println("Foo onCompleted!");
    }

    public void onBarCompleted() {
        System.out.println("Bar onCompleted!");
    }

    public void start() {
        final var foo = new Foo();
        final var bar = new Bar();

        foo.setListener(this::onFooCompleted);
        bar.setListener(this::onBarCompleted);

        foo.execute();
        bar.execute();

        // 実行結果
        // ↓
        //Foo execute
        //Foo onCompleted!
        //Bar execute
        //Bar onCompleted!
    }
}

無事、Foo と Bar の呼び出しを分けることができました。
メソッド参照のよいところは、implements が不要となり、コールバック用のメソッド名を自由にできることですね。

意外とメソッド名がぶつかることはあります…
分かりやすい一般的な名前にしようとすればするほど、名前がぶつかりやすくなるジレンマ…それもメソッド参照で解決です。

ラムダ式よりさらに簡略化した記述

主に Stream API で関数型インタフェースを使うときに、ラムダ式より簡略化した記述ができます。

メソッド参照 ラムダ式
final var stream = Stream.of("aaa", "bbb", "ccc");

// "aaa"
// "bbb"
// "ccc"
stream.forEach(System.out::println);
final var stream = Stream.of("aaa", "bbb", "ccc");

stream.forEach(s -> {
    // "aaa"
    // "bbb"
    // "ccc"
    System.out.println(s);
});
final var stream = Stream.of("aaa", "bbb", "ccc");

final var list = stream.map(
        s -> s.toUpperCase()).toList();
System.out.println(list); // [AAA, BBB, CCC]
final var stream = Stream.of("aaa", "bbb", "ccc");

final var list = stream.map(
        String::toUpperCase).toList();
System.out.println(list); // [AAA, BBB, CCC]

とはいえ、記述量的にはラムダ式でも十分かな、と個人的には思います。

コンストラクタ参照

メソッド参照のコンストラクタ版です。コンストラクタを参照します。
メソッド名の代わりに new を使います。

まずはコンストラクタにパラメータがない例です。

final Supplier<ArrayList<String>> supplier = ArrayList::new;

final ArrayList<String> list1 = supplier.get();
System.out.println(list1); // []
System.out.println(list1.size()); // 0

// ↓と同じ意味です。
final ArrayList<String> list2 = new ArrayList<>();
System.out.println(list2); // []
System.out.println(list2.size()); // 0

パラメータがある例です。

final Function<byte[], String> func = String::new;
final byte[] bytes = "abcd".getBytes();

final String str1 = func.apply(bytes);
System.out.println(str1); // "abcd"

// ↓と同じ意味です。
final String str2 = new String(bytes);
System.out.println(str2); // "abcd"

配列も可能です。

final IntFunction<int[]> func = int[]::new;

final int[] array1 = func.apply(5);
System.out.println(Arrays.toString(array1)); // [0, 0, 0, 0, 0]

// ↓と同じ意味です。
final int[] array2 = new int[5];
System.out.println(Arrays.toString(array2)); // [0, 0, 0, 0, 0]

まとめ

メソッド参照を使うことで、リスナーやイベントなどのコールバック処理を、より柔軟に実装することができます。

C#のデリゲートや、それに類似する機能は、Java以外のプログラミング言語でもよく見かけます。
それだけ有用な機能なのかなと思います。

ぜひ、Javaでも有効に活用していきましょう。


関連記事

ページの先頭へ