37

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

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

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

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

我可以得到更好的答案吗?

4

23 回答 23

55

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

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

他们想要的答案,嗯,其他海报已经非常好地突出了这些。多重接口继承,继承迫使类做出实现选择,接口可以更容易地改变。

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

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

于 2009-03-12T17:30:37.280 回答
24

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

interface
   abstract class
       concrete class 1       
       concrete class 2

该界面的存在有几个原因:

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

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

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

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

于 2009-03-12T17:45:27.260 回答
23

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

于 2009-03-12T17:17:49.853 回答
10

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

于 2009-03-12T17:18:05.093 回答
10

抽象类

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

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

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

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

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

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

界面

1.无法实例化。

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

3.扩展接口增加成员,破坏了版本兼容性。

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

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

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

于 2009-03-12T17:25:01.380 回答
7

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

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

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

于 2009-03-12T17:46:44.337 回答
5

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

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

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

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

于 2009-03-12T17:23:41.430 回答
4

继承实现时使用抽象类,继承规范时使用接口。JDBC 标准规定“连接必须这样做。那是规范。

于 2009-03-15T11:25:02.383 回答
3

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

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

于 2009-03-12T17:18:36.870 回答
3

“为什么接口比抽象类更受欢迎?”

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

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

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

于 2009-03-12T18:01:30.120 回答
3

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


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

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


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

例子:

public void foo(Hashtable bar);

对比

public void foo(Map bar);

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


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


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

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

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


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

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

然而,使用接口时,所有这些在查看抽象超类或具体类时都会出现的内部结构被安全地隐藏起来。


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

于 2009-03-15T07:34:00.457 回答
2

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

于 2011-07-30T16:12:02.080 回答
2

这就是“多重继承”的问题。我们一次可以通过另一个类“扩展”不超过一个抽象类,但是在接口中,我们可以在单个类中“实现”多个接口。因此,尽管 Java 通常不提供多重继承,但通过使用接口,我们可以在其中合并多重继承属性。

希望这可以帮助!!!

于 2012-12-03T17:05:21.130 回答
1

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

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

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

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

于 2009-03-12T17:34:52.510 回答
1

上面没有提到的原因之一。

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

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

于 2009-03-12T21:27:51.623 回答
1

接口不能代替抽象类

更喜欢

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

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


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

接口 vs 抽象类(通用 OO)

用例:

如果必须使用Template_method模式,则无法通过接口实现。应该选择抽象类来实现它。

如果您必须为许多不相关的对象实现功能,则抽象类无法达到目的,您必须选择interface

于 2016-11-07T18:03:31.370 回答
0

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

于 2009-03-12T17:19:32.813 回答
0

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

于 2009-03-12T17:27:12.327 回答
0

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

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

于 2009-03-12T17:50:22.707 回答
0

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

于 2009-03-12T21:05:06.483 回答
0

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

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

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

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

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

于 2009-04-13T21:47:13.817 回答
0

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

一个很好的例子是 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 契约的接口概念正交。

于 2009-10-21T13:28:43.593 回答
0

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

于 2009-11-25T13:03:26.370 回答