【问题标题】:Why are interfaces preferred to abstract classes?为什么接口优先于抽象类?
【发布时间】:2010-10-12 23:32:07
【问题描述】:

我最近参加了一次采访,他们问我一个问题“为什么接口比抽象类更受欢迎?”

我尝试给出一些答案,例如:

  • 我们只能得到一个扩展功能
  • 它们是 100% 抽象的
  • 实现不是硬编码的

他们要求我采用您使用的任何 JDBC api。 “为什么它们是接口?”。

我能得到更好的答案吗?

【问题讨论】:

  • 我几乎可以肯定我以前见过这样的问题,但即使谷歌也找不到。也许我的大脑又在捉弄我了。
  • 注意:我编辑了语法的标题;我离开了身体,因为它似乎是引用,也许他们确实是这样说的。
  • 这是一个加载的问题,因为它假定了一个关于该主题的位置并且没有给出它“可能”有效的上下文。我同意 devinb 的观点。它们都是工具——适当地使用它们。这里有太多答案证明了这个问题......如果你真的想要这份工作,这可能是可以接受的。
  • 不要用答案来证明问题的合理性。这不是他们(嗯,应该)寻找的。表明你知道你在说什么,并且可以胜任这项工作。如果他们值得为他们工作,他们就不是在寻找鹦鹉。
  • 在下面查看我的完整评论.. 但每当我得到像你这样的答案时,候选人都会得到一个冷淡的“谢谢你的时间”。答案没有显示出理解的深度。

标签: java oop interface abstraction


【解决方案1】:

接口不能替代抽象类

喜欢

接口:通过多个不相关的对象来实现一个契约

抽象类:在多个相关对象之间实现相同或不同的行为


有关接口和抽象类的用例,请参阅这个相关的 SE 问题

Interface vs Abstract Class (general OO)

用例:

如果你必须使用Template_method模式,你不能用接口来实现。应该选择抽象类来实现它。

如果你必须为许多不相关的对象实现一个能力,抽象类不能达到目的,你必须选择interface

