跳至主要內容

真正理解接口和抽象类

AruNi_Lu设计模式设计原则与思想约 2721 字大约 9 分钟

本文内容

前言

今天就来解决一个面试中经常被问到的问题:接口和抽象类有什么区别?

1. 抽象类是什么?

在 Java 中,被 abstract 关键字修饰的类,称为 抽象类抽象类不能被实例化(即通过 new 创建对象),只能被继承

因为抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。

抽象类除了不能被实例化之外,类的其他功能依旧存在,可以有成员变量、成员方法和构造方法。其中,方法可以有代码实现,也可以没有,没有代码实现的方法叫做抽象方法,需要使用 abstract 修饰。

如果子类继承了抽象类,则 必须实现该抽象类中的所有抽象方法,否则子类也要声明为抽象类。

总的来说,抽象类具有如下三个 特点

  • 不能被实例化,只能被继承;
  • 包含方法时,没有具体实现的方法为抽象方法;
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法,否则也要声明为抽象类。

2. 接口是什么?

在 Java 中,接口使用 interface 定义。其实,接口是一个 抽象类型,主要是 抽象方法的集合

所以,接口中的 方法默认都是隐式抽象的,即使用 public abstract 修饰。

接口也不能被实例化,但可以被实现,一个实现接口的类,必须实现接口中的所有方法,否则就要声明为抽象类。

需要注意的是,在 JDK 8 之后,有了如下改动:

  • 接口中 可以有成员变量(类变量),默认使用 public static final 修饰(只能是 public 修饰);
  • 接口中 可以有方法实现,分为如下两种:
    • default 默认方法,实现类可自行决定是否实现该方法;
    • private 私有方法(JDK 9),实现类没有权利实现该方法;
    • static 静态方法,实现类没有实现权力,可直接通过 . 调用

至于为什么后面要进行这样的改动,我认为是之前接口的定义太严格了。

如果只能定义方法,还不能有实现,这样导致如果很多实现类中的实现逻辑都一样的话,所有类都要实现一遍相同的逻辑。因此后面就干脆改进一下,可以有默认实现,实现类可以重新实现,也可以使用默认的。

类变量和私有/静态方法也一样,其实都是为了更灵活的进行编程。

示例如下:

public interface IDemo {
    // 变量,默认使用 public static final 修饰
    int publicVal = 0;

    // 普通方法,默认使用 public abstract 修饰,实现类必须实现
    void method();

    // 默认方法,实现类可自行决定是否实现
    default void defaultMethod() {
    }

    // 私有方法,实现类没有实现权力
    private void privateMethod() {
    }

    // 静态方法,实现类没有实现权力,可直接通过 . 调用
    static void staticMethod() {
    }
}

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

讲完了抽象类和接口的概念和特点,其实区别也就自然而然的出来了。

下面的特点都是一条一条对比着写的。

抽象类具有如下特点:

  • 不能被实例化,只能被继承;
  • 包含方法时,没有具体实现的方法为抽象方法;
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法,否则也要声明为抽象类;
  • 抽象类中的成员变量可以是各种类型的;
  • 一个类只可以继承一个抽象类。

接口具有如下特点:

  • 也不能被实例化,只能被实现;
  • 接口中的普通方法(没有任何修饰符)默认都是 public abstract

    JDK 8 之后,接口中也支持默认/私有/静态方法,以及类变量。

  • 实现类实现接口,必须实现所有普通方法;
  • 接口中的成员变量只能是 public static final 类型;
  • 一个类可以实现多个接口。

此外,还有一个 重要的区别,就是:

  • 抽象类其实就是类,子类继承后,两者是一种 is-a 关系,表示 类之间的关系
  • 而接口和实现类之间是一种 has-a 关系,表示 具有某些功能。因此,接口还有一种更形象的叫法,那就是 协议(contract)。

4. 抽象类和接口能解决什么?

4.1 为什么需要抽象类?

抽象类不能被实例化,只能被继承,我们知道,继承能解决代码复用问题,所以,抽象类自然也是为代码复用而生。多个子类可以继承抽象类中定义的属性和方法,避免在子类中编写重复的代码。

