在Java中,变量定义应该使用接口还是类型?

18
ArrayList aList = new ArrayList();

List aList = new ArrayList();

这两者有什么区别,使用哪个更好,为什么?


如果有正确答案的话,那就是第一种形式。请参考我下面的答案。 - irreputable
8个回答

22
  • 概述:List是一个接口,而ArrayList是该接口的一种实现。
  • 优势:其次,ArrayList更好,因为这意味着您可以在不需要更改应用程序其他部分的情况下将ArrayList更改为List的另一种实现。您可能希望出于性能原因或选择/将要选择的List实现的其他方面而这样做。

没错。如果你发现以后需要确保你正在使用 ArrayList,那么你可以随时更改声明。否则,你的代码其余部分真的不需要知道正在使用哪种 List,这样你就可以在以后根据性能或其他考虑因素换用不同的实现(例如 LinkedList)。 - Bill Michell
5
@Bill,每当提到实现时大家总是谈到LinkedList,但我认���如果你提到Collections.unmodifiableList()、Collections.checkedList()或Collections.synchronizedList(),情况会更加有力。 - ILMTitan
@ILMTitan 更不用说其他像Google Collections中的Immutable集合了。 - Jorn
简单明了。研究这个主题时,希望早些发现这些话语,因为它们回答了比一千篇臃肿、重复的论文更多的问题。 - Ben

7

自Java 1.5以来,这两个功能都已被弃用。

正确的写法应该是:

List<String> list = new ArrayList<String>();
// or whatever data type you are using in your list

请阅读 Joshua Bloch 的 Effective Java,特别是这两个条目:
  • 23:不要在新代码中使用原始类型(甚至可以在线使用
  • 52:通过它们的接口引用对象
顺便说一句,如果你使用 Guava,你可以使用一个工厂方法来构造 ArrayList,这样你就不必重复类型参数了。
List<String> list = Lists.newArraylist();

我希望每个人现在都知道应该使用List的模板版本(例如,如果您不这样做,eclipse会提醒您)。但是你关于Guava的第二条评论非常有趣! - Roalt
@dertoni 我同意,这是每个Java开发人员都应该阅读的唯一一本书。 - Sean Patrick Floyd
我认为这是每个Java开发人员应该阅读的第一本书籍 ;) - Jorn
"Effective Java" 的链接已经失效。 - JohnK

5
List<T>是一个接口,而ArrayList<T>是一个类,这个类实现了List<T>接口。
我更喜欢第二种形式,它更加通用,即如果你不使用ArrayList<T>特定的方法,你可以将其类型声明为接口List<T>类型。使用第二种形式,更容易将实现从ArrayList<T>更改为其他实现了List<T>接口的类。
编辑:正如许多SO用户评论的那样,两种形式都可以作为任何接受List<T>ArrrayList<T>的方法的参数。但是当我声明方法时,我更喜欢使用接口。
 void showAll(List <String> sl) ...

并使用:

 void showAllAS(ArrayList <String> sl) ...

只有在我的方法使用特定于ArrayList<T>的方法时,例如ensureCapacity()

建议我们使用带类型参数List<T>而不是简单的List是非常好的(当然,如果我们不使用古老的Java)。


1
对我来说,这听起来有点反过来(错误)。第一种形式ArrayList aList可以作为参数传递给任何需要ListArrayList参数的方法。第二种形式List aList只能作为参数传递给需要List参数的方法。虽然变量List aList引用了一个ArrayList,但强类型会防止引用变量aList匹配为ArrayList参数的参数。 - Bert F
2
完全相反;您可以将ArrayList传递给需要List的方法。但是,您无法将List传递给要求ArrayList的方法。 - Ishtar
3
@Ishtar,Bert,没错。但你们忘了提到一个方法几乎不应该期望一个 ArrayList。在99%的情况下,任何 List,在50%的情况下,任何 Collection 都可以。 - Sean Patrick Floyd
+1 我认为Michal在这里有些被误解了。如果创建一个接口,最好使用List,如果没有使用ArrayList特定的功能。这样你就不会对用户施加实现的强制。ArrayList是List的可能实现之一。 - Roalt
1
@Bert F:如果他说“...你可以将List(或ArrayList)传递给操作List参数的方法...”,那么就会少些混淆,对吧?我现在看到这已经不对了。 - Roalt
显示剩余5条评论

