如何解释接口和抽象类的区别?

538
在我的面试中,有人要求我解释一个接口(Interface)和一个抽象类(Abstract class)之间的区别。
这是我的回答:

Java接口的方法默认是抽象的,不能有实现。Java抽象类可以含有实现了默认行为的实例方法。

在Java接口中声明的变量默认都是final的。抽象类可以包含非final变量。

Java接口的成员默认都是public的。Java抽象类可以像普通类一样拥有私有、受保护等多种访问权限。

使用关键字"implements"来实现Java接口;使用关键字"extends"来继承Java抽象类。

Java接口只能继承另一个Java接口,而抽象类可以继承另一个Java类并实现多个Java接口。

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

然而,面试官不满意,并告诉我这个描述代表了“书本知识”。
他要求我给出一个更加实际的回答,用实际例子解释我何时会选择抽象类而不是接口
那么我哪里错了?

38
也许你的回答看起来像是在说一些你不理解的内容?可能只需要改变表达方式,让它更像自己的话语风格。 - Kirill Kobelev
22
你回答了一个(相当正确的)技术差异列表。采访者可能更希望得到更概念性的答案(例如,基于什么选择使用接口和抽象类)。 - Ted Hopp
18
你忘了提到抽象类具有构造函数,尽管你无法实例化抽象类,但构造函数可以被子类使用。接口只指示“什么”,而不是“如何”,因为它们定义了一个合同(方法列表),而抽象类也可以指示“如何”(实现一个方法)。使用接口可以模拟多重继承(一个类可以实现多个接口,但只能扩展一个类)。使用接口可以为不同的家族提供基类型:Flyer f=new Plane(); Flyer f2=new Bird(); 鸟和飞机不属于同一家族,但都可以飞行(都是飞行器)。 - Francisco Goldenstein
10
从Java8开始,接口可以包含方法。因此,除了面向对象的概念之外,这些所谓的“差异”随时可能会改变。 - ring bearer
22
我没有对你的回答有任何问题,并且我认为面试官无权嘲笑“书本知识”。面试官并不总是知道他们所问问题的正确答案,有些面试只是为了警告你不要在那里工作。 - user207421
显示剩余12条评论
32个回答

568

首先我会举个例子:

public interface LoginAuth{
   public String encryptPassword(String pass);
   public void checkDBforUser();
}

假设您的应用程序中有3个数据库。那么每个数据库的实现都需要定义上述2个方法:

public class DBMySQL implements LoginAuth{
          // Needs to implement both methods
}
public class DBOracle implements LoginAuth{
          // Needs to implement both methods
}
public class DBAbc implements LoginAuth{
          // Needs to implement both methods
}

但是如果encryptPassword()不依赖于数据库,并且对于每个类都相同,那么上述方法就不是一个好的方法。

相反,考虑以下方法:

public abstract class LoginAuth{
   public String encryptPassword(String pass){
            // Implement the same default behavior here 
            // that is shared by all subclasses.
   }

   // Each subclass needs to provide their own implementation of this only:
   public abstract void checkDBforUser();
}

现在,在每个子类中,我们只需要实现一个方法 - 与数据库有关的方法。


125
我不确定这真正解释了区别...虽然它是一种好的技术。我想值得指出的是,Java 8 终于承认 C++ 是正确的,多重继承可以做到并且有用,所以接口现在不仅可以定义函数签名,还可以提供默认实现。因此,使用接口会更好。 - thecoshman
3
@thecoshman 如果我像答案中那样(使用一个实现了一个方法并且其他抽象的抽象类)来解决问题,会有什么不同?或者,我定义一个带有默认方法实现的接口呢?基本上,我的问题是 - 你写道“使用接口更可取”,我想问的是 - 为什么? - Neutrino
3
所以,我认为可以这样说:通过接口定义的内容在实现时由实现接口的类决定,而抽象类中的内容则是“核心”内容,是继承该类的子类所共有的;也就是说,它不会改变。 - orrymr
6
尽管Java允许您实现多个接口,每个接口都提供函数的默认实现,但您仍然只能扩展一个类。因此,对于那些想要使用它以及其他接口的人来说,使用接口可以提供更多的灵活性。 - thecoshman
3
抱歉评论晚了,但我刚刚偶然发现了这个帖子。您也可以将类继承视为IS-A关系,而接口表示“具有某种功能”。 - Alexander Jank
显示剩余5条评论

