Java : メソッド参照の基本
メソッド参照を使うと、クラスのメソッド1つを、関数型インタフェースのインスタンスとして参照および実行できるようになります。
これにより、リスナーやイベントなどのコールバック処理を、より柔軟に実装することができます。
機能としては、C#のデリゲートに近いですね。
本記事では、そんなメソッド参照の基本的な使い方をご紹介します。
はじめに
本記事は、ラムダ式や関数型インタフェースをある程度理解していることを前提としています。
もし不安のあるかたは、以下の記事も合わせてご参照ください。
概要
メソッド参照は、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クラスは、ほぼ同じクラスです。
- setListener で listener を設定
- executeメソッドを呼び出し
- 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 で関数型インタフェースを使うときに、ラムダ式より簡略化した記述ができます。
メソッド参照 | ラムダ式 |
---|---|
|
|
|
|
とはいえ、記述量的にはラムダ式でも十分かな、と個人的には思います。
コンストラクタ参照
メソッド参照のコンストラクタ版です。コンストラクタを参照します。
メソッド名の代わりに 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でも有効に活用していきましょう。