2

在 SO 上回答这个其他问题时,我遇到了这个我无法理解的奇怪问题。为什么 Method 类的反射“调用”方法需要将 varargs 参数包装在数组中才能工作?

其他具有可变参数的类方法运行良好,只需以正常方式调用它们......

public class Main {
   public static void main(String[] args){


    try {
        Class<?> p = Main.class;

        String[] arguments1 = {"ciao"};
        String[] arguments2 = {"salve"};
        String[] arguments3 = {"buonasera"};

        Method m = p.getDeclaredMethod("showIt",String[].class);


        //this is ok
        showIt(arguments1);

        //this is ok
        m.invoke(null, new Object[]{arguments2});

        //this throws IllegalArgumentException!!
        m.invoke(null, arguments3);

    } catch (Exception e) {
        e.printStackTrace();
    }
  }

  public static void showIt(String[] result) {
    System.out.println(result[0]);
  }


}

调用方法有什么特别之处?

4

4 回答 4

7

这是因为这里对于可变参数和数组参数类型存在歧义。基本上,当一个方法被声明为

void takesSomeArgs(Object... values) {
}

这主要相当于

void takesSomeArgs(Object[] values) {
}

问题是,现在对以下调用有两种可能的(同样有效的)解释:

Object[] example = { "foo", "bar" };
takesSomeArgs(example);

第一种解释是

  • 使用单个参数值调用该方法,即values类似于Object[1] { Object[1] { "foo", "bar" } }(可变参数解释)
  • 使用两个参数值调用该方法,即values类似于Object[2] { "foo", "bar" },因为Object...本质上是Object[],因此,我们可以将数组引用example直接作为 的值传递value

根据JLS 15.12.2.3的规则,编译器订阅第二个解释(在调用invoke(Object, Object...)自身时,它是可变参数),这导致尝试将两个参数传递给您的方法 - 由于方法期望只有一个(编辑注意,我在这里描述了错误的症状;请参阅 Json 的答案,它是正确的)。换句话说,像这样的调用:

m.invoke(receiver, new Object[] { "a", "b", "c" });

m.invoke(receiver, "a", "b", "c");

实际上是一回事。因此需要写

m.invoke(receiver, new Object[] { new Object[] { "a", "b", "c" } });  // (X)

(明确地)或

m.invoke(receiver, (Object) (new Object[] { "a", "b", "c" }));

请注意强制转换:它使参数不再可转换为Object[]并强制编译器生成与 (X) 等效的代码。另请注意,new Object[] { ... }不要按字面意思理解,而只是为了使所涉及的值的类型清楚。

在我看来,这是一个不幸的副作用,Java 语言的设计者希望能够(并且方便)实现类似的东西

void logError(String control, Object... parameters) {
    // Forwarding the unknown number of arguments from one variadic method
    // to the next: String.format(String, Object...)
    log.message(String.format(control, parameters));  
}

没有引入新的语法。

于 2013-06-13T17:52:02.477 回答
6

可以说我们有方法

public static void someMethod(String... arguments){
    // implementation is irrelevant but will add it for demo purpose
    System.out.println("I have "+arguments.length +"arguments which are:");
    for (String arg:arguments)
        System.out.println(arg);
}

您可能知道 vararg 实际上是数组,但它很特殊,因为它允许以形式传递参数

  • someMethod("arg1")
  • someMethod("arg1", "arg2").

但是不要忘记,因为...数组接受数组参数,例如

  • someMethod(new MyArgumentsType[]{arg1, arg2, arg3}).

我们知道它invoke被声明为public Object invoke(Object obj, Object... args)

现在您要调用showIt(String[] result)需要String[]作为参数的方法。

你这样调用invoke方法m.invoke(null, arguments3);。由于每个数组都可以被视为Object[] invoke将理解您正在数组中传递一组参数(看看someMethod我的示例中的第三种调用方式)。这种方式方法只会找到一个参数:"buonasera". 但showIt不需要String[]String所以你会看到“参数类型不匹配”异常。

要摆脱这个问题,你有两个选择。

  1. 将您的 String 数组包装在其他数组中,就像您在m.invoke(null, new Object[] { arguments2 });. 这样 varargs 会将 Object 数组的竞争视为参数列表,因此它将找到包装的 String[] 数组作为正确的参数。

  2. someMethod将您的数组转换为 Object,以表明该数组只是一个参数,而不是一组参数,在我的示例中应视为案例 1. 和 2.

    m.invoke(null, (Object) arguments3); 
    
于 2013-06-13T18:18:11.870 回答
3

两种解释

m.invoke(null, arguments3);

第一种解释等价于

m.invoke(null, (Object)arguments3);

第二种解释等价于

m.invoke(null, (Object[])arguments3);

请注意,这不同于

m.invoke(null, new Object[] { arguments3 });

这个非常重要。在第二种解释中,String[]is 本身被强制转换为 anObject[]并用于args参数 to invoke; 即被arguments3视为args而不是组成数组的元素args

默认情况下,Java 将选择第二种解释。在这种情况下,它将解开 的元素Object[]并将其用作 的参数showIt。所以从 的角度来看showIt,它看起来像被调用为

showIt("buonasera")

这显然是无效的,因为showIt期待 a String[],而不是 a String

事实上,如果您阅读Method.invoke的文档,您会看到:

IllegalArgumentException- 如果方法是实例方法并且指定的对象参数不是声明底层方法(或其子类或实现者)的类或接口的实例;如果实际参数和形式参数的数量不同;如果原始参数的展开转换失败;或者,如果在可能的展开之后,参数值不能通过方法调用转换转换为相应的形式参数类型。

这正是这里发生的事情。同样,因为它看起来showIt是使用单个String参数而不是单个String[]参数调用的。

但在第一种解释中,我们将其视为varargs参数中String[]的一个实例Object并将其用作 an 中的单个元素。因此,如果您明确告诉 Java 它不能执行 to 的隐式转换,那么最终将作为第一个形式参数传递,而不是使用 the 的元素作为 to 的形式参数。Object[]argsString[]Object[]String[]String[]showIt

因此,

m.invoke(null, (Object)arguments3);

会工作得很好。

于 2013-06-13T18:19:08.377 回答
-1

如果您有两个重载方法:

showIt(String firstname, String lastname);

showIt(String names ...);

当你调用时invoke,java 必须知道你调用的是哪个方法。传递两个参数调用第一个,传递一个字符串数组调用第二个。

于 2013-06-13T17:46:45.927 回答