18
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Systemi(システムアイ)Advent Calendar 2022

Day 24

Javaのメソッド参照で注意べきすること

Last updated at Posted at 2022-12-23

java 8 では様々な機能が追加され、その中でも「ラムダ式」はかなり注目され、話題にもなりました。
同じバージョンで「メソッド参照」という機能も追加されましたが、注目度はイマイチのようです。
ラムダ式に比べると、できることも限られるし、そもそもラムダ式で代用できるため、致し方ないかもしれません。
ただ、メソッド参照で記述するとコード量が減り、かなりスッキリします。
そして、自分が好むか好まざるかに関わらず、既存のコードに含まれていれば、嫌でも理解しなければならないでしょう。
ということで、今回はメソッド参照についての記事となります。
メソッド参照を使うとスマートに記述できますが、結構わかりにくいところや落とし穴があるように感じました。
そのあたりを見ていこうと思います。
まずはテストで使用するコードです。

Greeter.java
package test01;

public class Greeter {
	private String greet;

	public Greeter(String greet) {
		this.greet = greet;
	}

	public void setGreet(String greet) {
		this.greet = greet;
	}

	public void hello() {
		System.out.println(greet);
	}
}

まあ、あまり解説は不要かと思います。
次に、ラムダ式やメソッド参照で使うインターフェースの定義です。
そのインターフェースは「関数型インターフェース」といい、メソッドは1つしか持てません。

FuncInter0.java
package test01;

@FunctionalInterface
public interface FuncInter0 {
	public void func();
}

@FunctionalInterfaceはなくてもいいですが、指定すると、例えばメソッドを複数定義するとエラーとなるなど、チェックをしてくれるようです。
これらを使うコードを書いていきます。
まず準備のコードです。

準備
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;

fi0変数は後で複数回インスタンスを代入するため、ここでは初期化のみを行っています。
さて、まずは匿名クラスの復習です。

匿名クラス

匿名クラス
// 匿名クラス
fi0 = new FuncInter0() {
	@Override
	public void func() {
		greeter.hello();
	}
};
fi0.func();

上のコードを実行すると「今日は!」が出力されます。
特に問題ないですね。

ラムダ式

上と同じことをラムダ式を使って書いてみます。

ラムダ式
// ラムダ式
fi0 = () -> greeter.hello();
fi0.func();

劇的に短くなりました。
なるほど便利ですね。
次に、同じことをメソッド参照を使って書いてみます。

メソッド参照

メソッド参照
// メソッド参照
fi0 = greeter::hello;
fi0.func();

メソッド参照は「(インスタンスを格納した)変数名::メソッド」という形をしており、それを代入したり引数として指定したりすることができます。
他に「クラス名::メソッド」という形式もありますが、ここでは触れません。
匿名クラスがラムダ式になったのに比べると、地味ですね。

連結した実行コード

上記の「準備」、「匿名クラス」、「ラムダ式」、「メソッド参照」のコードを単純に連結したコードを一応載せておきます。

実行コード
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;

// 匿名クラス
fi0 = new FuncInter0() {
	@Override
	public void func() {
		greeter.hello();
	}
};
fi0.func();

// ラムダ式
fi0 = () -> greeter.hello();
fi0.func();

// メソッド参照
fi0 = greeter::hello;
fi0.func();

上のコードを実行すると、「今日は!」が3回出力されます。
同じことを3回やっているので当然ですね。

コンパイルエラー?

さて、問題はここからです。
例えば次のようなコードを実行コードの最後にでも追加すると、コンパイルが通らなくなります。

greeter初期化
greeter = null;

エラー内容は以下のとおり。
"Local variable greeter defined in an enclosing scope must be final or effectively final"

これがどこで出るかというと、匿名クラスとラムダ式の中でgreeter変数を使っているところです。
greeterfinalにしろ、と言っているようです。

確かに匿名クラスの文法では、メソッド外の変数にアクセスする場合、その変数はfinalでないとダメでした。
でも、うまくいっていたコードでもgreeter変数にfinalはつけていません。
greeterに代入するコードを追加するまでは、暗黙的にfinalとみなしていたとしか思えないですね。

このコンパイルエラーを深掘りするつもりはないですが、メソッド参照でgreeterを使っているところでは、エラーになっていません。
このことが、メソッド参照を使うときの注意点のヒントになります。

メソッド参照は使われた時点のインスタンスを保持する

メソッド参照はラムダ式(ひいては匿名クラス)と同等のはずです。
なので、ラムダ式でコンパイルエラーになるのであれば、メソッド参照でもエラーになるべきだと個人的には思います。
でも実際にはコンパイルエラーにならないため、逆にちょっとした混乱の元となる可能性があります。
ちょっと実験をしてみます。
上のメソッド参照のコードを次のように変えます。

メソッド参照その2
// メソッド参照その2
fi0 = greeter::hello;
fi0.func();
greeter = new Greeter("今晩は!");
fi0.func();

これを準備コードの後に続けます。
要するに次のようなコードにします。

実行コードその2
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;

// メソッド参照その2
fi0 = greeter::hello; // ※1
fi0.func();
greeter = new Greeter("今晩は!"); // ※2
fi0.func();

新たにGreeterのインスタンスを生成し、greeter変数に代入しています(上記コードの※2)。
fi0変数に代入されたメソッド参照が、greeter変数が参照するインスタンスのhelloメソッドを呼び出すのであれば、2回目の出力は「今晩は!」になりそうです。
でも実際にはそうはならず、「今日は!」が出力されます。
これから分かることは、メソッド参照はそれが使用された(上記コードの※1)時点のインスタンスを保持しており、それを使用しているということです。
greeter変数がその後上書きされても、参照するインスタンスが変わるわけではないようです。
greeterfinalであれば、それを上書きして代入する(※2のような)コードは書けないため、混乱することもないんですけどね。

メソッド参照はディープコピーしない

もう1つ実験をしてみます。
上のメソッド参照のコードをさらに次のように変えます。

メソッド参照その3
// メソッド参照その3
fi0 = greeter::hello;
fi0.func();
greeter.setGreet("今晩は!");
fi0.func();

これを準備コードの後に続けます。
要するに次のようなコードにします。

実行コードその3
// 準備
Greeter greeter = new Greeter("今日は!");
FuncInter0 fi0 = null;

// メソッド参照その3
fi0 = greeter::hello; // ※1
fi0.func();
greeter.setGreet("今晩は!"); // ※2
fi0.func();

今度は、greeter変数に新たなインスタンスを代入して上書きしているわけではなく、setterを使って、インスタンスの中身を変えているだけです(上記コードの※2)。
もしメソッド参照が、それが使用された(上記コードの※1)時点でインスタンスをディープコピーし、それを保持しているのであれば、2回目の出力は「今日は!」と出力されるはずです。
でも実際にはそうはならず、「今晩は!」が出力されます。
よって、ディープコピーしたインスタンスではなく、インスタンスそのものを保持し、それを使用していることがわかります。

今回はJavaのメソッド参照についての記事でした。
かなりとんがった話題でしたが、実際にハマると、かなり苦戦するのではないかと思いました。

18
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?