Java 中的抽象类和接口

109

我被问了一个问题,我想在这里得到我的答案的审查。

问:在哪种情况下,扩展抽象类比实现接口更合适?

答:如果我们使用模板方法设计模式。

我是正确的吗?

如果我没有清楚地陈述问题,我很抱歉。
我知道抽象类和接口之间的基本区别。

  1. 当要求是需要在每个子类中为特定操作(实现方法)实现相同的功能,并为其他一些操作(仅方法签名)实现不同的功能时,请使用抽象类

  2. 如果您需要将签名设置为相同(并且实现不同),以便您可以符合接口实现,请使用接口

  3. 我们最多可以扩展一个抽象类,但可以实现多个接口

重申问题:除了上述提到的情况外,还有其他特定情况需要使用抽象类吗(其中一个概念基于模板方法设计模式)?

接口 vs. 抽象类

选择这两个之间真的取决于你想做什么,但幸运的是,Erich Gamma 可以帮助我们一些。
像往常一样,存在权衡,接口可以让你在基类方面拥有自由,抽象类则给你“自由添加新方法”的自由。——Erich Gamma
你不能更改接口而不必更改代码中的其他内容,因此避免这种情况的唯一方法是创建一个全新的接口,这可能并不总是好事。
抽象类应主要用于密切相关的对象。接口更适合为不相关的类提供通用功能。

可能是 接口 vs 抽象类 (面向对象通用) 的重复问题。 - bezmax
https://dev59.com/QnRB5IYBdhLWcg3wAjXR - Sandeep Manne
2
@shiplu.mokadd.im 这是没有区别的区分。你不能在不扩展它的情况下使用抽象类。你在这里挑剔似乎完全毫无意义。 - user207421
这个问题解释了接口和抽象类之间的区别:https://dev59.com/aXI-5IYBdhLWcg3wVWpi - Ravindra babu
这个回答解决了你的问题吗?接口 vs 抽象类(通用OO) - Shyam Kumar Sundarakumar
显示剩余2条评论
15个回答

93

何时使用接口

接口允许某个人从头开始实现您的接口或在一些其他代码中实现您的接口,这些代码的原始或主要目的与您的接口完全不同。对于他们来说,您的接口只是附带的东西,他们必须将其添加到自己的代码中以便能够使用您的软件包。缺点是接口中的每个方法都必须是公共的。您可能不想公开所有内容。

何时使用抽象类

相比之下,抽象类提供更多的结构。它通常定义了一些默认实现,并提供了一些对完整实现有用的工具。问题在于,使用它的代码必须将你的类作为基础。如果想要使用您的软件包的其他程序员已经独立开发了自己的类层次结构,那么这可能非常不方便。在Java中,一个类只能继承一个基类。

何时同时使用两者

您可以提供两个世界的最佳选择:接口和抽象类。实现者可以忽略您的抽象类。唯一的缺点是通过其接口名称调用方法比通过其抽象类名称调用方法稍慢。


我认为OP想知道何时扩展抽象类而不是实现接口。 - Shiplu Mokaddim
@shiplu.mokadd.im 实际上,原帖提出了一个非常具体的问题,答案只能是“是”或“否”。 - user207421
6
没错。但在SO上,我们需要用适当的解释回答“是/否”问题。 - Shiplu Mokaddim
3
我不认为这使你有权误传他的问题。 - user207421
仅凭这个单一的语句“如果我们正在使用模板方法设计模式”,我们不能说“是”或“否”。 - DivineDesert
显示剩余2条评论

35
重申问题: 在除上述提到的情况之外,是否还有其他场景中我们需要使用抽象类(一个例子是模板方法设计模式概念上就基于抽象类)?
是的,如果您使用JAXB,它不支持接口。您应该使用抽象类或者通过泛型绕过这个限制。
个人博客中得知: 接口:
  1. 一个类可以实现多个接口
  2. 接口不能提供任何代码
  3. 接口只能定义公共静态常量
  4. 接口不能定义实例变量
  5. 添加新方法会对实现类产生连锁反应(设计维护)
  6. JAXB无法处理接口
  7. 接口不能扩展或实现抽象类
  8. 所有接口方法都是公共的
通常情况下,接口应该用于定义合同(达成什么,而非怎样达成) 抽象类:
  1. 一个类最多只能扩展一个抽象类
  2. 抽象类可以包含代码
  3. 抽象类可以同时定义静态和实例常量(final)
  4. 抽象类可以定义实例变量
  5. 修改现有抽象类代码会对扩展类产生连锁反应(实现维护)
  6. 向抽象类添加新方法对扩展类没有连锁反应
  7. 抽象类可以实现接口
  8. 抽象类可以实现私有和受保护的方法
抽象类应该用于(部分)实现。它们可以是约束API合同实现方式的一种手段。

5
在Java 8中,对于接口#8,你也可以有“默认(default)”和“静态(static)”方法。 - Novice User

22

22

