为什么接口比抽象类更受青睐?

38

我最近参加了一次面试,他们问了我一个问题:"为什么接口比抽象类更受青睐?"

我尝试给出了几个答案,如下:

  • 我们只能获得一个Extends功能
  • 它们是100%抽象的
  • 实现没有硬编码

他们让我拿出你使用的任何JDBC api。 "为什么它们是接口?"

我可以给出更好的答案吗?


答:接口(Interfaces)比抽象类更受青睐,是因为在Java中一个类只能继承自一个父类,但是却可以实现多个接口,这样使得代码具有更高的灵活性和可扩展性。而且接口可以更好地实现松耦合(loose coupling),使得不同的模块之间保持独立性。对于JDBC api,由于它需要支持多个数据库供应商,而这些供应商提供的 JDBC 驱动程序都是通过实现相同的接口来完成的,因此 JDBC API 也被设计成接口。

1
我几乎肯定之前见过这样的问题,但甚至 Google 也找不到。也许我的大脑又在耍把戏了。 - Michael Myers
这是一个有争议的问题,因为它假设了一个立场,并没有给出任何可能有效的上下文。我同意devinb的观点。它们都是工具-适当使用它们。这里有太多的答案在为问题辩护...如果你真的想要这份工作,这可能是可以接受的。 - Robin
3
不要用回答来证明问题的合理性。这不是他们(应该)寻找的。展示你知道自己在说什么,能够胜任这份工作。如果他们值得为之工作,他们不会寻找一个鹦鹉学舌者。 - Adam Jaskiewicz
开始编写测试用例或者采用TDD路线,你会发现使用接口编写测试用例会更加容易。它允许你将无关的内容模拟出来,专注于测试用例本身。 - Lieven Keersmaekers
可能是[接口与抽象类(通用OO)]的重复问题(https://dev59.com/D3RA5IYBdhLWcg3w_C8w)。 - RAS
显示剩余3条评论
23个回答

55

那道面试题反映了提问者的某种信念。我认为他们是错误的,因此你可以选择以下两个方向之一。

  1. 给他们想要的答案。
  2. 尊重地表示不同意。

他们想要的答案,其他帖子已经很好地强调了那些要点:多接口继承,继承会强制类做出实现选择,接口更易于更改。

然而,如果你在不同意中提出令人信服且正确的论点,面试官可能会注意到。首先,强调接口的优点是必须的。其次,我会说在许多情况下接口更好,但它们也会导致代码重复,这是一个负面因素。如果你有大量的子类将执行大部分相同的实现以及额外的功能,则可能需要使用抽象类。它允许你拥有许多细节相似的对象,而仅使用接口时,你必须具有几个几乎重复的独特对象。

接口有许多用途,并且有令人信服的理由认为它们“更好”。然而,你应该始终使用适合工作的正确工具,这意味着你不能忽视抽象类。


3
如果您不同意但保持尊重,我认为您几乎肯定会失去获得这份工作的机会。 - Tom Hawtin - tackline
30
如果面试官思想保守,不愿听取不同观点,那么我就不想要这份工作。只要你尊重他人(并理解上下级关系),你应该能够提出合理、深思熟虑的意见,而不会因此受到惩罚。 - DevinB
4
@Tom,根据你的表现方式,它不应该让你失去工作机会。另一方面,如果确实这样,那么也许你并不真的想在那里工作 :-) - TofuBeer
8
这个问题并不意味着面试官真正的信仰或立场。他们可能有不同的想法,但是想知道你是否赞同他们的观点或者是否有勇气质疑这个问题。在竞争激烈的职位招聘中,回答这类含蓄问题很常见。 - Mike B
1
接口不会导致代码重复,如果您有两个具有相同功能的对象,那么它们意味着相同的API和相同的接口。抽象类是实现细节,而接口是您向使用您的API的程序员公开的通用契约。 - Christopher Perry
显示剩余2条评论

26

通常而言,这并不是一种应该盲目遵循的“规则”,但最灵活的安排方式是:

interface
   abstract class
       concrete class 1       
       concrete class 2

