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つ”を渡したいのに、呼び出すメソッドが可変長引数を取るため、配列を利用し複数の引数を渡す可変長引数メソッド呼び出しとして扱われてしまう。

method.invoke( null, 
method.invoke( null...
method.invoke( null, 
method.invoke( null...
呼び出し:concat( 
呼び出し:concat( 
定義:String concat( args )
定義:String concat( args )
"aaa", "bbb" )
"aaa", "bbb" )
"aaa", "bbb" ] )
[ "aaa", "bbb" ]...
"aaa", "bbb" )
"aaa", "bbb" )
可変長引数は内部では配列
可変長引数は内部では配列
配列の第1,第2... 要素を、
呼び出しの第1,第2...引数に
配列の第1,第2... 要素を、...
wrong number of arguments
wrong number of a...
Text is not SVG - cannot display

解決策

方法1: 明示的に配列渡しする

可変長引数メソッド(invoke)の呼び出しに、配列(new Object[])を使う。
その配列の第一要素として、methodの第一引数とする配列を入れ子にする。

method.invoke(null, new Object[]{ new String[]{"aaa", "bbb"} })
method.invoke( null, 
method.invoke( null...
呼び出し:concat( 
呼び出し:concat( 
定義:String concat( args )
定義:String concat( args )
[ [ "aaa", "bbb" ] ] )
[ [ "aaa", "bbb" ]...
[ "aaa", "bbb" ] )
[ "aaa", "bbb" ] )
配列の第1,第2... 要素を、
呼び出しの第1,第2...引数に
配列の第1,第2... 要素を、...
Text is not SVG - cannot display

方法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” のみとなり、手間取った。

Tags:

Categories:

Updated: