【问题标题】:Java method params: var args vs arrayJava 方法参数:var args 与数组
【发布时间】:2014-08-09 10:57:32
【问题描述】:

在整个 Google Guava 库中,我注意到使用“一个(或两个)加 var args”技术的趋势。

例子:

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

我花了一段时间才弄清楚原因:为了防止调用零参数(在第一种情况下)或一个参数(在第二种情况下)。

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

场景A:(两种方法)

  1. void add(T... valueArr)
  2. void add(Iterable<? extends T> iterable)

场景B:(三种方法)

  1. void add(T value, T... moreValueArr)
  2. void add(T[] valueArr)
  3. void add(Iterable<? extends T> iterable)

B 可能更好的一个想法:我注意到许多 Java 程序员不知道数组可以直接作为 var args 传递。因此,B 可能会提示什么是可能的。

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

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

【问题讨论】:

  • void add(T... valueArr) 和 void add(T[] valueArr) 相同,但您需要手动构造数组以将其以第二种形式传递。
  • void add(T... valueArr) 表示零个和多个 T 类型的对象是可以接受的,而void add(T value, T... moreValueArr) 表示一个和多个。现在选择取决于程序和要求。
  • void add(Iterable<T> iterable) 有所不同,在 Collection 对象的情况下更可取
  • @user3218114 - 答案胜过 3 cmets :)
  • @TheLostMind OK 刚刚删除。

标签: java arrays guava variadic-functions


【解决方案1】:

最后会得出结论。如果您只想得到结论,请跳到最后。


主要目标是性能。

如果有很多用例只有 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中也提到了性能

这个工厂,它的参数列表使用可变参数特性,可能是 用于创建最初包含任意数量的枚举集 元素,但它的运行速度可能比重载慢 不要使用可变参数。

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

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

实际上并不会强制传递至少一个参数,因为您可以像传递一个空数组一样轻松地传递 null,如果只有可变参数参数。

可变参数与数组

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

尽管数组参数的历史原因(varargs 仅在 Java 5.0 中加入)仍然存在必须使用数组的地方:

  1. 如果数组参数是“传出”参数,则表示接收数组的方法希望填充数组。示例:InputStream.read(byte[] b)
  2. 如果还有其他参数,可能是多个数组,显然 vararg 是不够的,因为只能有一个 vararg 参数(也必须在末尾)。

可迭代参数

Iterable 在这种情况下是向方法传递多个值的替代方法,但它不能替代数组和可变参数参数。 Iterable 用于集合,如果参数为Iterable(数组不实现Iterable),则不能使用数组或列出元素。如果调用者有集合形式的输入数据(例如List或Set),则Iterable参数是最方便、最通用、最有效的元素传递方式。


得出结论

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

场景A:(一种方法)

  1. void add(T... valueArr)

场景B:(两种方法)

  1. void add(T value, T... moreValueArr)
  2. void add(T[] valueArr)

由于方法add() 只从数组中读取(而不是写入;从名称add 假定),因此每个用例都可以用它们来解决。为简单起见,我会选择场景 A。

【讨论】:

  • 为什么这被否决了?这是一个详细而深思熟虑的答案。
  • 我不知道。我尊重正确的反对意见,但我想知道原因,以便从中吸取教训并改进我的答案。
  • 我没有投反对票,但您的论点是“它并没有真正强制传递至少一个参数,因为您可以轻松传递null”不准确。 method() 和 method(null) 是两个不同的调用,后者有一个参数。 of(E first, E... rest) 模式确实强制执行最小参数计数。空值检查是一个单独的问题。
  • @icza:这里有很多很棒的想法/要点。你能得出关于 A 与 B 的结论吗?
  • 编辑了我的答案以添加结论。
【解决方案2】:

您提供的 A 和 B 选项没有特别的原因,它们相互排斥,也没有唯一的可用选项。相反,请认识到可变参数解决的问题与集合不同。