225

世间没有完美的事物。他们可能希望更多的是实际操作方法。

但在您的解释之后,您可以使用略微不同的方法添加这些行。

  1. 接口是规则(规则是因为必须给它们提供实现,不能忽略或避免,以便像规则一样强制执行),在软件开发中各个团队之间作为共同理解文件起作用。

  2. 接口提供了要做什么的想法,但不涉及如何做。因此,实现完全取决于开发人员通过遵循给定方法签名来遵循给定规则。

  3. 抽象类可以包含抽象声明、具体实现或两者兼而有之。

  4. 抽象声明就像要遵循的规则,而具体实现则像指南(您可以按原样使用它,也可以通过覆盖并提供自己的实现来忽略它)。

  5. 此外,与相同签名的方法可能会在不同上下文中改变行为的方法被提供为接口声明,作为规则在不同上下文中进行实现。

编辑:Java 8使在接口中定义默认和静态方法成为可能。

public interface SomeInterfaceOne {

    void usualAbstractMethod(String inputString);

    default void defaultMethod(String inputString){
        System.out.println("Inside SomeInterfaceOne defaultMethod::"+inputString);
    }
}

现在,当一个类实现了SomeInterface接口时,它不必为接口的默认方法提供实现。

如果我们有另一个接口,其中包含以下方法:

public interface SomeInterfaceTwo {

    void usualAbstractMethod(String inputString);

    default void defaultMethod(String inputString){
        System.out.println("Inside SomeInterfaceTwo defaultMethod::"+inputString);
    }

}
Java不允许扩展多个类,因为这会导致“钻石问题”,即编译器无法决定使用哪个超类方法。使用默认方法时,接口也会出现钻石问题,因为如果一个类同时实现了两个接口。
SomeInterfaceOne and SomeInterfaceTwo

如果一个类实现了两个接口,而且这两个接口都定义了同名的默认方法,编译器无法决定选择哪个。因此,在Java 8中,强制要求实现不同接口之间的共同默认方法。如果某个类实现了上述两个接口,则必须提供defaultMethod()方法的实现,否则编译器将会抛出编译期错误。


11
+1,这确实是一个很好的答案,可以避免混淆。但我没有看到任何链接,也不知道为什么你引用了那些有价值的话。如果可能的话,请将它们制作成要点。 - Suresh Atta
请阅读我上面关于使用接口模拟多重继承以及使用接口作为不同家族类的基类型的注释。我认为面试官想要从受访者那里听到这种答案。 - Francisco Goldenstein
你的评论也指出了接口的一个很好的使用示例。我写下了我日常工作时的感受。这些话可能不够专业或准确,但这是我在日常编码中与抽象类和接口密切合作后所了解到的。 - Shailesh Saxena
  1. 具体实现也是规则,具有默认实现。
- Luten
@Luten:据我所知,如果你可以毫无问题地避免/忽略一条规则,那么它必须是一个指南而不是一条规则。如果我错了,请纠正我。 - Shailesh Saxena
@ShaileshSaxena,在接口中,你总是避免/忽略接口实现-因为根本没有实现。在抽象类中,你不能避免/忽略类接口-这是一个规则。抽象类中的具体实现也是规则-因为它们包含接口(规则)和实现。但也许我对词汇太挑剔了。 - Luten

197

您对使用和实现的实际差异做了很好的总结,但没有说到意义上的区别。

接口是实现类将具有的行为描述。实现类确保它将拥有可以在其上使用的这些方法。基本上它是一个类必须做出的契约或承诺。

