前言

前面我们学习了 Java 集合中的 HashMapArrayListLinkedList

这一篇回到 Java 面向对象基础中非常经典的问题:接口和抽象类有什么区别?

这个问题在面试中非常常见,尤其是 Java 初中级面试,经常会问:

  • 什么是接口?
  • 什么是抽象类?
  • 接口和抽象类有什么区别?
  • 什么时候用接口,什么时候用抽象类?
  • 一个类可以实现多个接口吗?
  • 一个类可以继承多个抽象类吗?
  • JDK 8 之后接口有什么变化?

很多人会背:

接口是 implements,抽象类是 extends;接口可以多实现,抽象类只能单继承。

这只是其中一部分。

真正回答时,还要从设计目的、成员变量、方法、构造方法、继承关系、使用场景等角度说清楚。

一、先给结论

可以先看一张表:

对比点 接口 抽象类
关键字 interface abstract class
实现/继承 implements extends
多继承关系 一个类可以实现多个接口 一个类只能继承一个抽象类
设计目的 定义规范、能力 抽取共性、代码复用
普通成员变量 默认是 public static final 常量 可以有普通成员变量
构造方法 没有构造方法 可以有构造方法
抽象方法 可以有 可以有
普通方法 JDK 8 后可以有默认方法和静态方法 可以有普通方法
适合场景 定义行为规范 多个子类有共同属性和方法

简单说:

接口更像是一套规范,表示“能做什么”。
抽象类更像是父类模板,表示“是什么,并且具备一些共性”。

比如:

会飞:接口
动物:抽象类

鸟是动物,同时会飞。

飞机不是动物,但也会飞。

所以“会飞”更适合用接口,“动物”更适合用抽象类。

二、什么是抽象类?

抽象类使用 abstract class 定义。

如果一个类中有抽象方法,那么这个类必须定义为抽象类。

示例:

public abstract class Animal {

    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public abstract void eat();

    public void sleep() {
        System.out.println(name + "正在睡觉");
    }

    public String getName() {
        return name;
    }
}

这里:

public abstract void eat();

是抽象方法。

抽象方法只有方法声明,没有方法体。

public void sleep() {
    System.out.println(name + "正在睡觉");
}

是普通方法,有具体实现。

抽象类中既可以有抽象方法,也可以有普通方法。

三、抽象类如何使用?

抽象类不能直接创建对象。

错误写法:

Animal animal = new Animal("动物");

因为 Animal 是抽象类,不能直接实例化。

它需要由子类继承,并实现抽象方法。

public class Cat extends Animal {

    public Cat(String name) {
        super(name);
    }

    @Override
    public void eat() {
        System.out.println(getName() + "正在吃鱼");
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        Animal cat = new Cat("小猫");

        cat.eat();
        cat.sleep();
    }
}

输出:

小猫正在吃鱼
小猫正在睡觉

这里体现了抽象类的作用:

  1. 抽取共同属性:name
  2. 抽取共同方法:sleep
  3. 定义必须由子类实现的方法:eat

四、什么是接口?

接口使用 interface 定义。

示例:

public interface Flyable {
    void fly();
}

这里定义了一个“会飞”的能力。

任何类只要实现这个接口,就必须实现 fly 方法。

public class Bird implements Flyable {

    @Override
    public void fly() {
        System.out.println("鸟在飞");
    }
}

飞机也可以实现这个接口:

public class Plane implements Flyable {

    @Override
    public void fly() {
        System.out.println("飞机在飞");
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        Flyable bird = new Bird();
        Flyable plane = new Plane();

        bird.fly();
        plane.fly();
    }
}

输出:

鸟在飞
飞机在飞

可以看到,接口更强调“能力”或“规范”。

BirdPlane 类型完全不同,但它们都具备 fly 这个能力。

五、接口和抽象类的设计目的不同

这是最关键的区别。

1. 抽象类:抽取共性

抽象类更适合描述一类事物的共同特征。

比如:

public abstract class Animal {
    private String name;
    private Integer age;

    public abstract void eat();

    public void sleep() {
        System.out.println("睡觉");
    }
}

猫、狗、老虎都属于动物。

它们都有:

name
age
sleep()

但是吃东西的方式不同,所以 eat 可以定义为抽象方法。

这种情况就适合用抽象类。

2. 接口:定义规范

接口更适合描述某种能力。

比如:

public interface Payable {
    void pay();
}

支付宝支付、微信支付、银行卡支付都可以实现这个接口。

public class AliPay implements Payable {
    @Override
    public void pay() {
        System.out.println("支付宝支付");
    }
}

public class WechatPay implements Payable {
    @Override
    public void pay() {
        System.out.println("微信支付");
    }
}