接口存在的原因有以下几点:

  • 已经继承某个类的现有类可以实现该接口(假设您对现有类的代码具有控制权)
  • 现有的类可以被子类化,子类可以实现该接口(假设现有类是可被子类化的)

这意味着您可以使用预先存在的类(或者必须继承自其他类的类)并使它们与您的代码配合使用。

抽象类提供了所有具体类的公共部分。在编写新类或修改要扩展它的类(假设它们继承自 java.lang.Object)时,需要从抽象类继承。

您应该始终(除非有非常好的理由不这样做)将变量(实例、类、本地和方法参数)声明为接口类型。


1
非常好的表述。列表和集合接口以及类中有很多这样的示例。 - Robin
这是一个非常强大和灵活的模式。我在框架中经常看到它被使用。抽象类有效地提供了接口的“骨架”实现。 - Andrew Fielden
值得注意的是,(1)任何继承自抽象类的具体类都不能再继承其他类——这个限制可能看起来不太可能成为问题,但实际上却可能会成为问题;直接实现接口的能力提供了一个“安全阀”,以防出现类继承会有问题的情况;(2)直接实现接口的类如果接口发生变化,则必须进行更改;如果基类可以提供新方法的默认实现,那么从它派生的类可能就不需要进行更改。 - supercat
如果最终得到一个接口,直接由三个类(包括抽象基类)实现,并且该基类被十几个其他类继承,添加中间层可能意味着更改接口需要修复三个类,而不是十四个类。这是一个相当大的胜利。值得注意的是,变量和存储位置等内容应声明为接口类型,而不是抽象类型,以便实现接口的类可以与继承抽象基类的类进行交互。 - supercat

23

继承只有一次机会。如果你创建了一个抽象类而不是接口,那么继承你的类的人不能同时继承另一个抽象类。


嗨,没错。但他仍然可以继承一个接口,对吧? - Shuo
Techmaddy已经在问题中指出了这一点:“我们只能获得一个Extends功能”。 - Jeff Axelrod

10

你可以实现多个接口,但只能继承一个类。


我给出了这个答案,但他们期望得到其他的回答。 - Techmaddy
1
在那种情况下,如果他们没有给你一个offer,我也不会太失望的 ;) - Rowland Shaw
这就像我之前给出的第一个答案,还有一些其他的,正如我在描述中指定的那样。我担心是否还有其他我错过的东西。 - Techmaddy
也许他们正在测试你的自信心 ;-) - Ryan Graham

9

抽象类

1.不能独立于其派生类实例化。抽象类构造函数仅由其派生类调用。

2.定义了基类必须实现的抽象成员签名。

3.比接口更具可扩展性,且不会破坏任何版本兼容性。使用抽象类,可以添加所有派生类都可以继承的附加非抽象成员。

4.可以包含存储在字段中的数据。

5.允许有(虚拟的)成员具有实现,并因此为派生类提供成员的默认实现。

6.从抽象类派生将使用子类唯一的基类选项。

接口

1.不能被实例化。

2.接口的所有成员实现都在基类中进行。在实现类中只能实现所有成员,而不能仅实现某些成员。

3.扩展接口以添加其他成员会破坏版本兼容性。

4.不能存储任何数据。字段只能在派生类中指定。解决此问题的方法是定义属性,但没有实现。

5.所有成员自动为虚拟的,不能包含任何实现。

6.虽然不能出现默认实现,但实现接口的类仍然可以相互派生。


7

正如devinb和其他人提到的那样,面试官不接受你的有效答案表明他们的无知。

然而,提到JDBC可能是一个提示。在这种情况下,也许他们正在询问使用接口而不是类编写客户端代码的好处。

因此,与类设计有关的完全有效的答案,例如"您只能使用一次继承",可能不是他们要寻找的答案,他们可能正在寻找更像"将客户端与特定实现分离"的答案。


5
抽象类有一些潜在的缺陷。例如,如果你重写一个方法,除非你明确调用,否则不会调用super()方法。这可能会导致覆盖类的问题。此外,在继承时使用equals()可能存在潜在问题。
使用接口可以鼓励在想要共享实现时使用组合。组合往往是复用他人对象的更好方式,因为它更加灵活。继承很容易被过度使用或用于错误的目的。
定义接口是一种非常安全的方式来定义对象应该如何行动,而不会冒着扩展另一个类(抽象或非抽象)可能出现的脆弱性的风险。
此外,正如您所提到的,你一次只能扩展一个类,但是你可以实现任意多个接口。