【讨论】:

    【解决方案2】:

    这是“多重继承”的问题。 我们一次可以通过另一个类“扩展”不超过一个抽象类,但是在接口中,我们可以在单个类中“实现”多个接口。 所以,虽然Java一般不提供多重继承,但是通过使用接口我们可以在其中加入多重继承属性。

    希望对你有帮助!!!

    【讨论】:

      【解决方案3】:

      如果他们认为 X 比 Y 好,我不会担心得到这份工作,我不会喜欢为那些强迫我使用一种设计而不是另一种设计的人工作,因为他们被告知界面是最好的。两者都好视情况而定,不然语言为什么选择加抽象类呢?当然,语言设计者比我聪明。

      【讨论】:

        【解决方案4】:

        这取决于您的要求和实施能力,这非常重要。 关于这个问题,你有很多答案。 我对这个问题的看法是,抽象类是 API 的演变。 您可以在抽象类中定义您未来的函数定义,但您不需要在主类中实现所有函数,但使用接口您无法做到这一点。

        【讨论】:

          【解决方案5】:

          抽象类提供了一种定义行为模板的方法,用户可以在其中插入详细信息。

          一个很好的例子是 Java 6 的SwingWorker。它定义了一个在后台执行某些操作的框架,要求用户为实际任务定义 doInBackground()

          我扩展了这个类,使它自动创建一个弹出进度条。我覆盖了 done(),以控制此弹出窗口的处理,但随后提供了一个新的覆盖点,允许用户选择性地定义进度条消失后会发生什么。

          public abstract class ProgressiveSwingWorker<T, V> extends SwingWorker<T, V> {
          
              private JFrame progress;
          
              public ProgressiveSwingWorker(final String title, final String label) {
                  SwingUtilities.invokeLater(new Runnable() {
                      @SuppressWarnings("serial")
                      @Override
                      public void run() {
                          progress = new JFrame() {{
                              setLayout(new MigLayout("","[grow]"));
                              setTitle(title);
                              add(new JLabel(label));
                              JProgressBar bar = new JProgressBar();
                              bar.setIndeterminate(true);
                              add(bar);
                              pack();
                              setLocationRelativeTo(null);
                              setVisible(true);
                          }};
                      }
                  });
              }
          
              /**
               * This method has been marked final to secure disposing of the progress dialog. Any behavior
               * intended for this should be put in afterProgressBarDisposed.
               */
              @Override
              protected final void done() {
                  progress.dispose();
                  try {
                      afterProgressBarDisposed(get());
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  } catch (ExecutionException e) {
                      e.printStackTrace();
                  }
              }
          
              protected void afterProgressBarDisposed(T results) {
              }
          
          }
          

          用户仍然需要提供doInBackground()的实现。但是,它们也可以有后续行为,例如打开另一个窗口、显示带有结果的 JOptionPane,或者干脆什么都不做。

          使用它:

          new ProgressiveSwingWorker<DataResultType, Object>("Editing some data", "Editing " + data.getSource()) {
          
              @Override
              protected DataResultType doInBackground() throws Exception {
                  return retrieve(data.getSource());
              }
          
              @Override
              protected void afterProgressBarDisposed(DataResultType results) {
                  new DataEditor(results);
              }
          
          }.execute();
          

          这展示了抽象类如何很好地提供模板化操作,与定义 API 契约的接口概念正交。

          【讨论】:

            【解决方案6】:

            总的来说,这绝不是一个应该盲目遵循的“规则”,最灵活的安排是:

            interface
               abstract class
                   concrete class 1       
                   concrete class 2
            

            界面存在有几个原因:

            • 已经扩展了某些东西的现有类可以实现该接口(假设您可以控制现有类的代码)
            • 现有类可以是子类,子类可以实现接口(假设现有类是可子类化的)

            这意味着您可以采用预先存在的类(或者只是必须从其他东西扩展的类)并让它们与您的代码一起使用。

            抽象类为具体类提供所有公共位。抽象类是在您编写新类或修改要扩展它的类时扩展的(假设它们是从 java.lang.Object 扩展的)。

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

            【讨论】:

            • 说得好。列表和集合接口和类有很多这样的例子。
            • 这是一个非常强大和灵活的模式。我已经看到它在框架中使用了很多。实际上,抽象类提供了接口的“骨架”实现。
            • 可能值得注意的是(1)任何从抽象类继承的具体类都不能从其他任何东西继承——这个限制似乎不太可能成为问题,但可能是;直接实现接口的能力提供了一个“安全阀”,以防出现类继承存在问题的情况; (2) 直接实现接口的类 如果接口发生变化,则必须更改;如果基类可以做一些事情,比如提供新方法的默认实现,那么从它派生的类可能不必改变。
            • 如果一个接口最终由三个类(包括抽象基类)直接实现,并且该基类被十几个其他类继承,添加中间层可能意味着改变该接口需要修复三个类,而不是十四个。相当大的胜利。但值得注意的是,变量和存储位置等应该声明为接口类型而不是抽象类型,以便直接实现接口的类与继承抽象基的类进行交互。跨度>
            【解决方案7】:

            他们要求我使用任何 JDBC api 你使用的。 “他们为什么 接口?”。

            我对这个具体问题的回答是:

            SUN 不知道如何实现它们或在实现中放入什么。由服务提供商/数据库供应商将他们的逻辑放入实现中。

            JDBC 的设计与桥接模式有关,桥接模式说“将抽象与其实现分离,以便两者可以独立变化”。

            这意味着 JDBC api 的接口层次结构可以演变,而与 jdbc 供应商提供或使用的实现层次结构无关。

            【讨论】:

              【解决方案8】:

              继承实现时使用抽象类,继承规范时使用接口。 JDBC 标准规定“连接必须执行this”。这是规范。

              【讨论】:

                【解决方案9】:

                不同意上述大多数海报(对不起!如果你愿意,请修改我 :-))


                首先,“只有一个超类”的答案是蹩脚的。任何在采访中给我这个答案的人都会很快被反驳说“C++ 在 Java 和 C++ 有多个超类之前就已经存在了。为什么你认为 James Gosling 只允许 Java 有一个超类?”

                了解你的答案背后的哲学,否则你会干杯(至少如果我采访你的话)。


                其次,与抽象类相比,接口具有多种优势,尤其是在设计接口时。最大的问题是没有对方法的调用者施加特定的类结构。没有什么比尝试使用需要特定类结构的方法调用更糟糕的了。这是痛苦和尴尬的。使用接口 anything 可以以最少的期望传递给方法。

                例子:

                public void foo(Hashtable bar);
                

                对比

                public void foo(Map bar);
                

                对于前者,调用者将始终采用他们现有的数据结构并将其猛烈撞击到一个新的 Hashtable 中。


                第三,接口允许具体类实现者中的公共方法是“私有的”。如果该方法未在接口中声明,则该方法不能被没有使用该方法的业务的类使用(或滥用)。这让我想到了第 4 点......


                第四,接口代表了实现类和调用者之间的最小契约。这个最小的契约准确地指定了具体实现者期望如何被使用,仅此而已。调用类不允许使用接口的“契约”未指定的任何其他方法。使用中的接口名称也符合开发人员对他们应该如何使用对象的期望。如果开发者通过了一个

                public interface FragmentVisitor {
                    public void visit(Node node);
                }
                

                开发人员知道他们可以调用的唯一方法是访问方法。他们不会被他们不应该乱用的具体类中闪亮的方法分心。


                最后,抽象类有许多方法,这些方法实际上只存在于要使用的子类中。因此,对于外部开发人员来说,抽象类往往看起来有点像一团糟,没有关于外部代码打算使用哪些方法的指导。

                是的,当然可以保护一些这样的方法。但是,可悲的是,受保护的方法对同一包中的其他类也是可见的。如果抽象类的方法实现了接口,则该方法必须是公共的。

                但是,使用接口时,所有这些在查看抽象超类或具体类时都会出现的内部问题都可以安全地隐藏起来。


                是的,我知道开发人员当然可以使用一些“特殊”知识将对象转换为另一个更广泛的接口或具体类本身。但是这样的演员阵容违反了预期的合同,开发商应该被打三文鱼。

                【讨论】:

                • 您要反驳的问题比原来的问题要好,恕我直言,尽管人们可能会用“许多最近的语言和框架”而不是特别是 Java 来表达它。虽然 .net 确实从 Java 中借用了一些相当愚蠢的东西(例如,浮点数应该隐式转换为双精度数的概念,但反之则不然,当浮点数到双精度数的转换更有可能是错误的)我认为 .net 没有单继承仅仅是因为 Java 做到了。继承对于代码/数据共享和可替换性都很有用。代码/数据共享通常更方便...
                • ...继承而不是组合,尽管它可以通过任何一种方式实现。允许包含代码/数据共享的多重继承形式会带来困难(例如“钻石问题”)。由于代码/数据的多重继承不允许通过组合实现很多事情,但是事物可以替代多个彼此无关的其他事物的能力非常有用,因此提供一些东西是有意义的类似于继承,但仅限于后一种功能。
                • (顺便说一句,wrt 浮点数和双精度数,将等于 1E38 的两个双精度数相乘,然后将结果转换为浮点数,将产生一个答案,意思是“浮点数要大”,这将是正确的. 将等于 1E38 的两个浮点数相乘,然后将结果转换为双精度数,将产生“答案对于双精度数来说太大”,这是不正确的。
                【解决方案10】:

                上面没有提到一个原因。

                您可以使用 java.lang.reflect.Proxy 轻松装饰任何接口,允许您在运行时将自定义代码添加到给定接口中的任何方法。它非常强大。

                有关教程,请参阅 http://tutorials.jenkov.com/java-reflection/dynamic-proxies.html

                【讨论】:

                  【解决方案11】:

                  好吧,我建议应该改写问题本身。接口主要是一个类获得的契约,该契约本身的实现会有所不同。抽象类通常包含一些默认逻辑,其子类将添加更多逻辑。 我想说问题的答案取决于钻石问题。 Java 防止多重继承来避免它。 (http://en.wikipedia.org/wiki/Diamond_problem)。

                  【讨论】:

                    【解决方案12】:

                    “为什么首选接口而不是 抽象类?”

                    其他帖子在研究接口和抽象类之间的差异方面做得很好,所以我不会重复这些想法。

                    但是看看面试问题,更好的问题真的是“什么时候应该优先选择接口而不是抽象类?” (反之亦然)。

                    与大多数编程结构一样,它们的存在是有原因的,而面试问题中的绝对陈述往往会忽略这一点。它让我想起了你曾经读过的关于 C 中的 goto 语句的所有语句。“你永远不应该使用 goto - 它显示出糟糕的编码技能。”然而,goto 总是有其适当的用途。

                    【讨论】:

                      【解决方案13】:

                      当您只需要某个对象实现某些方法但您不关心它的谱系时,您就可以定义接口。因此,有人可以扩展现有类来实现接口,而不会影响该类先前存在的行为。

                      这就是为什么 JDBC 都是接口;您并不真正关心 JDBC 实现中使用了哪些类,您只需要任何 JDBC 实现即可具有相同的预期行为。在内部,Oracle JDBC 驱动程序可能与 PostgreSQL 驱动程序有很大不同,但这与您无关。一个可能必须继承数据库开发人员已经拥有的一些内部类,而另一个可能是完全从头开发的,但这对您来说并不重要,只要它们都实现相同的接口,以便您可以与其中一个或其他不知道其中任何一个的内部工作原理。

                      【讨论】:

                      • 如果 JDBC 都是纯抽象类,那会有什么不同呢? (撇开不同版本之间的接口有所不同。)
                      • @Tom Hawtin:我也是这么说的,他们问 Tom Hawtin 的问题。
                      • 如果它是抽象类而不是接口,它可能会更快,具体取决于 VM,并且它会强制实现者仅从作为 java.sql 库的一部分提供给他们的类扩展,这可能会受到限制。
                      【解决方案14】:

                      interfaces 是编写纯抽象类的一种更简洁的方式。您可以看出实现并没有潜入(当然您可能希望在某些维护阶段这样做,这会使接口变得糟糕)。就是这样。客户端代码几乎看不出区别。

                      JDBC 是一个非常糟糕的例子。询问任何尝试实现接口并在 JDK 版本之间维护代码的人。 JAX-WS 更糟糕,在更新版本中添加方法。

                      存在技术上的差异,例如能够乘以“继承”接口。这往往是设计混乱的结果。在极少数情况下,拥有不同于接口层次结构的实现层次结构可能会很有用。

                      在接口的不利方面,编译器无法接受一些不可能的强制转换/instanceofs。

                      【讨论】:

                        【解决方案15】:

                        正如devinb 和其他人所提到的,听起来面试官在不接受你的有效答案方面表现出他们的无知。

                        但是,提到 JDBC 可能是一个提示。在这种情况下,也许他们要求客户端针对接口而不是类进行编码的好处。

                        因此,与类设计有关的完全有效的答案(例如 “你只能使用继承”),他们可能正在寻找更像 “解耦一个来自特定实现的客户端”

                        【讨论】:

                          【解决方案16】:

                          抽象类

                          1.不能独立于它们的派生类进行实例化。抽象类构造函数只能由其派生类调用。

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

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

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

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

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

                          界面

                          1.无法实例化。

                          2.接口的所有成员的实现都发生在基类中。在实现类中只实现部分成员是不可能的。

                          3.用额外成员扩展接口会破坏版本兼容性。

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

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

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

                          【讨论】:

                            【解决方案17】:

                            当您使用抽象类时,您会在子类和基类之间建立耦合。这种耦合有时会使代码很难更改,尤其是随着子类数量的增加。接口没有这个问题。

                            你也只有一个继承,所以你应该确保你使用它是出于正当的理由。

                            【讨论】:

                              【解决方案18】:

                              该面试问题反映了提问者的某种信念。我相信这个人是错的,因此你可以选择两个方向之一。

                              1. 给他们想要的答案。
                              2. 不同意。

                              他们想要的答案,嗯,其他海报已经突出了那些令人难以置信的好。 多接口继承,继承迫使类做出实现选择,接口更容易改变。

                              但是,如果您在分歧中提出令人信服(且正确)的论点,那么面试官可能会注意到这一点。 首先,突出接口的积极方面,这是必须的。 其次,我会说接口在许多场景中更好,但它们也会导致代码重复,这是一个负面的事情。如果您有大量的子类,它们将执行基本相同的实现,加上额外的功能,那么您可能需要一个抽象类。它允许您拥有许多具有细粒度细节的相似对象,而仅使用接口,您必须拥有许多具有几乎重复代码的不同对象。

                              接口有很多用途,并且有令人信服的理由相信它们“更好”。但是,您应该始终使用正确的工具来完成这项工作,这意味着您不能取消抽象类。

                              【讨论】:

                              • 如果您不同意,我认为您几乎肯定已经放弃了获得这份工作的机会。
                              • 如果你的面试官心胸狭窄,不愿意听到其他观点,那么我不会想要这份工作。只要您尊重他人(并且了解指挥系统),您就应该能够提出有效、深思熟虑的意见,而不会因此受到惩罚。
                              • 如果可以的话,我会多次 +1。到目前为止,我所看到的唯一答案(适当地)挑战了这个问题。一个更好的问题应该是“什么时候接口比抽象类更受欢迎”,或者“说出接口和抽象类的优缺点”。
                              • @Tom 取决于你如何呈现它,它不应该让你失去工作机会。另一方面,如果确实如此,那么您可能并不想在那里工作:-)
                              • 这个问题并不代表面试官是这样认为的。他们可能有不同的想法,但想看看你是否同意他们的观点,或者有瓶子来挑战一个问题。当职位空缺竞争激烈时,通常会回答加载问题。
                              【解决方案19】:

                              因为接口不会强迫您进入某些继承层次结构。

                              【讨论】:

                                【解决方案20】:

                                抽象类有许多潜在的陷阱。例如,如果您覆盖一个方法,则不会调用 super() 方法,除非您显式调用它。这可能会导致实现不佳的覆盖类出现问题。此外,当您使用继承时,equals() 存在潜在问题。

                                当您想要共享实现时,使用接口可以鼓励使用组合。组合通常是重用其他对象的更好方法,因为它不那么脆弱。继承很容易被过度使用或用于错误的目的。

                                定义一个接口是一种非常安全的方式来定义一个对象应该如何行动,而不会冒可能扩展另一个抽象或非抽象类所带来的脆弱性。

                                另外,正如您所提到的,您一次只能扩展一个类,但您可以实现任意数量的接口。

                                【讨论】:

                                  【解决方案21】:

                                  您可以实现多个接口,但特别是对于 c#,您不能拥有多个继承

                                  【讨论】:

                                    【解决方案22】:

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

                                    【讨论】:

                                    • 我给出了这个答案,他们期待别的东西。
                                    • 那样的话,如果他们不给你报价,我也不会太难过;)
                                    • 这就像我给出的第一个答案,还有一些我在描述中指定的其他答案。我担心我是否还缺少其他东西。
                                    • 也许他们在考验你的信心 ;-)
                                    【解决方案23】:

                                    您只能获得一次继承权。如果你创建一个抽象类而不是一个接口,那么继承你的类的人不能同时继承不同的抽象类。

                                    【讨论】:

                                    • 嗨,这是真的。但是他仍然可以继承一个接口,不是吗?
                                    • Techmaddy 已经在问题中指出了这一点:“我们只能获得一个扩展功能”
                                    猜你喜欢
                                    • 1970-01-01
                                    • 2011-06-04
                                    • 2019-05-30
                                    • 2018-07-19
                                    • 1970-01-01
                                    • 2011-02-27
                                    • 2011-01-23
                                    • 1970-01-01
                                    • 2013-09-21
                                    相关资源
                                    最近更新 更多