这里接口定义的是支付规范,具体怎么支付由实现类决定。

六、继承关系不同

Java 是单继承的。

也就是说,一个类只能继承一个父类或抽象类。

public class Cat extends Animal {
}

但是一个类可以实现多个接口。

public class Bird extends Animal implements Flyable, Runnable {
}

这表示:

Bird 是一种 Animal
Bird 具备 Flyable 能力
Bird 具备 Runnable 能力

这也是接口非常重要的作用:

Java 类只能单继承,但可以通过多个接口扩展多种能力。

七、成员变量区别

接口中的变量默认是:

public static final

也就是常量。

比如:

public interface Config {
    int MAX_SIZE = 100;
}

等价于:

public static final int MAX_SIZE = 100;

所以接口中的变量必须赋值,不能作为普通成员变量使用。

抽象类中可以有普通成员变量:

public abstract class Animal {
    private String name;
    private Integer age;
}

这些属性可以由子类继承和使用。

所以如果你需要保存对象状态,比如姓名、年龄、创建时间,更适合使用抽象类。

八、构造方法区别

接口没有构造方法。

因为接口不是类的对象模板,不能直接实例化,也不保存普通对象状态。

抽象类可以有构造方法。

public abstract class Animal {

    private String name;

    public Animal(String name) {
        this.name = name;
    }
}

子类构造方法中可以调用父类构造方法:

public class Dog extends Animal {

    public Dog(String name) {
        super(name);
    }
}

这里说明抽象类虽然不能直接创建对象,但它仍然是一个类,可以参与子类对象的初始化过程。

九、方法区别

1. 抽象类中的方法

抽象类中可以有:

  • 抽象方法
  • 普通方法
  • 静态方法
  • final 方法
  • private 方法

比如:

public abstract class Animal {

    public abstract void eat();

    public void sleep() {
        System.out.println("睡觉");
    }

    public static void info() {
        System.out.println("动物信息");
    }
}

2. 接口中的方法

在早期 Java 中,接口中主要是抽象方法。

JDK 8 之后,接口可以有:

  • 抽象方法
  • 默认方法
  • 静态方法

示例:

public interface Flyable {

    void fly();

    default void stop() {
        System.out.println("停止飞行");
    }

    static void info() {
        System.out.println("飞行能力");
    }
}

这里:

default void stop()

是默认方法,实现类可以直接使用,也可以重写。

static void info()

是接口静态方法,可以通过接口名调用:

Flyable.info();

JDK 9 之后,接口中还可以有私有方法,用来复用接口内部逻辑。

不过初学和面试中,重点掌握 JDK 8 的默认方法和静态方法就够了。

十、接口默认方法有什么用?

JDK 8 之后接口支持默认方法,主要是为了解决接口升级问题。

假设有一个接口:

public interface UserService {
    void save();
}

已经有很多类实现了它。

现在想给接口新增一个方法:

void delete();

如果直接加抽象方法,那么所有实现类都必须立刻实现 delete,否则会编译报错。

为了兼容旧实现类,可以使用默认方法:

public interface UserService {

    void save();

    default void delete() {
        System.out.println("默认删除逻辑");
    }
}

这样旧实现类即使不重写 delete,也不会报错。

这就是默认方法的一个重要作用。

十一、实际开发中的例子

1. 抽象类适合模板复用

比如有多个导出任务,它们都有共同流程:

查询数据
生成文件
上传文件
记录日志

可以写一个抽象类:

public abstract class AbstractExportService {

    public void export() {
        List<String> data = queryData();
        String file = generateFile(data);
        upload(file);
        log();
    }

    protected abstract List<String> queryData();

    protected String generateFile(List<String> data) {
        return "导出文件";
    }

    protected void upload(String file) {
        System.out.println("上传文件:" + file);
    }

    protected void log() {
        System.out.println("记录导出日志");
    }
}

子类只需要实现不同的数据查询逻辑:

public class EmpExportService extends AbstractExportService {

    @Override
    protected List<String> queryData() {
        return Arrays.asList("员工1", "员工2");
    }
}

这种场景抽象类很合适,因为它既有共同流程,又有部分方法需要子类实现。

2. 接口适合定义能力

比如支付接口:

public interface PayService {
    void pay(BigDecimal amount);
}

不同支付方式实现它:

public class AliPayService implements PayService {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("支付宝支付:" + amount);
    }
}

public class WechatPayService implements PayService {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("微信支付:" + amount);
    }
}

调用方只依赖接口:

public class OrderService {

    private PayService payService;

    public OrderService(PayService payService) {
        this.payService = payService;
    }

    public void createOrder(BigDecimal amount) {
        payService.pay(amount);
    }
}