抽象类是不需要重复创建的不同子类的基础共享行为。子类必须完成该行为,并具有覆盖预定义行为的选项(只要未定义为finalprivate)。

您可以在 java.util 包中找到很好的例子,其中包括像 List 这样的接口和像 AbstractList 这样的抽象类,后者已经实现了接口。 官方文档AbstractList 进行了以下描述:

此类提供了 List 接口的骨架实现,以最小化实现由“随机访问”数据存储(例如数组)支持的此接口所需的工作量。


23
这应该是答案,不是详细列表,而是表明接口和抽象类之间的根本区别的概念,不仅在Java中,而且在一般情况下也是如此。 - edc65
1
这真的很好。当然其他答案也很好。但是这告诉你一个关于“抽象”关键字的重要提示,即当编译器看到它时,他们知道以下信息是不完整的并需要实现。接口总是不完整的,但抽象类是抽象的,因为它们必须有不完整(抽象)的方法。 - user2453382

98

一个接口由单例变量(public static final)和公共抽象方法组成。通常情况下,当我们知道要做什么但不知道如何做时,我们会在实时中使用接口。

通过示例可以更好地理解这个概念:

考虑一个Payment类。可以通过许多方式进行支付,例如PayPal、信用卡等。因此,我们通常将Payment作为接口,其中包含一个makePayment()方法,而CreditCard和PayPal则是两个实现类。

public interface Payment
{
    void makePayment();//by default it is a abstract method
}
public class PayPal implements Payment
{
    public void makePayment()
    {
        //some logic for PayPal payment
        //e.g. Paypal uses username and password for payment
    }
}
public class CreditCard implements Payment
{
    public void makePayment()
    {
        //some logic for CreditCard payment
        //e.g. CreditCard uses card number, date of expiry etc...
    }
}

在上面的示例中,CreditCard和PayPal是两个实现类/策略。接口还允许我们在Java中使用多重继承的概念,而这是抽象类无法实现的。

有一些特性我们知道该怎么做,而其他特性我们知道如何执行时,我们选择抽象类。

考虑以下示例:

public abstract class Burger
{
    public void packing()
    {
        //some logic for packing a burger
    }
    public abstract void price(); //price is different for different categories of burgers
}
public class VegBerger extends Burger
{
    public void price()
    {
        //set price for a veg burger.
    }
}
public class NonVegBerger extends Burger
{
    public void price()
    {
        //set price for a non-veg burger.
    }
}

如果我们将来在给定的抽象类中添加方法(具体/抽象),那么实现类将不需要更改其代码。但是,如果我们将来在接口中添加方法,则必须为所有实现该接口的类添加实现,否则会出现编译时错误。

还有其他区别,但这些是主要的区别,可能是你的面试官所期望的。希望这有所帮助。


2
这个回答非常有道理,通过示例讲解得非常清晰,当我们在选择“接口”和“抽象类”之间时,可以很好地理解。 - Ram
“知道该做什么,但不知道如何去做”就像我们在定义一个方法时,没有进行任何实现,“void makePayment();”,而是在实现该接口的类中定义该方法的实现。 - Abdel-Raouf
1
有人能解释一下为什么这个例子中的抽象类不是一个具体类,它包含了一个打包方法,然后使用一个单独的接口和一个价格字段吗?既然我们可以同时继承和实现呢? - Bert
需要更正从“如果我们添加方法(具体/抽象)”到“如果我们添加具体方法”的内容。 - Vyshnav Ramesh Thrissur

67

1.1 抽象类和接口的区别

在IT技术中,抽象类和接口都是重要的概念。它们都可以用来定义方法,但有一些关键的区别。
抽象类可以包含具体方法的实现,而接口只能指定方法的签名。抽象类只能被单继承,而一个类可以实现多个接口。此外,抽象类可以有构造函数,而接口不能有任何构造函数。当然,这些不是所有区别的详尽列表,但是这些是最基本和最常见的。
1.1.1. Abstract classes versus interfaces in Java 8
1.1.2. Conceptual Difference:

1.2 Interface Default Methods in Java 8

1.2.1. What is Default Method?
1.2.2. ForEach method compilation error solved using Default Method
1.2.3. Default Method and Multiple Inheritance Ambiguity Problems
1.2.4. Important points about java interface default methods:

1.3 Java接口静态方法

1.3.1. Java Interface Static Method, code example, static method vs default method
1.3.2. Important points about java interface static method:

1.4 Java函数式接口



1.1.1. Java 8中的抽象类与接口

Java 8接口的变化包括在接口中添加静态方法和默认方法。在Java 8之前,我们只能在接口中声明方法。但是从Java 8开始,我们可以在接口中定义默认方法和静态方法。

引入默认方法后,接口和抽象类似乎相同。但是,在Java 8中它们仍然是不同的概念。

抽象类可以定义构造函数。它们更有结构性并且可以与状态相关联。而默认方法则只能在调用其他接口方法的情况下实现,没有对特定实现状态的引用。因此,两者用于不同的目的,并且在两者之间进行选择取决于场景上下文。

1.1.2. 概念上的区别:

抽象类对于接口的骨架(即部分)实现是有效的,但是没有匹配的接口时不应存在。

因此,当抽象类有效降低为低可见性的接口骨架实现时,缺省方法是否也会被剥夺?明确:不会!实现接口几乎总是需要一些或所有这些类构建工具,而默认方法缺少这些工具。如果某个接口不需要这些工具,那么显然它是一个特殊情况,不应使您走上岔路。

1.2 Java 8中的接口默认方法

Java 8引入了“默认方法”或(Defender方法)新功能,允许开发人员向接口添加新方法,而不会破坏这些接口的现有实现。它提供了灵活性,允许接口定义实现,在具体类未提供该方法的情况下使用作为默认值。

让我们考虑一个小例子来理解它的工作原理:

public interface OldInterface {
    public void existingMethod();
 
    default public void newDefaultMethod() {
        System.out.println("New default method"
               + " is added in interface");
    }
}

以下Java类可在Java JDK 8中成功编译:
public class OldInterfaceImpl implements OldInterface {
    public void existingMethod() {
     // existing implementation is here…
    }
}

如果您创建一个OldInterfaceImpl的实例:
OldInterfaceImpl obj = new OldInterfaceImpl ();
// print “New default method add in interface”
obj.newDefaultMethod(); 

1.2.1. 默认方法:

默认方法永远不会被设为final,不能被同步并且不能覆盖Object类的方法。它们总是公开的,这严重限制了编写简短和可重用方法的能力。

默认方法可以被提供到接口中而不影响实现类,因为它包含一个实现。如果在接口中定义每个添加的方法都有实现,则没有任何实现类受到影响。实现类可以重写接口提供的默认实现。

默认方法使得可以在不破坏旧接口的实现的情况下向现有接口添加新功能。

当我们扩展包含默认方法的接口时,可以执行以下操作:

  1. 不覆盖默认方法并继承默认方法。
  2. 像覆盖子类中的其他方法一样覆盖默认方法。
  3. 重新声明默认方法为抽象方法,强制子类覆盖它。

1.2.2. 使用默认方法解决ForEach方法编译错误

对于Java 8,JDK集合已经扩展,forEach方法已添加到整个集合(与lambda一起使用)。用传统的方式,代码如下所示:

public interface Iterable<T> {
    public void forEach(Consumer<? super T> consumer);
}

自从这个结果出现后,每个实现类都有编译错误,因此添加了一个默认方法,并需要实现,以便不改变现有的实现。
下面是带有默认方法的Iterable接口:
public interface Iterable<T> {
    public default void forEach(Consumer
                   <? super T> consumer) {
        for (T t : this) {
            consumer.accept(t);
        }
    }
}

