【问题标题】:Does the best practice of 'programming to interfaces' apply to local variables?“编程接口”的最佳实践是否适用于局部变量?
【发布时间】:2012-05-24 13:25:33
【问题描述】:

有人告诉我,对局部变量的接口进行编程是没有用的,不应该这样做,因为它只会损害性能并且没有任何好处。

public void foo() {
    ArrayList<Integer> numbers = new ArrayList<Integer>();
    // do list-y stuff with numbers
}

而不是

public void foo() {
    List<Integer> numbers = new ArrayList<Integer>();
    // do list-y stuff with numbers
}

我觉得性能损失可以忽略不计,但不可否认,使用 ArrayList 的 List 语义并没有太多好处。是否有充分的理由采取一种方式或另一种方式?

【问题讨论】:

  • 谁告诉你这会“影响性能”?您可能应该考虑在将来忽略他/她的建议。

标签: java generics collections interface


【解决方案1】:

以这个类为例:

public class Tmp {
    public static void main(String[] args) {
        ArrayList<Integer> numbers = new ArrayList<Integer>();
        numbers.add(1);
    }
}

编译成这样:

Compiled from "Tmp.java"
public class Tmp extends java.lang.Object{
public Tmp();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   new #2; //class java/util/ArrayList
   3:   dup
   4:   invokespecial   #3; //Method java/util/ArrayList."<init>":()V
   7:   astore_1
   8:   aload_1
   9:   iconst_1
   10:  invokestatic    #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   13:  invokevirtual   #5; //Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
   16:  pop
   17:  return

}

上课时:

public class Tmp {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<Integer>();
        numbers.add(1);
    }
}

编译成这样:

Compiled from "Tmp.java"
public class Tmp extends java.lang.Object{
public Tmp();
  Code:
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return

public static void main(java.lang.String[]);
  Code:
   0:   new #2; //class java/util/ArrayList
   3:   dup
   4:   invokespecial   #3; //Method java/util/ArrayList."<init>":()V
   7:   astore_1
   8:   aload_1
   9:   iconst_1
   10:  invokestatic    #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   13:  invokeinterface #5,  2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
   18:  pop
   19:  return

}

您会看到唯一的区别是第一个(使用ArrayList)调用invokevirtual,而另一个(使用List 使用)invokeinterfaceinvokeinterface 实际上比invokevirtual 慢一点(根据this guy 大约38%)。这是apparently,因为 JVM 在搜索具体类的虚拟方法表与接口的方法表时可以进行优化。所以你说的其实是真的。接口调用比具体的类调用慢。

但是,您必须考虑我们所说的速度类型。对于 100,000,000 次调用,实际差异为 0.03 秒。因此,您必须进行 次调用才能真正显着降低速度。

另一方面,正如@ChinBoon 指出的那样,对接口进行编码使那些使用您的代码的人变得更加容易,尤其是当您的代码返回某种List 时。所以在广大的大多数情况下,编码的容易程度超过了相对的性能开销。


在阅读 @MattQuigley 的评论并考虑开车回家后添加

所有这一切的意思是,这不是您应该过分担心的事情。任何性能提升或损失都可能非常很小。

请记住,将接口用于返回类型和方法参数是一个非常好的主意。这使您和任何使用您的代码的人都可以使用最适合他们需要的List 的任何实现。我的示例还表明,如果您碰巧使用了一个在 99% 的情况下返回 List 的方法,那么最好将其转换为具体的类来提高性能。施放所需的时间可能会超过性能上的收益。

话虽如此,这个例子还表明,对于 local 变量,使用具体类而不是接口确实更好。如果您使用的唯一方法是List 上的方法,那么您可以切换出没有副作用的实现类。另外,如果需要,您将可以访问特定于实现的方法。此外,性能略有提升。

tl;dr

始终使用接口作为方法的返回类型和参数。对局部变量使用具体类是个好主意。如果您使用的唯一方法是在接口上,它会带来较小的性能提升,并且切换实现不会产生任何成本。最后,你不应该太担心它。 (返回类型和参数除外。这很重要。)

【讨论】:

  • 是的,基准测试有点幼稚。然而,invokeinterface 的相对缓慢是有据可查的。在虚拟机预热后,这种情况的真实程度可能会有所不同,但它仍然是真实的。
  • 看来你是对的。即使在热身后我也可以复制。您对此有什么好的解释吗?
  • @aioobe 是的,我在修改答案时添加了一些关于它的内容。我在this SO answer 中找到了解释。不完全有名气,但它是一个的解释(听起来很合理)。
  • @TimPote 我只是不同意将其声明为列表会使任何事情变得更容易。没有什么比这更容易了。为什么说“容易”?有人需要 LinkedList 来快速删除,有人需要 ArrayList 来快速随机访问。 List 声明掩盖了这一事实,使事情变得更加困难。考虑到它是一个局部变量,在以后的某个时间点将 ArrayList 更改为 LinkedList 很容易。
  • @MattQuigley 提出这个问题的前提是您是否不需要特定的具体类。如果您需要特定于类 X 的方法,那么您必须使用类 X。但是,如果您只需要一个通用列表,而不是特定类型的列表,那么将其声明为列表没有任何缺点。 (嗯,除了这个答案讨论的可忽略不计的性能下降之外。)
【解决方案2】:

“接口编程”的最佳实践是否适用于局部变量?

即使此类变量的范围小于成员变量的范围,大多数the benefits 在涉及成员变量时针对接口进行编码也适用于局部变量。

除非您需要 ArrayList 特定方法,否则请使用 List。

我觉得性能损失可以忽略不计,但诚然,使用 ArrayList 的 List 语义并没有太多好处

我从未听说过调用接口方法会比调用类类型的方法慢。

