Java方法参数:var args vs array

时间:2014-08-09 10:57:32

标签: java arrays guava variadic-functions

在整个Google Guava库中,我注意到了使用"一个(或两个)加上var gugs"技术

示例:

  • void add(T value, T... moreValueArr)
  • void add(T value, T value2, T... moreValueArr)

我花了一些时间才弄清楚原因:为了防止使用零args(第一种情况下)或一种arg(第二种情况)调用。

如果在下面的情景A和B之间做出选择,那么进一步扩展这项技术,哪个更好?我希望有深入Java知识的人可以提供见解。

情景A :(两种方法)

  1. void add(T... valueArr)
  2. void add(Iterable<? extends T> iterable)
  3. 情景B :(三种方法)

    1. void add(T value, T... moreValueArr)
    2. void add(T[] valueArr)
    3. void add(Iterable<? extends T> iterable)
    4. 为什么 B 可能更好的一个想法:我注意到许多Java程序员都不知道数组可以直接作为var args传递。因此, B 可能提供可能的提示。

      最后,我意识到 B 有额外的开发,测试和维护开销。请把这些考虑放在一边。

      这个问题是我原来问题的一个微妙变化:Java varags method param list vs. array

5 个答案:

答案 0 :(得分:6)

结论将在最后绘制。如果您只想得出结论,请跳到最后。


主要目标是表现。

如果有很多用例只有1或2个元素可以通过,则可以避免创建数组。是的,仍然会传递一个零长度数组,但由于无法修改零长度数组,因此允许JVM传递共享实例,例如,如果缓存完全没有性能影响。

最明亮的例子是EnumSet.of()方法(返回列出的枚举实例的EnumSet)。

您将看到以下重载:

static <E extends Enum<E>> EnumSet<E> of(E e);
static <E extends Enum<E>> EnumSet<E> of(E first, E... rest);
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2);
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3);
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4);
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5);

如果使用5个或更少的元素调用of()方法,则不会创建任何数组,因为存在可能需要5个或更少元素的重载。性能也在EnumSet.of(E first, E... rest)

的javadoc中提到
  

此工厂的参数列表使用varargs功能,可能是   用于创建最初包含任意数量的枚举集   元素,但它可能比过载运行慢   不要使用varargs。

至于为什么要使用of(E first, E... rest),即使有单独的of(E e1, E e2)

这只是实现的便利。如果声明first参数,则可以使用它而无需检查数组的长度或必须使用索引。例如,您可以使用它来检查其类型(这在使用泛型时通常很重要。)

它实际上并不强制传递至少一个参数,因为如果只有vararg参数,你可以像传递空数组一样轻松传递null

Vararg vs array

如果数组的类型不是基本类型,则没有真正的区别,除了数组参数强制您显式创建数组,而vararg参数允许您传递数组或只列出元素。 / p>

尽管数组参数有历史原因(varargs只加入Java 5.0),但仍然需要使用数组:

  1. 如果数组参数是“传出”参数,则意味着接收数组的方法会填充数组。示例:InputStream.read(byte[] b)
  2. 如果有其他参数,可能是多个数组,显然vararg是不够的,因为只能有一个vararg参数(也必须在最后)。
  3. 可重复参数

    在这种情况下,

    Iterable是将多个值传递给方法的替代方法,但它不是数组和vararg参数的替代。 Iterable用于集合,因为如果参数为Iterable(数组未实现Iterable),则无法使用数组或列出元素。如果调用者具有集合形式的输入数据(例如ListSet),Iterable参数是最方便,最通用和最有效的方法元素。


    得出结论

    回到原来的场景A和B.由于两个场景都包含带Iterable参数的方法,并且Iterable s不与数组“混合”,我通过省略来减少问题那些:

    情景A :(一种方法)

    1. void add(T... valueArr)
    2. 情景B :(两种方法)

      1. void add(T value, T... moreValueArr)
      2. void add(T[] valueArr)
      3. 由于方法add()只从数组中读取(而不是写入数据;假设来自名称add),因此每个用例都可以用它们来解决。为简单起见,我会选择Scenario A

答案 1 :(得分:3)

您提供的选项A和B没有特别的理由是相互排斥的,也不是唯一可用的选项。相反,要认识到varargs 解决了与集合不同的问题