使用相同的机制将 Stream 添加到 JDK 接口中,而不会破坏实现类。


1.2.3. 默认方法和多继承模糊问题

由于 Java 类可以实现多个接口,每个接口都可以定义具有相同方法签名的默认方法,因此继承的方法可能会发生冲突。

考虑下面的示例:

public interface InterfaceA {  
       default void defaultMethod(){  
           System.out.println("Interface A default method");  
    }  
}
 
public interface InterfaceB {
   default void defaultMethod(){
       System.out.println("Interface B default method");
   }
}
 
public class Impl implements InterfaceA, InterfaceB  {
}

上面的代码会出现以下错误而无法编译:

java: class Impl inherits unrelated defaults for defaultMethod() from types InterfaceA and InterfaceB

为了修复这个类,我们需要提供默认方法实现:
public class Impl implements InterfaceA, InterfaceB {
    public void defaultMethod(){
    }
}

此外,如果我们想调用任何超级接口提供的默认实现而不是我们自己的实现,我们可以按照以下方式进行:

public class Impl implements InterfaceA, InterfaceB {
    public void defaultMethod(){
        // existing code here..
        InterfaceA.super.defaultMethod();
    }
}

我们可以选择任何默认实现或两者都作为新方法的一部分。
1.2.4. Java接口默认方法的重要点:
1. Java接口默认方法将帮助我们扩展接口而不用担心破坏实现类。 2. Java接口默认方法弥合了接口和抽象类之间的差异。 3. Java 8接口默认方法将帮助我们避免实用程序类,例如所有集合类方法都可以在接口中提供。 4. Java接口默认方法将帮助我们删除基础实现类,我们可以提供默认实现,实现类可以选择覆盖哪个实现。 5. 引入接口默认方法的主要原因之一是增强Java 8中的Collections API以支持lambda表达式。 6. 如果层次结构中的任何类具有相同签名的方法,则默认方法变得无关紧要。默认方法不能覆盖java.lang.Object中的方法。原因很简单,因为Object是所有java类的基类。因此,即使我们在接口中定义了Object类方法作为默认方法,它也是无用的,因为始终会使用Object类方法。这就是为什么为了避免混淆,我们不能有覆盖Object类方法的默认方法。 7. Java接口默认方法也称为Defender方法或Virtual扩展方法。
资源链接:
1. 何时使用:Java 8+接口默认方法与抽象方法 2. JDK 8时代的抽象类与接口 3. 通过虚拟扩展方法进行接口演变 1.3 Java接口静态方法
1.3.1 Java接口静态方法,代码示例,静态方法与默认方法
Java接口静态方法类似于默认方法,只是我们不能在实现类中覆盖它们。这个特性帮助我们避免在实现类中出现不良实现导致的意外结果。让我们用一个简单的例子来看看这个特性。
public interface MyData {

    default void print(String str) {
        if (!isNull(str))
            System.out.println("MyData Print::" + str);
    }

    static boolean isNull(String str) {
        System.out.println("Interface Null Check");

        return str == null ? true : "".equals(str) ? true : false;
    }
}

现在让我们看一个实现类,它具有具有糟糕实现的isNull()方法。
public class MyDataImpl implements MyData {

    public boolean isNull(String str) {
        System.out.println("Impl Null Check");

        return str == null ? true : false;
    }
    
    public static void main(String args[]){
        MyDataImpl obj = new MyDataImpl();
        obj.print("");
        obj.isNull("abc");
    }
}

请注意,isNull(String str)是一个简单的类方法,它不会覆盖接口方法。例如,如果我们将@Override注释添加到isNull()方法中,将导致编译器错误。
现在当我们运行应用程序时,我们会得到以下输出:
接口空检查 实现空检查
如果我们将接口方法从静态更改为默认,则会得到以下输出:
实现空检查 MyData打印: 实现空检查
Java接口静态方法仅对接口方法可见,如果我们从MyDataImpl类中删除isNull()方法,则无法将其用于MyDataImpl对象。但是像其他静态方法一样,我们可以使用接口静态方法使用类名。例如,一个有效的语句将是:
boolean result = MyData.isNull("abc");

