为什么反射调用方法需要将varargs包装在数组中?

时间:2013-06-13 17:42:04

标签: java

在S.O.上回答this other question ,我遇到了一个我无法理解的奇怪问题。为什么反射'invoke'方法类需要将varargs参数包装在一个数组中才能工作?

使用vararg参数的其他类方法运行正常,只是以正常的方式调用它们......

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 个答案:

答案 0 :(得分:6)

这是因为varargs和数组参数类型存在歧义。基本上,当方法声明为

void takesSomeArgs(Object... values) {
}

这大致相当于

void takesSomeArgs(Object[] values) {
}

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

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

第一种解释是

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

根据JLS 15.12.2.3的规则,编译器订阅第二种解释(在invoke(Object, Object...)本身的调用上,这是可变的),这导致尝试传递 2 < / em>你的方法的参数 - 失败,因为该方法只需要一个(编辑注意,我在这里描述了错误的症状;请参阅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));  
}

没有引入新语法。

答案 1 :(得分: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. 将数组转换为Object,以显示该数组只是一个参数,而不是参数集 在我的someMethod示例

    中应该按照案例1.和2.进行处理
    m.invoke(null, (Object) arguments3); 
    

答案 2 :(得分:3)

的两种解释
m.invoke(null, arguments3);

第一种解释相当于

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

第二种解释相当于

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

请注意,这与

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

这非常重要。在第二种解释中,String[]本身正在转换为Object[],并在args参数中用于invoke; arguments3被视为args被视为组成args的数组的元素。

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

showIt("buonasera")

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

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

  

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

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

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

因此,

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

工作得很好。

答案 3 :(得分:-1)

如果您有两个重载方法:

showIt(String firstname, String lastname);

showIt(String names ...);

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