这里有很多很棒的回答,但我通常发现同时使用接口和抽象类是最佳选择。考虑这个人为构造的例子:

你在一家投资银行担任软件开发人员,需要构建一个将订单放入市场的系统。你的接口捕捉了交易系统的最一般概念。

1) Trading system places orders
2) Trading system receives acknowledgements

并且可以在接口 ITradeSystem 中捕获

public interface ITradeSystem{

     public void placeOrder(IOrder order);
     public void ackOrder(IOrder order);

}

现在,工程师们可以在销售部门和其他业务领域开始与您的系统进行接口交互,以将下单功能添加到他们现有的应用程序中。而且您甚至还没有开始构建!这就是接口的威力。

那么,您继续为股票交易员构建系统;他们听说您的系统有一个找到廉价股票的功能,并非常渴望尝试!您在一个名为findGoodDeals()的方法中捕获了此行为,但也意识到连接市场涉及许多混乱的事情。例如,您必须打开一个SocketChannel

public class StockTradeSystem implements ITradeSystem{    

    @Override 
    public void placeOrder(IOrder order);
         getMarket().place(order);

    @Override 
    public void ackOrder(IOrder order);
         System.out.println("Order received" + order);    

    private void connectToMarket();
       SocketChannel sock = Socket.open();
       sock.bind(marketAddress); 
       <LOTS MORE MESSY CODE>
    }

    public void findGoodDeals();
       deals = <apply magic wizardry>
       System.out.println("The best stocks to buy are: " + deals);
    }

具体实现将有很多这样混乱的方法,比如connectToMarket(),但实际上交易员们只关心findGoodDeals()

现在抽象类发挥作用了。你的老板告诉你货币交易商也想使用你的系统。看着货币市场,你会发现与股票市场相比,它们的基础结构非常相似。事实上,connectToMarket()可以完全复用以连接到外汇市场。但是,在货币领域中,findGoodDeals()是一个完全不同的概念。因此,在将代码库传递给海外的外汇专家之前,您首先要重构成一个abstract类,使findGoodDeals()处于未实现状态。

public abstract class ABCTradeSystem implements ITradeSystem{    

    public abstract void findGoodDeals();

    @Override 
    public void placeOrder(IOrder order);
         getMarket().place(order);

    @Override 
    public void ackOrder(IOrder order);
         System.out.println("Order received" + order);    

    private void connectToMarket();
       SocketChannel sock = Socket.open();
       sock.bind(marketAddress); 
       <LOTS MORE MESSY CODE>
    }

你的股票交易系统已经实现了findGoodDeals(),就像你之前定义的那样,

public class StockTradeSystem extends ABCTradeSystem{    

    public void findGoodDeals();
       deals = <apply magic wizardry>
       System.out.println("The best stocks to buy are: " + deals);
    }

但现在这位外汇交易高手可以通过提供一个实现findGoodDeals()的系统来构建她的系统;她不必重新实现套接字连接或甚至是接口方法!

public class CurrencyTradeSystem extends ABCTradeSystem{    

    public void findGoodDeals();
       ccys = <Genius stuff to find undervalued currencies>
       System.out.println("The best FX spot rates are: " + ccys);
    }

面向接口编程是很强大的,但是类似的应用程序通常以几乎相同的方式重新实现方法。使用抽象类可以避免重新实现,同时保留接口的功能。

注意:有人可能会想知道为什么findGreatDeals()不是接口的一部分。记住,接口定义了交易系统最普遍的组件。另一位工程师可能会开发一个完全不同的交易系统,在那里他们并不关心寻找好的交易。接口保证销售桌面也能与他们的系统接口,因此最好不要将接口与"好的交易"等应用概念纠缠在一起。


8
你应该使用抽象类或接口?
如果以下任何一种情况适用于您的用例,请考虑使用抽象类:
- 您想在几个密切相关的类之间共享代码。 - 您期望扩展抽象类的类具有许多公共方法或字段,或需要除public之外的访问修饰符(例如protected和private)。 - 您想声明非静态或非最终字段。这使您能够定义可以访问和修改它们所属对象状态的方法。
如果以下任何一种情况适用于您的用例,请考虑使用接口:
- 您期望无关的类实现您的接口。例如,Comparable和Cloneable接口由许多不相关的类实现。 - 您想指定特定数据类型的行为,但不关心谁实现其行为。 - 您希望利用类型的多重继承。 - 提供者定期向接口添加新方法,以避免问题,请改为扩展抽象类而不是接口。
参考链接:http://docs.oracle.com/javase/tutorial/java/IandI/abstract.html

5
在过去的三年里,随着与Java 8版本的接口新增功能的加入,事情已经发生了很大变化。
从Oracle文档page中关于接口的说明:

接口是一个引用类型,类似于类,只能包含常量、方法签名、默认方法、静态方法和嵌套类型。方法体仅存在于默认方法和静态方法中。