1.3.2. Java接口静态方法的重要点:

  1. Java接口静态方法是接口的一部分,我们不能用它来实现类对象。
  2. Java接口静态方法适用于提供实用程序方法,例如null检查、集合排序等。
  3. Java接口静态方法通过不允许实现类覆盖它们来帮助我们提供安全性。
  4. 我们不能为Object类方法定义接口静态方法,否则会出现编译器错误“此静态方法无法隐藏来自Object的实例方法”。这是因为在Java中不允许这样做,因为Object是所有类的基类,我们不能有一个类级别的静态方法和另一个具有相同签名的实例方法。
  5. 我们可以使用Java接口静态方法来删除实用程序类,例如Collections,并将它的所有静态方法移动到相应的接口中,这样就容易找到并使用了。

1.4 Java功能接口

在结束本文之前,我想简要介绍一下功能接口。具有正好一个抽象方法的接口称为功能接口。

引入了一个新的注释@FunctionalInterface来标记接口为功能接口。@FunctionalInterface注释是避免在功能接口中意外添加抽象方法的一种方式。它是可选的,但使用它是一个好习惯。

Java 8的功能接口是期待已久的和迫切需要的特性,因为它使我们能够使用lambda表达式来实例化它们。引入了一个名为java.util.function的新包,其中添加了一堆功能接口,以提供lambda表达式和方法引用的目标类型。我们将在未来的帖子中深入研究功能接口和lambda表达式。

资源位置:

  1. Java 8接口变更 - 静态方法、默认方法

10
我正好在寻找这些类型的更新答案。感谢快速回复。 - Ravindra babu
2
非常彻底,但就面试而言,我认为情况只会变得更糟!可怜的面试官只想知道这个人是否能够将他的知识应用于实际情况,而不是在该主题上的百科全书条目! - see sharper

43

除了Java 8发布后的第一条语句外,您的所有陈述都是正确的:

Java接口的方法隐含为抽象方法,不能有实现

来自文档页面

接口是引用类型,类似于类,只能包含常量、方法签名、默认方法、静态方法和嵌套类型

方法体仅存在于默认方法和静态方法中。

默认方法:

接口可以有默认方法,但与抽象类中的抽象方法不同。

默认方法使您能够向您库的接口添加新功能,并确保与针对这些接口旧版本编写的代码具有二进制兼容性。

当您扩展包含默认方法的接口时,可以执行以下操作:

  1. 完全不提及默认方法,让您的扩展接口继承默认方法。
  2. 重新声明默认方法,将其设为abstract
  3. 重新定义默认方法,重写它。

静态方法:

除了默认方法外,您还可以在接口中定义静态方法(静态方法是与其定义的类而不是任何对象相关联的方法。类的每个实例共享其静态方法。)

这使您更容易组织库中的辅助方法;

文档页面示例代码关于interface拥有staticdefault方法。

import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

使用以下准则来选择是使用接口还是抽象类。