2
所有这些答案都是从某个地方抄袭来的教条。
变量声明和初始化语句Type x = new Constructor();明确是实现细节的一部分。它不是公共API的一部分(除非它是public final,但List是可变的,所以这是不合适的)。
作为实现细节,你试图用你的抽象来愚弄谁?最好保持类型尽可能具体。它是一个数组列表还是一个链接列表?应该是线程安全的还是不安全的?对于你的实现来说,这个选择很重要,你要仔细选择特定的列表实现。然后你声明它只是一个普通的List,好像这并不重要,你也不在乎?
唯一合法的将其声明为List的理由是"我太懒了,不想打字"。这也涵盖了这样一个论点:"如果我需要移动到另一个列表实现上,我就少修改了一个地方"。
只有当变量作用域很小且你可以从一个眼神中看到所有的用法时,这个理由才是合法的。否则,保持最具体的类型,使其性能和语义特征在使用变量的所有代码中都得到体现。

我大部分不同意这个观点,但它不应该被评为-3;我投赞成票是因为它提出了合理的论点(即使我不同意结论)。 - sleske
顺便说一句,我不同意的一点是“选择对于你的实现很重要,你要仔细选择特定的列表实现”。如果是这种情况,使用具体类型可能是合理的,但根据我的经验,通常并非如此。例如,在使用集合类时,我几乎总是只使用ArrayList和HashSet,只有在出现问题时才重新考虑(即实际上几乎从未)。在这种情况下,使用List/Set更能表达我的意图,即“我只想要一个列表”。 - sleske
+1;我完全同意。如果范围很小,那么请使用特定类型。我是从您的其他答案中跳转到这里的:https://dev59.com/xmnWa4cB1Zd3GeqP0GMo#12805778 - maba

1
在大多数情况下,我更喜欢第二种方式,因为它表示您没有使用ArrayList API中的任何特定内容,如果以后需要,您可以替换任何其他类型的List而无需更改任何代码,除了第一行。

1
这是Java的一个怪癖,由于类型推断和面向对象编程教义的限制。对于局部变量而言,这主要是风格问题;如果需要子类型的某些特定功能,请使用子类型(如下例),否则可以使用任何一种。Java风格是使用超类型,在实现体内强制执行接口,并与可见类型保持一致(《Effective Java》第二版:条款52:按其接口引用对象)。在具有更多类型推断的语言中,例如C++/C#/Go等,您不需要显式声明类型,局部变量将具有特定类型。对于可见类型(公共或受保护字段或方法的参数和返回值),您几乎总是希望使用最通用类型:接口或抽象类,以提供更好的灵活性(《Effective Java》:条款40:仔细设计方法签名)。但是,对于不可见的类型(私有或包私有成员或局部变量),可以使用任何一种(这只是风格),有时必须使用更具体的类型,包括具体类。请参阅《Effective Java》了解标准指南;以下是个人想法。
即使不可见,使用更通用的类型的原因是为了减少噪音:您声明只需要更通用的类型。在不可见的成员(如私有方法)上使用通用类型还可以减少类型更改的繁琐程度。但是,在本地变量中,这并不适用,因为它只是更改一行代码:ConcreteFoo foo = new ConcreteFoo();OtherConcreteFoo foo = new OtherConcreteFoo();
需要子类型的情况包括:
  • 您需要仅存在于子类型的成员,例如:
    • 实现的某些功能,例如ArrayList<T>ensureCapacity
    • (测试代码中常见)虚拟类的某个成员,例如(假设)FakeFileSystem#createFakeFile
  • 您依赖于子类型的行为,特别是在覆盖超类型方法时,例如:
    • 具有更通用类型的参数,
    • 更具体的返回类型,或者
    • 引发更具体或更少的异常类型。