Guava(以及现代 Java 最佳实践)强烈鼓励在数组上使用集合。它们比数组更具可扩展性和可互换性,并提供更强大的抽象,例如惰性迭代。

另一方面,可变参数提供了一种调用方法的好方法。如果您希望人们希望直接传递参数,而不是作为集合的一部分,可变参数更方便。几乎所有 varargs 样式的方法都应该简单地调用您的集合等效方法,并且尽可能少做其他事情,因为一旦您必须实际使用数组,这种便利就会变得不方便。创建直接将数组作为参数的新方法几乎没有价值。

但为什么会有奇怪的可变参数签名?

您想要或需要您看到 Guava 经常使用的签名类型有几个原因:

  1. 执行更严格的签名:

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

  2. 为了避免泛型冲突:

    有时您会想要定义一个可变参数方法,由于type erasure,它与另一个方法相同。例如,如果您使用足够通用的类型定义了void add(T ... arr) 和void add(Iterable&lt;T&gt; iter),则可能传递一个可迭代对象实际上会匹配可变参数方法。使用void add(T one, T ... arr) 有助于使这些方法对编译器保持不同。 (我会尝试找到一个更具体的例子来说明这个问题)。

  3. 为了避免创建对象:

    有时您会看到方法签名看起来像是可变参数,但实际上并非如此,例如 ImmutableList.of() 的重载。这些方法的目的是避免在可变参数调用中必须在幕后进行的额外数组分配。这实际上更像是泄漏抽象的情况,而不是其他任何事情,并且在功能上可以安全地忽略它。除非您正在实现一个可以期望像 Guava 实用程序那样经常被调用的方法,否则节省的分配可能不值得增加代码复杂性。

总之,坚持编写采用Iterable、Iterator 或适当的Collection 子接口的方法。如果您预计您的方法的用户希望传入单个值,请考虑添加将参数包装到内部集合中的可变参数方法,以方便调用者。

【讨论】:

    【解决方案3】:

    我会选择稍微修改过的 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 更有效

    【讨论】:

    • 查看@kajacx 的建议。至少,我应该将? extends 添加到我的Iterable 类型中。
    • @kevinarpe extends 已授予并添加。其余的都是废话。有一种添加零个或多个值的方法没有意义,零个元素是无操作的,不应该被允许。数组是过去的遗物,也应该避免使用(参见 Joshua Bloch 的 Effective Java 2nd Edition,第 25 条:Prefer Lists to Arrays)。
    • 虽然在大多数情况下都是合理的,但我会犹豫是否像总是添加 ? extends 那样做一个空白声明。这会改变您正在使用的类型,有时会以意想不到的方式。更喜欢限制您的类型。如果您发现有需要,您始终可以从 &lt;T&gt; 扩展到 &lt;? extends T&gt;,但如果您意识到您的用例不支持 ? extends,您将破坏向后兼容性。
    • @dimo414:有趣的一点。你能提供一个? extends 不起作用的例子吗?
    • @kevinarpe 好吧,一个简单的例子就是你应该使用? super 的任何情况。一般来说,? extends 是 可能会做你想做的事,但这是一个普遍合理的决定,与将其作为所有通用方法签名的规则相去甚远。谨慎起见,只声明你知道你需要的类型;你以后可以随时扩展你的类型签名。
    【解决方案4】:

    明显的区别是,如果你定义了一个方法

    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 定义是值得的。

    【讨论】:

      【解决方案5】:

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

      您还可以使用wildcard 改进您的add(Iterable&lt;T&gt; data) 方法,并将其签名更改为add(Iterable&lt;? extends T&gt; 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);
      

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

      【讨论】:

      • 这没有意义。如果该方法需要任意数量的参数,则必须使用数组或列表。隐式和显式数组创建之间没有性能差异(除了编译器可以更有效地优化隐式数组创建)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-10-21
      • 1970-01-01
      • 2011-07-21
      • 2014-10-24
      • 2019-07-14
      • 1970-01-01
      • 2010-10-18
      相关资源
      最近更新 更多