正如你在问题中引用的那样,抽象类最适合用于模板方法模式,其中您必须创建骨架。接口不能在这里使用。
再考虑一点,更喜欢抽象类而不是接口:
基类中没有实现,只有子类需要定义自己的实现。您需要抽象类而不是接口,因为您想与子类共享状态。 抽象类在相关类之间建立“is a”关系,接口在不相关的类之间提供“has a”能力
关于您问题的第二部分,这适用于包括Java在内的大多数编程语言,但不包括Java-8发布版之前。
引用Erich Gamma的话:“总是会有一个权衡,接口让你在基类方面更加自由,抽象类则让你能够随后添加新方法。” 您无法更改接口而不必更改代码中的其他许多内容。
如果您以前优先选择抽象类而不是接口,并考虑了上述两个因素,则现在必须重新思考,因为默认方法使接口具有了强大的功能。
默认方法使您能够向库的接口添加新功能,并确保与旧版本的该些接口编写的代码二进制兼容。
要在接口和抽象类之间进行选择,Oracle文档页面引用的是:
抽象类与接口类似,无法实例化,可以包含具有或不具有实现的方法。但是,使用抽象类,您可以声明非静态和final字段,并定义公共、受保护和私有的具体方法。
使用接口,所有字段都自动为public、static和final,并且您声明或定义的所有方法(作为默认方法)都是public。此外,您只能扩展一个类,无论它是否是抽象类,而您可以实现任意数量的接口。
有关详细信息,请参阅以下相关问题: Interface vs Abstract Class (general OO) How should I have explained the difference between an Interface and an Abstract class? 总之:现在更倾向于使用接口。 除了上述提到的情况外,还有哪些场景需要使用抽象类(其中一个是看到模板方法设计模式仅基于此概念)?
除了模板方法模式之外,某些设计模式使用抽象类(而不是接口)。
创建型模式: 抽象工厂模式 结构型模式: 装饰者模式 行为型模式: 中介者模式

抽象类建立相关类之间的“是一个”关系,而接口在不相关的类之间提供“有一个”能力。 - Gabriel
接口也可以有私有方法。它们之间的关键区别是“IS-A”和“HAS-A”关系。基于此,可以找到其他差异。 - Rohit Gaikwad

3
在我看来,基本的区别在于一个接口不能包含非抽象方法,而抽象类可以。 因此,如果子类共享一个常见的行为,这个行为可以在超类中实现,从而被继承到子类中。
此外,我引用了《Java软件架构设计模式》一书中的以下内容:
“在Java编程语言中,不支持多重继承。 这意味着一个类只能从一个单一类继承。因此,应该仅在绝对必要时使用继承。只要可能,表示共同行为的方法应该以Java接口的形式声明,由不同的实现类来实现。但是,接口受到的限制是它们不能提供方法实现。这意味着接口的每个实现者都必须显式地实现接口中声明的所有方法,即使其中一些方法表示功能的不变部分,并且在所有实现者类中具有完全相同的实现。这会导致冗余代码。以下示例演示了如何在不需要冗余方法实现的情况下使用抽象父类模式。”

3

您的说法不正确。有很多情况需要考虑。无法简化成一个8个单词的规则。


1
除非你像这样含糊不清:“尽可能使用接口 ;)” - Peter Lawrey
@PeterLawrey 嗯,不要让循环论证拖慢你的速度;-) - user207421
毕竟这是“堆栈溢出”。 ;) 我的意思是,如果你可以使用更简单的接口,请这样做。否则,你只能使用抽象类。我认为这并不是很复杂。 - Peter Lawrey
我认为你可以提供更具建设性的想法,比如谈论一些代表性的场景。 - Adams.H

3
最简短的答案是,当你寻求的一些功能已经在抽象类中实现时,扩展抽象类。
如果你实现接口,你必须实现所有方法。但对于抽象类,你需要实现的方法数量可能会更少。
模板设计模式中,必须定义行为。这个行为取决于其他抽象方法。通过创建子类并定义这些方法,您实际上定义了主要行为。底层行为不能在接口中定义,因为接口不定义任何东西,它只是声明。因此,模板设计模式始终带有一个抽象类。如果您想保持行为流程完整,必须扩展抽象类,但不要覆盖主要行为。

纯虚函数的附加参考资料将为抽象类和接口的收敛提供更多深入了解, 纯虚函数也可以用于方法声明被用来定义一个接口的情况 - 类似于Java中interface关键字明确指定的内容。在这种情况下,派生类将提供所有实现。在这种设计模式中,作为接口的抽象类将只包含纯虚函数,而没有数据成员或普通方法。 第(1/2)部分 - Abhijeet
抽象类和接口的差异可以通过上面最后一行的解释“没有数据成员或普通方法”来说明。 - Abhijeet

2

抽象类与接口在两个重要方面不同

  • 它们为所选方法提供默认实现(这已经包含在您的答案中)
  • 抽象类可以具有状态(实例变量)-因此,这是另一种情况,您希望使用它们代替接口

我会完善接口可以有变量,但它们默认是final的。 - Tomasz Mularczyk

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