这样以后新增银行卡支付,只需要新增实现类,不需要大改调用方代码。

十二、什么时候用接口,什么时候用抽象类?

可以这样判断:

1. 优先考虑接口的场景

如果你想表达的是一种能力或规范,用接口。

比如:

可支付
可飞行
可序列化
可比较
可运行

这些都适合接口。

Java 中很多经典接口也是这样:

Runnable
Comparable
Serializable
List
Map

接口更适合做解耦。

2. 优先考虑抽象类的场景

如果多个类之间有明显的共同属性、共同方法,或者有一套共同流程需要复用,可以考虑抽象类。

比如:

动物都有姓名和年龄
导出任务都有共同执行流程
模板方法模式中的父类
多个服务类有共用逻辑

这种情况下抽象类更合适。

3. 实际开发中常见组合

很多时候,接口和抽象类会一起使用。

比如:

public interface Handler {
    void handle();
}

然后提供一个抽象基础类:

public abstract class AbstractHandler implements Handler {

    protected void log() {
        System.out.println("记录日志");
    }
}

具体实现类:

public class UserHandler extends AbstractHandler {

    @Override
    public void handle() {
        log();
        System.out.println("处理用户逻辑");
    }
}

这种写法很常见:

接口定义规范,抽象类提供部分通用实现。

十三、面试标准回答

如果面试官问:

接口和抽象类有什么区别?

可以这样回答:

接口使用 interface 定义,抽象类使用 abstract class 定义。一个类可以实现多个接口,但只能继承一个抽象类。

从设计目的上看,接口更偏向定义规范或能力,比如某个类具备支付、飞行、运行等能力;抽象类更偏向抽取共同属性和共同方法,适合多个子类之间有明显共性或者需要复用公共代码的场景。

从成员上看,接口中的变量默认是 public static final 常量,不能保存普通对象状态;抽象类可以有普通成员变量。接口没有构造方法,抽象类可以有构造方法。JDK 8 之后接口可以有默认方法和静态方法,但抽象类在普通方法、成员变量、构造方法等方面仍然更像一个父类模板。

实际开发中,如果是定义规范,优先使用接口;如果是抽取共性和复用代码,可以使用抽象类。有时候也会接口和抽象类一起使用,接口定义规范,抽象类提供通用实现。

十四、常见问题总结

1. 抽象类可以没有抽象方法吗?

可以。

一个类即使没有抽象方法,也可以定义为抽象类。

public abstract class BaseService {
    public void log() {
        System.out.println("记录日志");
    }
}

这样做的目的通常是不希望这个类被直接实例化,而是只作为父类使用。

2. 接口可以有普通方法吗?

JDK 8 之后,接口可以有默认方法:

default void method() {
    // 默认实现
}

也可以有静态方法:

static void method() {
    // 静态方法
}

3. 抽象类可以实现接口吗?

可以。

public abstract class AbstractService implements Service {
}

抽象类可以实现接口,但不一定要实现接口中的所有方法。

未实现的方法可以交给子类实现。

4. 接口可以继承接口吗?

可以。

而且接口可以继承多个接口。

public interface C extends A, B {
}

5. 一个类可以继承多个抽象类吗?

不可以。

Java 类是单继承,一个类只能继承一个父类。

6. 一个类可以实现多个接口吗?

可以。

public class MyClass implements A, B, C {
}

这也是接口解决多能力扩展的重要方式。

十五、实际开发建议

实际开发中可以记住下面几点:

  1. 定义规范优先用接口
  2. 抽取共同属性和公共逻辑用抽象类
  3. 不要为了用抽象类而强行设计继承关系
  4. Java 中类只能单继承,所以抽象类要谨慎使用
  5. 接口更适合解耦和扩展
  6. 抽象类更适合模板方法和公共逻辑复用
  7. JDK 8 后接口可以有默认方法,但不要把接口写得太重
  8. 很多框架中会同时使用接口和抽象类:接口定规范,抽象类给默认实现

十六、总结

这一篇主要学习了接口和抽象类的区别。

接口更强调规范和能力,一个类可以实现多个接口;抽象类更强调共性抽取和代码复用,一个类只能继承一个抽象类。

接口中的变量默认是常量,没有构造方法;抽象类可以有普通成员变量和构造方法。JDK 8 之后,接口可以有默认方法和静态方法,但抽象类在复用公共状态和公共逻辑方面依然更强。

面试回答时,不要只背 implementsextends,更重要的是讲清楚它们的设计目的:接口定义规范,抽象类抽取共性。

下一篇我们继续学习 Java 面向对象中的另一个经典问题:重载和重写的区别。

更多推荐