作为最后一个例子,请参见我应该关闭StringReader吗?StringReader.html#close覆盖了Reader.html#close并且不会抛出IOException,因此使用StringReader代替Reader作为局部变量意味着您不需要处理实际上无法发生的异常,并且显著减少了样板文件。

0
如果您使用的是Java 1.4或更早版本,则建议使用第二个选项。尽可能通用地声明字段总是更好的,以防以后需要将其转换为其他类型,例如Vector等。
自从1.5版本以后,我会选择以下方式。
List<String> x = new ArrayList<String>();

它为您提供一点类型安全性。列表 'x' 只能添加一个字符串对象。当你从列表 'x' 获取一个项目时,可以保证返回的是一个字符串。这也有助于消除不必要的强制转换,当你在6个月后回过头来尝试记住你的代码时,它可以使代码难以阅读。您的编译器/IDE 将帮助您通过显示错误来记住应该添加到列表 'x' 中的类型,如果您尝试添加其他任何对象类型,则会显示错误。

如果要向列表中添加多个对象类型,则可以添加注释以抑制编译错误。

@SuppressWarnings("unchecked")
List y = new ArrayList();

1
这是糟糕的写法。不要禁止警告,使用List<Object>。例如:List<Object> y = new ArrayList<Object>(); y.add(1); y.add("abc"); y.add(true); - Sean Patrick Floyd
我在示例中使用的自动装箱也是不好的风格。 - Sean Patrick Floyd
1
如果您正在使用1.4或更早版本,首先要做的是升级您的JVM。 - Tony Ennis
我不想引发争端,只是想为放弃Java 1.4参考资料提供理由。我相信至少还有其他一位可怜的开发人员在维护旧产品时别无选择,只能使用不是最新工具的版本。不幸的是,我们中的一些人没有其他选择,只能在大型遗留项目上继续使用旧的JVM,因为业务只允许更新自定义代码,而不是支持库。此外,重新测试整个代码库可能会导致使用旧的JVM,这也是正当的理由。 - bakoyaro

0

我知道至少有一种情况下,使用接口声明变量是行不通的。那就是当你想要使用反射时。

我曾经修复过一些代码中的一个错误,其中我将一个变量声明为Map<String, MyObject>并将其分配给HashMap<String, MyObject>的实例。这个变量被用作一个方法调用的参数,该方法通过反射访问。问题在于,反射试图找到一个具有HashMap签名而不是声明的Map签名的方法。由于没有带有HashMap参数的方法,我无法通过反射找到方法。

Map<String, Object> map = new HashMap<String, Object>();

public void test(Map<String, Object> m) {...};

Method m = this.getClass().getMethod("test", new Class<?>[]{map.getClass()});

将无法找到使用接口的方法。如果您创建另一个使用HashMap的测试版本,则它将起作用 - 但现在您被迫使用具体类声明变量,而不是更灵活的接口...


3
程序中的 bug 存在于方法查找代码中。一个高级的查找不要求确切的参数类型匹配,相反,它模拟编译器的算法来寻找合适的方法。 - irreputable
这里的实际问题是你正在使用map.getClass()进行方法查找,它将返回HashMap.class,而你应该使用Map.class。 - Jorn
@John 看起来像是一个小错别字,但实际上不仅仅如此。在这种情况下使用反射的整个目的是我不知道要调用哪个方法。我会传递一个 List<Object> 参数。如果我知道我想要使用带有 Map<> 参数的方法,我就不会使用反射 :) - BigMac66
然而,Java不应被与带有反射的Java混淆。 - Pacerier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接