使用ArrayList 而不是List 作为静态类型对我来说听起来像是过早的优化。像往常一样,在对整个程序进行概要分析之前,优先考虑可读性和可维护性而不是优化。

【讨论】:

  • 如果您需要特定于数组列表的方法,您可以从列表中向下转换。
  • @aioobe 你说过它更好,因为它更灵活,但没有解释为什么使用 List 而不是 ArrayList 更灵活。介意解释吗?请记住,我们在这里只讨论局部变量。
  • @MattQuigley,在这个特定问题中,重点是局部变量。为什么在一般情况下针对接口进行编码是一个好主意,已经在其他地方得到了非常详细的回答。我鼓励您阅读例如this questionthis one
  • @aioobe 我想你误会了。我知道为什么通常针对接口进行编码是个好主意。我要说的是,您在回答中写道,出于同样的原因,它作为局部变量也更好,但没有解释原因。我相信人们认为不理解为什么会更好,只是因为它是在其他地方完成的。一个人并没有保护自己免受将来更改实现的影响,因为它是一个局部变量并且很容易更改。我看不到“更灵活”的论点,因为它是一个局部变量。而且它不能被换成测试——它是本地的!
  • @MattQuigley 在我看来,始终对最通用的抽象进行编程是有好处的,即使在使用局部变量时也是如此。 List 接口比 ArrayList 接口简单,如果我想要做的事情只需要 List 语义,我不需要针对 ArrayList 接口进行编程,该接口具有与我想要做的事情无关的额外方法。在编程时不必看到一堆不必要的方法的改进的开发人员体验对我来说几乎可以忽略不计的性能损失。
【解决方案3】:

TLDR:方法调用在接口/抽象类上较慢,并且在稍后的时间点更改局部变量的实现几乎没有障碍使大多数论点成为争论点。

这里有很多答案都是一味的说将局部变量声明为接口而不理解为什么。首先,方法调用慢 - 大约 3 倍,我记得在测试中。第一个代码块中的add 方法比第二个慢3 倍。您可以通过一个简单的测试自行测试,该测试测量 for 循环中经过的时间。

List<Integer> numbers = new ArrayList<Integer>();
numbers.add(1); // slower

ArrayList<Integer> numbers = new ArrayList<Integer>();
numbers.add(1); // faster

其次,问题是关于局部变量,而不是一般接口的面向对象设计原则。公共类方法将返回接口或抽象类 (List) 而不是实现 (ArrayList) 有很好的 OO 原因。您可以在其他地方阅读有关此类原因的信息,但这个问题不是与此有关 - 它与局部变量有关。 99.999% 的情况下,您永远不会将该局部变量的实现从 ArrayList 更改为 Vector。但是,在实际执行的 0.001% 次中,这样做没有任何障碍 - 您只需将单词 Vector 复制并粘贴到单词 ArrayList 上即可完毕。每个人似乎都认为这样的改变很难。不是。

第三,LinkedListArrayList 不同。您选择其中一个是有原因的(访问/追加/插入/删除所需的时间)。将其声明为ArrayList 是一种肯定,它让您知道此局部变量用于快速随机访问。过早优化的论点是愚蠢的 - 一旦实例化,它就会以一种或另一种方式进行优化。

【讨论】:

【解决方案4】:

使用可以让您完成工作的最抽象的类型。如果你只需要List,那就用它。如果您需要将类型限制为更具体的类型,请改用它。例如,在某些情况下使用List 会过于具体,而使用Collection 将是首选。再举一个例子,在某些情况下,有人可能需要ConcurrentMap 而不是常规的Map

【讨论】:

    【解决方案5】:

    特别是如果该方法返回列表,如果有一天您不得不将实现更改为链表,那么对接口进行编码会对您有所帮助。这是一个好处,您无需更改方法即可返回链表或数组列表。

    【讨论】:

    • 我不确定我是否得到了这个答案。 OP 询问的是局部变量的类型,而不是方法的返回类型。 (他的示例甚至返回 void。)即使将方法声明为返回 List&lt;Integer&gt;,您也可以很好地返回 ArrayList&lt;Integer&gt; 作为静态类型的变量。
    • @aioobe 所有的答案都已经呈现出来了,我决定展开。
    【解决方案6】:

    这里有一个建议:将 List 声明为 final,但带有接口。

    final List<Integer> numbers = new ArrayList<Integer>();
    

    如果我不得不猜测(而且我不是 java 编译器本身的专家),但是将变量设为 final,编译器应该能够删除 invokeinterface 以支持 invokevirtual,因为它知道类型一旦声明变量就不会改变。

    【讨论】:

    • 不幸的是,局部变量的 final 关键字对性能没有任何好处。局部变量的 final 没有给编译器使用任何东西。唯一的好处是设计语义。
    【解决方案7】:

    是的@Matthew,使用

    的最佳实践要好得多
    List<Integer> numbers = new ArrayList<Integer>
    

    因为你可以使用 ArrayList 类和 List 类的方法。希望能帮到你。

    【讨论】:

    • 因为你可以使用 ArrayList 类和 List 类的方法 -- 不,(A)你不能例如numbers.ensureCapacity(10)(B)List不是一个类。
    • 好的,Java 是所有类的。谢谢。
    • ((ArrayList)numbers).ensureCapacity(10)
    • @hekomobile 我认为 aioobe 的意思是 List 是一个接口。
    • @hekomobile 您的回答具有误导性。如果 numbers 也被声明为 ArrayList,您也可以使用 ArrayList 和 List 中的方法。这就是 OO(面向对象)设计 101。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-08
    • 2017-10-30
    • 1970-01-01
    • 2011-01-21
    • 2019-12-19
    • 1970-01-01
    相关资源
    最近更新 更多