4

当你需要继承实现时,使用抽象类;当你需要继承规范时,使用接口。JDBC标准规定:“连接必须要做这个”。那就是规范。


3

对于以上大部分帖子,我持有不同意见(抱歉!如果你想的话可以将我降级:-))


首先,“只有一个超类”的答案是无聊的。在面试中给我这个答案的人很快就会被反驳:“C++存在于Java之前,而且C++有多个超类。为什么你认为James Gosling只允许Java有一个超类?”

理解你回答背后的哲学,否则你就完蛋了(至少如果我面试你的话)。


其次,接口比抽象类有多种优势,特别是在设计接口时。最大的优势是不会对方法调用者强加特定的类结构。没有什么比尝试使用要求特定类结构的方法调用更糟糕的了。它很痛苦和尴尬。使用接口,任何东西都可以传递给方法,期望值最小。

例如:

public void foo(Hashtable bar);

对比。

public void foo(Map bar);

对于前者,调用者将始终采取其现有数据结构并将其插入到新的Hashtable中。
第三,接口允许具体类实现者中的公共方法变为“私有”。如果该方法未在接口中声明,则不能使用(或误用)该方法的类。这就带来了第四点……
第四,接口代表实现类和调用者之间的最小契约。这个最小契约确切地指定了具体的实现者期望如何被使用,而不再多余。调用类不允许使用接口“契约”未指定的任何其他方法。所使用的接口名称也影响开发人员对他们应该如何使用对象的期望。如果开发人员传递一个
public interface FragmentVisitor {
    public void visit(Node node);
}

开发人员知道他们唯一可以调用的方法是 visit 方法。他们不会被具体类中那些不应该乱动的亮闪闪的方法所分心。
最后,抽象类有许多方法,这些方法实际上只存在于子类中供其使用。因此,抽象类对于外部开发人员来说有点像一团乱麻,没有指导哪些方法是打算供外部代码使用的。
当然,有些方法可以设置为 protected。然而,可悲的是,protected 方法也对同一包中的其他类可见。如果抽象类的方法实现了接口,则该方法必须是公共的。
然而,使用接口可以将所有这些内部细节安全地隐藏起来,既不会看到抽象超类或具体类中的内部细节。
当然,我知道开发人员可能会使用一些“特殊”知识将对象强制转换为另一个更广泛的接口或具体类本身。但是这样的转换违反了预期的合同,开发人员应该被用鲑鱼拍打。

你会反问的问题比原来的更好,我个人认为,尽管有些人可能会用“许多最近的语言和框架”而不是特别提到Java的方式来表达。虽然.net从Java借鉴了一些相当愚蠢的东西(例如,浮点数应该隐式转换为双精度浮点数,但反之则不然,而浮点数到双精度浮点数的转换更容易出错),但我不认为.net之所以具有单继承性是因为Java具有这种特性。继承对于代码/数据共享和可替代性都非常有用。代码/数据共享通常更方便... - supercat
与组合相比,继承更容易实现,但两种方式都可以实现。允许一种包括代码/数据共享的多重继承形式会引入困难(例如“菱形问题”)。由于代码/数据的多重继承不允许通过组合无法实现的很多功能,但是能够替代多个不相关的事物非常有用,因此提供类似继承的东西,但仅限于后者的功能是有意义的。 - supercat
关于浮点数和双精度浮点数,将两个值为1E38的双精度浮点数相乘,然后将结果转换为浮点数,会得到一个“数字太大无法转换为浮点数”的答案,这是正确的。将两个值为1E38的浮点数相乘,然后将结果转换为双精度浮点数,会得到一个“答案太大无法转换为双精度浮点数”的答案,这是不正确的。 - supercat

3

当您使用抽象类时,您创建了子类和基类之间的耦合。随着子类数量的增加,这种耦合有时会使代码变得非常难以更改。接口没有这个问题。

您还只有一个继承,因此您应该确保出于适当的原因使用它。


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