8

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

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 语义并没有什么好处。是否有充分的理由采取一种方式或另一种方式?

4

7 回答 7

14

以这个类为例:

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

}

您会看到唯一的区别是第一个(使用 an ArrayList)调用invokevirtual和另一个(使用Listuses)invokeinterfaceinvokeinterface实际上比头发慢invokevirtual(根据这个家伙的说法约为 38% )。这显然是由于 JVM 在搜索具体类的虚拟方法表而不是接口的方法表时可以进行的优化。所以你说的其实是真的。接口调用具体类调用慢。

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

另一方面,正如@ChinBoon 指出的那样,对接口进行编码使使用您的代码的人变得更加容易,特别是如果您的代码返回某种 sort List。因此,在绝大多数情况下,编码的便利性远远超过了相对的性能开销。


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

这一切意味着这不是你应该过分担心的事情。任何性能增益或损失都可能非常小。

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

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

tl;博士

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

于 2012-05-16T22:41:17.093 回答
12

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

尽管此类变量的范围小于成员变量的范围,但针对成员变量进行接口编码的大部分 好处也适用于局部变量。

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

我觉得性能影响可以忽略不计,但不可否认,使用 ArrayList 的 List 语义并没有什么好处

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

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

于 2012-05-16T22:40:08.490 回答
5

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% 的情况下,您永远不会将该局部变量的实现从 a 更改ArrayList为 a Vector。但是,在您实际执行的 0.001% 次中,这样做没有任何障碍 - 您只需将单词Vector复制并粘贴到单词ArrayList上,就完成了。每个人似乎都认为这样的改变很难。它不是。

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

于 2012-05-16T23:44:58.893 回答
3

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

于 2012-05-16T22:41:03.123 回答
0

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

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

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

于 2012-05-17T02:39:15.007 回答
0

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

于 2012-05-16T22:43:00.323 回答
-1

是的@Matthew,使用的最佳实践要好得多

List<Integer> numbers = new ArrayList<Integer>

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

于 2012-05-16T22:49:49.913 回答