Java 可変長引数メソッドをリフレクションで呼び出す
Javaのプロジェクトで「privateかつ可変長引数を取るメソッド」の単体テストを書きたくなった際、リフレクションによる呼び出しでハマったことを書きます。
答え
// "aaa", "bbb" -> "aaabbb"
private static String concat(String ... args) {
return String.join("", args);
}
上のconcat
メソッドを他のクラスからリフレクションで呼び出す際は、目的の引数へ渡すオブジェクトを格納した配列を Object
にキャストし、invoke
に渡せばよい
public static void test_concat() throws Exception {
Method method = Impl.class.getDeclaredMethod("concat", String[].class);
method.setAccessible(true);
assert "aaabbb".equals(method.invoke(null, (Object)new String[]{"aaa", "bbb"}));
}
以下、経緯や思考など
実行環境
openjdk version "18.0.1.1" 2022-04-22
OpenJDK Runtime Environment (build 18.0.1.1+2-6)
OpenJDK 64-Bit Server VM (build 18.0.1.1+2-6, mixed mode, sharing)
前提: 通常の引数の場合
privateなメソッドであっても、以下のようにメソッド名と引数の型の情報から Method
オブジェクトを取得し、呼び出すことができる。
// 実装クラス
private static String concat(String arg0, String arg1) {
return arg0 + arg1;
}
// テストクラス
public static void test_concat() throws Exception {
Method method = Impl.class.getDeclaredMethod("concat", String.class, String.class);
method.setAccessible(true);
assert "aaabbb".equals(method.invoke(null, "aaa", "bbb"));
}
補足: Class#getDeclaredMethod
Class#getDeclaredMethod
は、 メソッド名を表す文字列, 第1引数のクラス, 第2引数のクラス, ...
から、合致するメソッドのMethodオブジェクトを取得するメソッド。
Methodオブジェクトは method.invoke(対象インスタンス, 第1引数, 第2引数...)
で呼び出せる。
本題: 可変長引数の場合
下のように、可変長引数に変更するとどうなるか。
// 実装クラス
private static String concat(String ... args) { // 可変長引数
return String.join("", args);
}
まず、 getDeclaredMethod
へ渡すクラス情報について考える。”可変長”を表すClassオブジェクトとは…?
可変長引数の正体
ドキュメント - 可変長引数 によると、実は可変長引数は配列による受け渡しのショートカットらしい。
つまり、上のメソッドは”配列1つを引数として取る”と扱うことができる。
// 実装クラス
private static String concat(String[] args) { // 配列引数
return String.join("", args);
}
従って、getDeclaredMethod
の引数クラスとしては String[]
を渡せばよい。
// テストクラス
public static void test_concat() throws Exception {
Method method = Impl.class.getDeclaredMethod("concat", String[].class);
method.setAccessible(true);
assert "aaabbb".equals(method.invoke(null, "aaa", "bbb"));
}
これでMethodオブジェクトの取得ができるようになったが、今度はそのinvokeでエラーになってしまった。
引数の配列?配列が引数?
エラーメッセージは以下。
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments: 2 expected: 1
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.checkArgumentCount(DirectMethodHandleAccessor.java:337)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:102)
at java.base/java.lang.reflect.Method.invoke(Method.java:577)
at com.example.TestImpl.test_concat(TestImpl.java:9)
at com.example.Main.main(Main.java:5)
wrong number of arguments: 2 expected: 1
、つまり配列1つを取るメソッドに対し、オブジェクト2つを渡してしまっている。
Method#invoke
の内部では、可変長引数→配列 の変換は行ってくれない模様。
そこで、配列オブジェクトを渡すようにしてみた。
// before
method.invoke(null, "aaa", "bbb");
// after
method.invoke(null, new String[]{"aaa", "bbb"});
しかし、まだ同じエラーが出た。
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments: 2 expected: 1
なぜか。
Method#invoke
自体も可変長引数を取るメソッドであるため、
method.invoke(null, "aaa", "bbb")
method.invoke(null, new String[]{"aaa", "bbb"})
この2つは同等、ということになる。
第1引数として”配列1つ”を渡したいのに、呼び出すメソッドが可変長引数を取るため、配列を利用し複数の引数を渡す可変長引数メソッド呼び出しとして扱われてしまう。
解決策
方法1: 明示的に配列渡しする
可変長引数メソッド(invoke
)の呼び出しに、配列(new Object[]
)を使う。
その配列の第一要素として、methodの第一引数とする配列を入れ子にする。
method.invoke(null, new Object[]{ new String[]{"aaa", "bbb"} })
方法2: 配列をObjectにキャストする
配列をそのまま渡すと、可変長引数メソッドの配列渡しとして解釈されてしまう。
ならば、配列をObjectにキャストし、可変長引数メソッド呼び出しの第一引数としか解釈できないようにしてやればよい。
method.invoke(null, (Object)new String[]{"aaa", "bbb"})
なぜハマったか
実は、最初のやり方をすると、cast to Object for a varargs call
と親切な警告メッセージが出力される。
com/example/TestImpl.java:9: warning: non-varargs call of varargs method with inexact argument type for last parameter;
assert "aaabbb".equals(method.invoke(null, new String[]{"aaa", "bbb"}));
^
cast to Object for a varargs call
cast to Object[] for a non-varargs call and to suppress this warning
1 warning
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of arguments: 2 expected: 1
しかし、使っていた環境(IDE組み込みのJUnit)では、Exception以降のみが表示される仕様になっていたため、手がかりが “IllegalArgumentException: wrong number of arguments” のみとなり、手間取った。