接口:

  1. 为定义契约(最好是无状态的 - 没有变量)
  2. 将不相关的类链接起来,具有“具有”的功能。
  3. 声明公共常量变量(不可变状态

抽象类:

  1. 在多个密切相关的类之间共享代码。它建立了“is a”关系。

  2. 相关类之间共享通用状态(状态可以在具体类中修改)

相关帖子:

Interface vs Abstract Class (general OO)

Implements vs extends: When to use? What's the difference?

通过这些示例,您可以理解非相关的类可以通过接口具有功能,而相关的类通过扩展基类来改变行为


你说的“无状态合约”是什么意思?这是与接口相关的第一项。 - Maksim Dmitriev
1
可变状态的缺失。由于接口可以有常量,数据可以被改变,不像抽象类。 - Ravindra babu
1
以上声明有误。在接口中,数据不可变,与抽象类不同。 - Ravindra babu
2
这是最好的答案。它不仅涉及到Java8,而且还解释了在哪些特定情况下你会使用其中的任何一个。 - Shuklaswag
接口中的“无状态”概念是一个很好的设计。接口不能有任何状态(接口可以有常量,但它们是final/static的,因此是不可变的)。 - Kaihua

24

你的解释看起来还不错,但是可能会像从教科书上读出来的一样? :-/

更让我困扰的是,你的示例有多少实质性内容?你是否费心包含了抽象和接口之间的几乎所有差异?

个人建议查看此链接:http://mindprod.com/jgloss/interfacevsabstract.html#TABLE,其中详细列出了它们的区别。

希望它能帮助您和其他读者在未来的面试中取得更好的表现。


1
分享链接真的很棒。 - Premraj
1
你可以使用“default”关键字在Java接口中提供默认实现。 - Ogen
正如@Ogen所提到的,该表格已经过时,与默认实现(接口)单元格有关。 - hjr2000

23
许多初级开发人员犯了一个错误,认为接口、抽象类和具体类只是同一事物的轻微变化,并且仅基于技术原因选择其中之一:我需要多重继承吗?我需要一个放置通用方法的地方吗?我需要烦恼除了具体类之外的东西吗?这是错误的,在这些问题中隐藏着主要问题:"我"。当你自己编写代码时,很少考虑其他现在或将来与你的代码一起工作的开发人员。
尽管从技术角度来看,接口和抽象类看起来很相似,但它们具有完全不同的含义和目的。

摘要

  1. 接口定义了一个合同,某个实现将会为你满足

  2. 抽象类提供了默认行为你的实现可以重用。

以上两点是我面试时寻找的内容,也是足够简洁的摘要。继续阅读获取更多详细信息。

另一种摘要

  1. 接口用于定义公共API
  2. 抽象类用于内部使用,以及定义SPI

举例说明

换句话说:具体类以非常特定的方式执行实际工作。例如,ArrayList 使用连续的内存区域以紧凑的方式存储对象列表,提供了快速的随机访问、迭代和原地更改,但插入、删除和有时甚至添加操作却很慢;与此同时,LinkedList 使用双向链接节点来存储对象列表,这提供了快速的迭代、原地更改以及插入/删除/添加操作,但对于随机访问则很慢。这两种类型的列表都针对不同的用例进行了优化,你使用它们的方式很重要。当你试图从一个你正在大量交互的列表中挤出性能,并且在选择列表类型时由你来决定时,你应该仔细选择你要实例化哪个。

另一方面,列表的高级用户其实并不关心它到底是如何实现的,他们应该与这些细节隔离开来。假设Java没有暴露List接口,只有一个具体的List类,而这个类实际上就是LinkedList现在的样子。所有Java开发人员都要根据实现细节调整他们的代码:避免随机访问,添加缓存以加速访问,或者自己重新实现ArrayList,尽管那将与所有实际使用List的其他代码不兼容。那将是可怕的...但现在想象一下,Java大师们实际上意识到链表对于大多数实际用例来说很糟糕,并决定换成数组列表作为他们唯一可用的List类。这将影响世界上每一个Java程序的性能,人们不会满意。而罪魁祸首就是那些实现细节可用,并且开发人员认为这些细节是他们可以依赖的永久合同。这就是为什么隐藏实现细节并且只定义抽象契约很重要。这就是接口的目的:定义方法接受哪种输入和期望哪种输出,而不暴露所有内部细节,这可能会引诱程序员调整他们的代码以适应可能随着任何未来更新而改变的内部细节。
抽象类处于接口和具体类之间。它旨在帮助实现共享共同或乏味的代码。例如,AbstractCollection基于size为0提供了isEmpty的基本实现,使用迭代和比较提供了contains的实现,使用重复的add提供了addAll的实现等等。这使实现可以专注于区分它们之间的关键部分:如何实际存储和检索数据。
另一个视角:API与SPI
接口是低内聚度的各个代码部分之间的网关。它们允许库存在不破坏每个库用户的情况下存在和演变。它被称为应用程序编程接口,而不是应用程序编程类。在较小的规模上,它们还通过良好记录的接口将不同的模块分离,使多个开发人员能够成功地在大型项目上协作。
抽象类是高内聚度的帮助器,用于在实现接口时使用,假设某些实现细节。或者,抽象类用于定义SPI(服务提供程序接口)。
API和SPI之间的区别微妙但重要:对于API,重点在于谁使用它,而对于SPI,重点在于谁实现它。向API中添加方法很容易,所有现有API用户仍将编译通过。向SPI中添加方法很困难,因为每个服务提供者(具体实现)都必须实现新方法。如果使用接口定义SPI,则提供程序必须在SPI合同更改时发布新版本。如果改用抽象类,则可以将新方法定义为现有抽象方法的术语,或者作为空的“throw not implemented exception”存根,这将至少允许旧版本的服务实现仍能编译和运行。
Java 8和默认方法说明:
尽管Java 8为接口引入了默认方法,使得接口和抽象类之间的界限变得更加模糊,但这并不是为了使实现可以重用代码,而是为了更轻松地更改同时充当API和SPI(或错误地用于定义SPI而不是抽象类)的接口。
"书本知识":
OP答案提供的技术细节被认为是“书本知识”,因为这通常是学校和大多数关于语言的技术书籍所使用的方法:介绍一个东西是什么,而不是如何在实践中使用它,尤其是在大规模应用中。
以下是一个比喻:假设问题是:
“毕业舞会晚上租车还是租酒店房间更好?”
技术回答听起来像这样:
“嗯,在车里你可以更快地做到这一点,但在酒店房间里你可以更舒适地做到这一点。另一方面,酒店房间只在一个地方,而在车里,你可以在更多的地方做到这一点,比如说,你可以去观景点看美景,或者在汽车驶入影院中,或者许多其他地方,甚至在更多的地方。另外,酒店房间有淋浴。”
这一切都是真实的,但完全忽略了它们是两个完全不同的事物,都可以用于不同的目的,而“做某事”的方面并不是两个选项中最重要的事情。回答缺乏视角,表现出一种不成熟的思考方式,同时正确地呈现了真正的“事实”。

你是不是想说“低耦合”? - user2418306
@user2418306 不,内聚性是一个更通用的术语,包括耦合,虽然它们是近义词,但任何一个术语都可以使用。 - Sergiu Dumitriu

10

接口是一个“契约”,实现契约的类承诺实现这些方法。我写接口而不是类的例子是我在将游戏从2D升级到3D时。我必须创建一个接口来在游戏的2D和3D版本之间共享类。

package adventure;
import java.awt.*;
public interface Playable {
    public void playSound(String s);
    public Image loadPicture(String s);    
}

在不知道正在加载哪个版本的游戏的对象中,我可以根据环境实现方法,并仍然能够调用这些方法。

public class Adventure extends JFrame implements Playable

public class Dungeon3D extends SimpleApplication implements Playable

public class Main extends SimpleApplication implements AnimEventListener, ActionListener, Playable

通常在游戏世界中,世界可以是一个抽象类,在游戏中执行方法:

public abstract class World...

    public Playable owner;

    public Playable getOwner() {
        return owner;
    }

    public void setOwner(Playable owner) {
        this.owner = owner;
    }

9
以下是翻译:

那么可以这样考虑:

  • 类与抽象类之间的关系是“是一个”
  • 类与接口之间的关系是“有一个”

所以当你有一个抽象类哺乳动物、一个子类人类和一个接口驾驶时,你可以说

  • 每个人类是哺乳动物
  • 每个人类有驾驶(行为)

我的建议是,作者提到书本知识的短语,表明他想听到两者之间的语义差异(正如其他人在这里已经建议的那样)。


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