Guava(和现代Java最佳实践)强烈建议使用集合而不是数组。它们比数组更具可扩展性和可互换性,并提供更强大的抽象,例如延迟迭代。

另一方面,varargs为调用方法提供了一种很好的方法。如果您希望人们想要直接传递参数,而不是作为集合的一部分,那么varargs会更方便。几乎所有的varargs样式方法都应该简单地调用你的集合等效方法,然后尽可能少地做,因为只要你必须真正使用数组,这种便利就会变得很不方便。创建直接将数组作为参数的新方法没有什么价值。

但为什么奇怪的vararg签名?

您想要或需要签名类型的一些原因,您会看到Guava使用很多:

  1. 执行更严格的签名:

    您可以使用任意数量的参数调用vararg方法,包括零。如果您的方法在没有任何参数的情况下没有任何意义,void add(T one, T ... rest)的签名会在类型级别强制执行该要求。

  2. 为避免泛色冲突:

    有时您会想要定义一个varargs方法,由于type erasure,该方法与另一种方法相同。例如,如果您使用足够通用的类型定义void add(T ... arr)以及void add(Iterable<T> iter),那么传递iterable可能会实际匹配varargs方法。使用void add(T one, T ... arr)有助于使编译器保持这些方法的不同。 (我将尝试找到这个问题的更具体的例子。)

  3. 避免创建对象:

    有时您会看到方法签名似乎可能是varargs,但不是,例如ImmutableList.of()的重载。这些方法的目的是避免在varargs调用中必须在幕后发生的额外数组分配。这实际上更像是泄漏抽象的情况,而且在功能上可以安全地忽略。除非您正在实施一种可以像Guava实用程序那样经常调用的方法,否则分配节省可能不值得增加代码复杂性。

  4. 总之,坚持使用IterableIterator或适当的Collection子接口编写方法。如果您预计您的方法的用户想要传递单个值,请考虑添加将内部参数包装到集合中的varargs方法,以方便调用者。

答案 2 :(得分:2)

我会稍微修改一下b:

void add (T first, T ... more){
   // call the second version
   add(Lists.asList(first, more);
}
void add(Iterable<? extends T> data){
   ... // do stuff here
}

您不应该提供数组方法。阵列已经过时,维护噩梦。如果您的库的客户端实际上有一个数组,他们仍然可以将它传递给包含在Arrays.asList(...)

中的第二个方法

如果您更像是一个控制狂,您可以在第一种方法中进行额外的检查:

void add (T first, T ... more){
   // call the second version
   add(more.length == 0 
             ? Collections.singleton(first)
             : Lists.asList(first, more));
}

虽然我怀疑它会比使用Lists.asList

更有效率

答案 3 :(得分:0)

我会选择A,并鼓励用户(在方法文档中)将数组传递给您的方法而不是varargs,因为每次调用varargs方法时都必须创建anonnymous数组,如here所述。

您还可以使用wildcard改进add(Iterable<T> data)方法,并将其更改为add(Iterable<? extends T> data)

编辑:(回复@ Torben的评论)

我们假设您拨打add(a,b,c) lot ,如果每次都创建匿名数组,如上面link所述,那么你将浪费时间在heap上分配内存以及让垃圾收集器忙碌。

另一方面,我不确定正在做多少优化,所以它不可能那么可怕。但是,您可以通过创建自己的1数组并继续使用它来确保不会创建任何数组:

YourType[] arr3 = new YourType[3]; //declared as field
...
arr3[0] = a;
arr3[1] = b;
arr3[2] = c;
add(arr3);

但这不是线程安全的,所以在多线程环境中,确保每个线程使用不同的数组。

答案 4 :(得分:0)

明显的区别在于,如果你定义一个方法

void a(int[] args) { }
void b(int ... args) { }

你不能打电话

a(1, 2, 3);

但你可以打电话

b(new int[] { 1, 2, 3 });

如果你想在数组之后有参数,那么实际上只需要数组类型的参数。

a(int[] args, int arg);
a(int ... args, int arg); // Won't even compile.

另一个区别是,如果你的方法需要至少一个参数,那么你必须进行参数长度检查,除非你像这样定义你的方法

c(int arg, int ... args) { }

你在代码中做了一个折衷,因为你必须以不同的方式处理第一个参数,但IMO更严格的API定义是值得的。