Java变长参数的限制

Java从Java 5开始,引入了可变长参数(varargs)机制,这个机制不是所有语言都有,并且各个语言实现可变长参数的方式也都不尽相同。 Java5是Java语言历史上改动最大的一代,引入了泛型机制。 我们知道Java的泛型是“伪泛型”,只是“编译器小说(compiler fiction)”,因为只是Java语言层的改进,而JVM层没有做改变。因此Java的泛型便存在了若干限制(譬如不支持泛型数组)。

同样,Java的可变长参数也存在若干限制。并且Java支持继承和函数重载,因此在选择调用哪个函数时,变会存在更多规则,以及一些不够优雅的现象。

本质是语法糖

和泛型一样,可变长参数只是编译器提供的语法糖,是通过数组来实现的。 在JVM层并没有做任何改动,只是编译器前端将可变参数翻译成了数组。

因此这两行代码,编译生成的字节码是完全一样的:

    test.foo("1", "2");
    test.foo(new String[] {"1", "2"});
    /*
      21: aload_1
      22: iconst_2
      23: anewarray     #21                 // class java/lang/String
      26: dup
      27: iconst_0
      28: ldc           #22                 // String 1
      30: aastore
      31: dup
      32: iconst_1
      33: ldc           #23                 // String 2
      35: aastore
      36: invokevirtual #24                 // Method foo:([Ljava/lang/String;)V
    */

类型不安全

正如前面所说,可变长参数本质是转换为数组对象进行传递。而Java不支持泛型数组why?,因此对于引用类型而言,可变长参数是类型不安全的,需要加上SafeVarargs注解,以绕过编译器检查。

以下代码摘自Java标准库 Collections.java

    @SafeVarargs
    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }

这样可变参数数组,实质上就是一个泛型数组。

譬如这样的代码,是完全合法的:

    public void foo(List<Integer>... arg) {
        System.out.println("hello");
    }
    List<Integer> list = new ArrayList<>();
    List list2 = list;
    list2.add("boom");
    test.foo(list2);

因此泛型数组的类型安全问题,在可变参数上都得到了体现。

一个有意思的现象

之前说了,可变长参数本质是语法糖。可以用数组来替代。 可以将数组传给可变参数,但不能将可变参数传给数组参数。 后者比较容易理解,为了保证原有代码的语义。

但是我觉得“可以将数组传给可变参数”则不是一个很好的想法,尤其是当数组作为参数时,会产生歧义,导致各种稀奇古怪的问题,而这些歧义只能通过编译器的“约定”来解决。 虽然正常人是不会写这种代码的,但是这种不完备性依然会让人觉得很不舒服。

对于如下代码:

    public void foo(Object... arg) {
        System.out.println(arg[0].getClass());
    }
    test.foo(1);
    test.foo(1, 2);
    /* output:
     class java.lang.Integer
     class java.lang.Integer
     */

传入一个参数或者传入两个参数,最后结果是一样的,这点毫无疑问。

但是另一种情况下:

    test.foo(new Object[] {1, 2, 3});
    test.foo(new Object[] {1, 2, 3}, new Object[] {4, 5, 6});
    /* output:
     class java.lang.Integer
     class [Ljava.lang.Object;
    */

如果只传一个数组,那么Java会把数组当成可变参数来看,而如果传入两个数组,则会把数组本身当成一个可变参数数组的一个元素。 这种异常的表现,如果不清楚可变参数底层原理的话,就难以理解了。

当然,不能只怪可变参数,因为Object是所有类的公共父类,如果上述例子改成Integer就不会有这个问题了。

一些其他问题

在函数重载时,如果有多个函数可以调用,具体调用哪个函数的优先级是一个问题。一旦加入可变参数后,这个问题就更加烧脑。

譬如这种情况,到底会调用哪一个呢?

    public void foo(String s, String... arg) {
        System.out.println("String s, String... arg");
    }

    public void foo(String s, Object arg) {
        System.out.println("String s, Object arg");
    }
    test.foo("1", "2");
    /* output:
     String s, Object arg
    */

好在大部分人并不会写这样的代码,并且现在的IDE都可以很清楚地指出实际调用的函数。

总结

Java由于历史原因,后加入了很多特性,譬如泛型、可变参数等等,不可避免地带来了一下不够优雅的地方,而Java为了保证兼容性而做出的一些妥协,我觉得还是值得的。并且那些可以由IDE很容易就检测出来的错误,就没必要像孔乙己那样去钻牛角尖了。

Java里还有很多稀奇古怪的问题,推荐阅读《Java解惑》


点击加载Disqus评论

Powered by Jekyll and Theme by solid