但是,继承本身就能够达到这个目的了,而且继承也没有规定父类一定要是抽象类,所以不使用抽象类,也能实现继承和复用,为什么还要定义个抽象类出来呢?

主要是因为使用 抽象类,可以 使代码更加优雅。具体来说,假设我们要实现多态:

  • 如果使用普通的继承,多个子类需要重写父类中的方法,所以父类中必须要定义这个方法,哪怕父类用不着这个方法,也要包含一个空实现
  • 而使用抽象类,虽然父类中也必须要定义这个方法,但是 父类中可以定义为抽象方法,不用去实现,而让子类去实现(也是 必须的)。

所以,主要的原因有下面几点:

  • 普通继承中,父类会莫名其妙的有一个 空实现的方法,影响代码的可读性,其他人在看到这个空实现的时候就会有疑问,要看继承关系才明白;而在 抽象类中的抽象方法,一看就知道是需要给子类继承重写的

  • 普通继承中,当父类的方法很多时,子类可能会忘记重写父类的方法;而使用 抽象类,则会强制要求重写

  • 普通继承中,父类可以被实例化,所以 增加了类被误用的风险;而 抽象类规定不能被实例化

    当然,这个问题也可以通过私有的构造器来解决,但是很显然,抽象类更加优雅。

4.2 为什么需要接口

抽象类更多的是为了代码复用,而 接口则更侧重于解耦接口是对行为的一种抽象,相当于一组协议或契约

调用者 只需要关注抽象的接口,不需要了解具体的实现,接口实现了约定和实现相分离,可以降低代码间的耦合度,提高代码的可扩展性。

实际上,接口比抽象类应用更广泛、也更重要,比如常常提到的 “基于接口而非实现编程”,它能极大地提高代码地灵活性、扩展性。

下面简单的讲讲为什么要 “基于接口而非实现编程”?

基于接口而非实现编程

其实这里的接口不单单只接口,可以理解为编程语言中的接口或抽象类。

实际上,这个原则的另一种说法是 “基于抽象而非实现编程”。

在软件开发中,最大的挑战之一就是需求的不断变化,这也是考验代码设计好坏的一个标准。越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。而抽象就是提高代码扩展性、灵活性、可维护性最有效的手段之一。

所以,为了做到 “基于接口而非实现编程”,我们需要做到下面几点:

  • 函数的命名不能暴露任何实现细节,要抽象为一个广泛的名字。例如,要定义一个存储图片的函数,应该定义为 upload(),而非 uploadToAliyun(),方便后续修改图片存储位置;
  • 封装具体的实现细节,只暴露简单的接口给调用者使用;
  • 为实现类定义抽象的接口。具体的实现类都依赖于统一的接口定义,遵从一致的功能协议。

但是,并不是一定要为每个类都定义接口,这条原则是为了 将接口和实现相分离,封装不稳定的实现,暴露稳定的接口。上游系统面向接口而非实现编程,不依赖不稳定的实现细节,这样 当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来 降低代码间的耦合性,提高代码的扩展性

所以,如果在业务场景中,某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那直接使用实现类就可以了。

除此之外,越是不稳定的系统,越要在代码的扩展性、维护性上下功夫。相反,如果某个系统特别稳定,在开发完之后,基本上不需要做维护,那我们就没有必要为其扩展性,投入不必要的开发时间。

5. 如何决定使用抽象类还是接口?

在实际编码过程中,什么时候应该使用抽象类?什么时候使用接口?

其实判断的标准很简单:

  • 如果要表示一种 is-a 关系,并且是为了解决 代码复用 问题时,就使用 抽象类
  • 如果要表示一种 has-a 关系,并且是为了解决 抽象 而非代码复用,就使用 接口

从类的继承层次上来看,抽象类是一种自下而上的设计思路,先有子类的代码重复,然后再抽象出上层的父类(也就是抽象类)。而 接口是一种自上而下的设计思路,一般都是先设计接口,规定该接口具有什么功能,然后再让含有该功能的类去具体实现功能逻辑

上次编辑于: