何时使用接口而不是抽象类?反之又如何?

531

这可能是一个通用的面向对象编程问题。我想进行关于接口和抽象类在使用上的通用比较。

什么时候应该使用接口,什么时候应该使用抽象类


1
这个问题被问了很多次:https://dev59.com/onVD5IYBdhLWcg3wL4mM - mmcdole
1
除了下面的答案,这是一个很好的简短列表,您可能想要优先使用接口的地方以及您可能不想使用接口的地方:何时使用接口:http://msdn.microsoft.com/en-us/library/3b5b8ezk(v=vs.80).aspx - Anthony
当你不确定类要做什么时,请使用抽象类。如果你确定了,请使用接口。 - Uğur Gümüşhan
1
我很想知道有多少开发人员在日常开发中定义和使用接口,而不是在微软工作的开发人员。 - user1451111
https://dev59.com/D3RA5IYBdhLWcg3w_C8w#74781277 - Billu
26个回答

541

我写了一篇关于这个的文章:

抽象类和接口

总结:

当我们谈论抽象类时,我们正在定义对象类型的特征;指定对象是什么

当我们谈论接口并定义我们承诺提供的功能时,我们正在建立关于对象能做什么的契约。


169
非常有帮助:接口不表达“杜宾犬是狗的一种,每只狗都会走路”这样的内容,而更像是“这个东西可以走路”。谢谢。 - ᴍᴇʜᴏᴠ
Alex的解释如下,关于仅描述已实现功能与同时描述存储状态之间的区别,似乎是对这个问题更好的回答,因为这些差异不仅仅是哲学上的。 - Duncan Malashock
1
邓肯·马拉肖克,不是很好。豪尔赫的回答更好。亚历克斯的回答侧重于机械方面,而豪尔赫的则更注重语义学。 - Nazar Merza
29
在你指定的答案之前,我喜欢这个陈述:如果可以说“一个A是一个B”,请使用抽象类和继承。如果可以说“A能够[做]某事”,请使用接口。 - S1r-Lanzelot
1
我认为Java中抽象类的一个很好的例子是Number,而接口方面则是Comparable - Gaurav Krishna
显示剩余3条评论

517

抽象类可以拥有共享的状态或功能。接口仅是提供状态或功能的承诺。良好的抽象类能够减少需要重新编写的代码量,因为它的功能或状态可以被共享。而接口没有定义任何信息可供共享。


81
对我来说,这是最好的答案,可惜没有被投票得更高。是的,这两个概念之间存在哲学差异,但根本观点是抽象类确保所有后代共享功能/状态,而接口只确保了一个共同的联系。 - drharris
5
例如,抽象基类用于“模板方法”设计模式,而接口用于“策略”设计模式。 - Raedwald
2
我认为Jorge的总结解释了它们存在的主要思想,而Alex的答案则是结果上的区别。我希望我能将两者都标记为正确答案,但我仍然更喜欢Jorge的答案。 - Chirantan
2
这里是带有代码的示例,请点击此处 - Shaiju T
对我来说,“一个好的抽象类将减少需要重写的代码量,因为它的功能或状态可以共享。”这个陈述是答案的核心。 - Mantra

85

就我个人而言,我几乎从来没有写过抽象类。

大多数情况下,我看到抽象类被滥用,是因为抽象类的作者使用了“模板方法”模式。

“模板方法”的问题在于它几乎总是有点可重入 - “派生”类不仅知道它正在实现的基类的“抽象”方法,还知道基类的公共方法,尽管大部分时间它不需要调用它们。

(过度简化的)示例:

abstract class QuickSorter
{
    public void Sort(object[] items)
    {
        // implementation code that somewhere along the way calls:
        bool less = compare(x,y);
        // ... more implementation code
    }
    abstract bool compare(object lhs, object rhs);
}
这里,这个类的作者编写了一个通用算法,并希望人们通过提供自己的"钩子"(在这种情况下为"compare"方法)来对其进行"特化"以便使用它。因此,预期的用法类似于:
class NameSorter : QuickSorter
{
    public bool compare(object lhs, object rhs)
    {
        // etc.
    }
}
这样做的问题在于您过度耦合了两个概念:
1. 比较两个项目的方法(哪个项目应该先执行) 2. 对项目进行排序的方法(例如,快速排序与归并排序等)
在上面的代码中,理论上,“compare”方法的作者可以重新进入超类“Sort”方法...即使实际上他们永远不想或需要这样做。
这种不必要的耦合所付出的代价是很难更改超类,在大多数面向对象语言中,无法在运行时更改它。
另一种可选方法是使用“策略”设计模式。
interface IComparator
{
    bool compare(object lhs, object rhs);
}

class QuickSorter
{
    private readonly IComparator comparator;
    public QuickSorter(IComparator comparator)
    {
        this.comparator = comparator;
    }

    public void Sort(object[] items)
    {
        // usual code but call comparator.Compare();
    }
}

class NameComparator : IComparator
{
    bool compare(object lhs, object rhs)
    {
        // same code as before;
    }
}

现在请注意:我们拥有的全部是接口和这些接口的具体实现。实际上,你不需要其他任何东西来进行高级面向对象设计。

为了“隐藏”我们通过使用“QuickSort”类和“NameComparator”实现了“姓名排序”的事实,我们可能仍然会在某个地方编写工厂方法:

ISorter CreateNameSorter()
{
    return new QuickSorter(new NameComparator());
}

任何时候你有一个抽象类,你都可以这样做...即使在基类和派生类之间存在自然的可重入关系时,通常也值得将它们显式化。

最后一点思考:我们所做的只是通过使用 "QuickSort" 函数和 "NameComparison" 函数来“组合”一个“NameSorting”函数...在函数式编程语言中,这种编程风格变得更加自然,代码更少。


6
仅仅因为你可以使用抽象类或模板方法模式并不意味着你需要避免它们。策略模式是一种针对不同情况的不同模式,就像这个例子一样,但是有很多例子,模板模式比策略模式更加适合。 - Jorge Córdoba
5
根据我的经验,我很少遇到需要使用模板方法的情况,或者说几乎没有。而“抽象”就是用来支持“模板方法”设计模式的语言特性。 - Paul Hollingsworth
好的,我曾经在一个专家系统中使用过它,其中流程大致如下:1. 填充参数,2. 对它们进行向量积,3. 对每对计算结果,4. 合并结果,其中步骤1和3被委托,而步骤2和4则在基类中实现。 - Jorge Córdoba
不仅是你的例子,而是大多数其他例子都表明了许多人抱怨面向对象系统变得过于复杂的原因。是的,使用接口方法确实带来了一些好处,但抽象类方法要简单得多,更易于理解和使用。 - Dunk
13
我发现几乎所有抽象类的使用都比较难理解。用盒子之间互相通信的思路来代替继承关系更容易理解(对我来说)...但我也同意目前面向对象语言过于强制样板代码...函数式编程将会成为未来的趋势,超过面向对象。 - Paul Hollingsworth
7
误用的例子相当琐碎。它很少仅限于像比较这样的简单功能。更常见的情况是派生类要么替换扩展某些默认功能(在后一种情况下,调用基类函数是完全有效的)。在您的示例中没有默认功能,因此使用抽象类没有合理的理由。 - SomeWittyUsername

76
如果您将Java视为OOP语言,那么在Java 8发布之后,"接口不提供方法实现"这个说法已经不再适用。现在Java提供了接口默认方法的实现。
简单来说,我想使用: 接口:通过多个无关的对象来实现合同。它提供了"HAS A"的能力。 抽象类:用于在多个相关对象之间实现相同或不同的行为。它建立了"IS A"的关系。
Oracle网站提供了interfaceabstract class之间的主要区别。
如果符合以下情况,请考虑使用抽象类:
  1. 您希望在几个紧密相关的类之间共享代码。
  2. 您希望扩展您的抽象类的类具有许多公共方法或字段,或需要访问修饰符(例如protected和private)。
  3. 您希望声明非静态或非最终字段。
如果符合以下情况,请考虑使用接口:
  1. 您希望无关的类实现您的接口。例如,许多不相关的对象可以实现Serializable接口。
  2. 您希望指定特定数据类型的行为,但不关心谁实现其行为。
  3. 您想利用多继承性质。
例如:
抽象类(IS A关系) Reader是一个抽象类。 BufferedReader是一个Reader

FileReader是一个Reader

FileReaderBufferedReader用于读取数据的共同目的,并且它们通过Reader类相关联。

接口(具有能力的接口)

Serializable是一个接口。

假设您的应用程序中有两个实现Serializable接口的类

Employee implements Serializable

Game implements Serializable

在这里,您无法通过Serializable接口在EmployeeGame之间建立任何关系,因为它们的目的不同。两者都能够序列化状态,但比较到此结束。

看一下这些帖子:

我应该如何解释接口和抽象类之间的区别?


2
最佳答案我认为是: - Rajib Sarker
每个人都更善于通过例子来学习。非常好的回答。谢谢! - AliN11

49

我的观点:

接口基本上定义了一个合同,任何实现该接口的类必须遵守(实现接口成员),它不包含任何代码。

另一方面,抽象类可以包含代码,并且可能有一些标记为抽象的方法,继承类必须实现这些方法。

我使用抽象类的罕见情况是当我有一些默认功能时,继承类可能没有兴趣覆盖,在抽象基类中,某些专业类继承自此基类。

例如(非常简单的例子!):考虑一个名为Customer的基类,其中包含抽象方法如CalculatePayment()CalculateRewardPoints(),以及一些非抽象方法如GetName()SavePaymentDetails()

RegularCustomerGoldCustomer这样的专业类将从Customer基类继承并实现自己的CalculatePayment()CalculateRewardPoints()方法逻辑,但重新使用GetName()SavePaymentDetails()方法。

您可以向抽象类添加更多功能(非抽象方法),而不会影响使用旧版本的子类。而向接口添加方法会影响实现它的所有类,因为它们现在需要实现新添加的接口成员。

所有成员都是抽象的抽象类类似于接口。


2
你可以在抽象类中添加更多的功能(非抽象方法),而不会影响使用旧版本的子类。而将方法添加到接口将影响所有实现它的类,因为它们现在需要实现新添加的接口成员。 - Mantra
1
接口可以有“默认”方法,因此在接口中没有方法实现是错误的想法。 “从父类到子类”的IS-A关系是关键。此外,“共享属性”与“共享属性”之间存在区别。例如,狗是动物的一种。但是狗也可以“走路”。 - ha9u63a7

41

好的,我刚“领悟”了这个问题 - 这里是通俗易懂的解释(如果我理解有误请纠正)- 我知道这个话题很老了,但是未来可能还有其他人会遇到类似的问题...

抽象类允许你创建一个蓝图,并且允许你构造(实现)你想要所有子类都拥有的属性和方法。

另一方面,接口只允许你声明在所有实现它的类中希望存在具有给定名称的属性和/或方法 - 但不指定如何实现它。此外,一个类可以实现多个接口,但只能扩展一个抽象类。接口更像是一个高层次的架构工具(如果你开始掌握设计模式,就会更清楚) - 抽象类在两个方面都有所涉足,也可以执行一些琐碎的工作。

为什么要使用其中之一?前者允许对后代进行更具体的定义,而后者允许更大程度上的多态性。这最后一点对于最终用户/编码者非常重要,他们可以利用这些信息来实现A.P.(接口)以适应各种组合/形状的需求。

我认为这是我的“灯泡瞬间” - 少从作者的角度考虑接口,更多地从后来在项目中添加实现或扩展API的任何编码器的角度考虑接口。


在此基础上构建: 实现接口的对象采用它们的类型。这是至关重要的。因此,您可以将接口的不同变体传递给类,但是使用接口的类型名称引用它们(及其方法)。因此,您可以消除开关或if / else循环的需要。尝试使用此教程学习该主题-它演示了通过策略模式使用接口的用法。http://www.phpfreaks.com/tutorial/design-patterns---strategy-and-bridge/page2 - sunwukung
我完全同意你的恍然大悟时刻:“A.P.I(接口)以各种组合/形状来适应他们的需求”!非常好的观点。 - Trevor Boyd Smith

31

如果你的概念清晰,那么在什么时候做什么是一件非常简单的事情。

抽象类可以派生出子类,而接口只能被实现。这两者之间存在一些区别。当你派生一个抽象类时,派生类和基类之间的关系是"是一个"的关系。例如,狗是动物,羊也是动物,这意味着派生类继承了一些来自基类的属性。

而对于接口的实现,关系则是"可以是"。例如,狗可以是间谍狗,狗可以是马戏团狗,狗可以是赛跑狗。这意味着你通过实现特定的方法来获得某些东西。

希望我表达清楚。


2
你的第二个例子仍然可以是“Is A”关系。一只赛狗就是一只狗。 - AliN11

13

何时优先选择抽象类而非接口?

  1. 如果计划在程序/项目的整个生命期内更新基类,则最好将基类设置为抽象类。
  2. 如果要构建与层次结构中密切相关的对象的骨干,则使用抽象类非常有益。

何时优先选择接口而非抽象类?

  1. 如果不涉及大型层次结构类型的框架,则接口是一个很好的选择。
  2. 由于抽象类不支持多重继承(钻石问题),使用接口可以解决这个问题。

4
让我寻找简单答案的那种思维方式。 - Satya
1
就我个人而言,我真的很喜欢这个答案。 - Brent Rittenhouse

13

1.如果您正在创建为无关类提供常见功能的内容,请使用接口。

2.如果您正在为在层次结构中紧密相关的对象创建内容,请使用抽象类。


12

类只能继承一个基类,因此如果您想使用抽象类为一组类提供多态性,则它们必须全部继承自该类。抽象类还可以提供已实现的成员。因此,您可以使用抽象类确保某些相同的功能,但不能使用接口。

以下是一些建议,以帮助您决定是使用接口还是抽象类来为您的组件提供多态性:

  • 如果您预计将创建组件的多个版本,请创建抽象类。抽象类为您的组件提供了一种简单易行的版本控制方式。通过更新基类,所有继承类都将自动更新更改。然而,接口无法像这样被更改。如果需要新版本的接口,则必须创建全新的接口。
  • 如果您正在创建的功能将在各种不同的对象之间广泛使用,请使用接口。抽象类应主要用于密切相关的对象,而接口最适合为不相关的类提供公共功能。
  • 如果您设计的是小而简洁的功能部分,请使用接口。如果您正在设计大型功能单元,请使用抽象类。
  • 如果您要为组件的所有实现提供公共的已实现功能,请使用抽象类。抽象类允许您部分实现类,而接口不包含任何成员的实现。

摘自:
http://msdn.microsoft.com/en-us/library/scsyfw1d%28v=vs.71%29.aspx


UML并没有阻止多类继承。多重继承是由编程语言决定的,而不是由UML决定的。例如,在Java和C#中不允许多类继承,但在C++中允许。 - BobRodes
@BobRodes:面向对象框架可以提供多种特性的组合,但并非所有组合都能实现。广义多重继承排除了其他一些有用特性的组合,包括将引用直接转换为实际实例的任何父类型或支持的任何接口类型,以及独立编译基类型和派生类型并在运行时加入它们的能力。 - supercat
@supercat,您对使用多重继承所带来的一些问题进行了很好的解释。然而,在UML中并没有禁止在图表中使用多个类继承。我是在回应上述“类只能从一个基类继承...”这个说法时发言的,但情况并不完全如此。 - BobRodes
@BobRodes:这个问题被标记为Java。Java包含了所指示的功能,因此仅限于多重继承的形式,不能产生“致命钻石”(虽然事实上,他们实现默认接口实现的方式可能会导致致命钻石的出现)。 - supercat
@supercat 哦,好的。我通常不看Java标签,所以当我写下那个评论时,至少我认为我是在评论一个UML答案。无论如何,我同意你的评论。 - BobRodes

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