Java 中何时使用抽象类和接口

原文链接:When to use abstract classes vs. interfaces in Java - 原文作者:Rafael-del-Nero
本文采用意译的方式

抽象类和接口在 Java 代码中很常见,甚至在 Java 开发工具包(JDk)中也是这样。每个代码元素都有其基本的目的:

  • 接口是一种代码合约,必须由具体类来实现。
  • 抽象类和正常类相似,不同的是它可以包括抽象方法,该方法没有方法体。抽象类不能被实例化。

很多开发认为接口和抽象类很相似,但是它们实际上有着明显的区别。本文,我们来探讨它们的区别。

Java 中接口的本质

本质上,接口就是一个合约,所以它依赖于具体类实现来实现其目的。接口由于不会有状态,因此,它不能使用可变的实例变量。接口只能使用 final 变量。

Java 中什么时候使用接口

接口在解耦代码和实现多态方面非常有用。我们以 JDK 中的 List 接口为例:

java 复制代码
public interface List<E> extends Collection<E> {

  int size();
  boolean isEmpty();
  boolean add(E e);
  E remove(int index);
  void clear();
  
}

正如你看到的那样,这段代码很简洁且非常具有描述性。我们很容易看到方法签名。我们会在具体的类中实现这些接口中的方法。

List 接口包含一个合约,可以由 ArrayListVectorLinkedList 和其他类实现。

为了使用多态,我们可以简单声明变量类型为 List,然后选中任何可用的实例化。如下:

java 复制代码
List list = new ArrayList();
System.out.println(list.getClass());

List list = new LinkedList();
System.out.println(list.getClass());

如下是对应输出:

java 复制代码
class java.util.ArrayList
class java.util.LinkedList

在这个案例中,ArrayListLinkedListVector 的实现方法都不同,这是使用接口很好的场景。如果你留意到很多类都属于具有相同方法操作但行为不同的的父类,那么使用接口是一个好主意。

接下来,我们看看一些我们可以用接口实现的事情。

Java 中重写接口方法

记住这点:接口是一种类型的合约,必须通过具体的类来实现。 接口方法是隐式抽象的,并且需要一个具体类来实现。

如下:

java 复制代码
public class OverridingDemo {
  public static void main(String[] args) {
    Challenger challenger = new JavaChallenger();
    challenger.doChallenge();
  }
}

interface Challenger {
  void doChallenge();
}

class JavaChallenger implements Challenger {
  @Override
  public void doChallenge() {
    System.out.println("Challenge done!");
  }
}

如下是上面的输入:

java 复制代码
Challenge done!

请注意,接口方法是隐式抽象的。这意味着我们不需要将它们声明为抽象方法。

Java 中的常量变量

需要记住的另外一条规则是:接口只能包含常量变量。比如下面:

java 复制代码
public interface Challenger {

  int number = 7;
  String name = "Java Challenger";
  
}

注意,这两个变量都是隐式的 finalstatic。这意味着它们是常量,不依赖于实例,并且不能被更改。

如果我们尝试更改 Challenger 接口中的变量,比如:

java 复制代码
Challenger.number = 8;
Challenger.name = "Another Challenger";

我们将触发一个编译错误,如下:

java 复制代码
Cannot assign a value to final variable 'number'
Cannot assign a value to final variable 'name'

Java 中默认的方法

Java8 引入默认方法时,一些开发者认为它们与抽象类相同。然而,这并不正确,因为接口不能具有状态

一个默认的方法可以实现,但是抽象方法不能。默认方法是对 Lambdasstreams 的伟大创新,但是我们应该谨慎使用它们。

JDK 中使用默认方法的一个方法是 forEach(),它是 Iterable 接口的一部分。我们可以简单地重用 forEach 方法,而不是将代码复制到每个 Iterable 实现中,如下:

java 复制代码
default void forEach(Consumer<? super T> action) {
  // Code implementation here...
}

任何 Iterable 实现都可以使用 forEach() 方法,而不需要新的方法实现。然后,我们可以使用默认方法重用代码。

我们创建自己的默认方法:

java 复制代码
public class DefaultMethodExample {
  public static void main(String[] args) {
    Challenger challenger = new JavaChallenger();
    challenger.doChallenge();
  }
}

class JavaChallenger implements Challenger {  }

interface Challenger {
  default void doChallenge() {
    System.out.println("Challenger doing a challenge!");
  }
}

下面是输出:

java 复制代码
Challenger doing a challenge!

值得注意的是,默认方法都需要实现。一个默认方法不可能是 static

现在,我们来谈谈抽象类。

Java 中抽象类的本质

抽象类可以拥有实例变量来存储状态。这就意味着可以使用和更改实例变量。如下:

java 复制代码
public abstract class AbstractClassMutation {
  private String name = "challenger";
  
  public static void main(String[] args) {
    AbstractClassMutation abstractClassMutation = new AbstractClassImpl();
    abstractClassMutation.name = "mutated challenger";
    System.out.println(abstractClassMutation.name);
  }
}

class AbstractClassImpl extends AbstractClassMutation {  }

下面是输出:

java 复制代码
mutated challenger

抽象类中抽象方法

就像接口那样,抽象类可以有抽象方法。一个抽象方法是没有方法体的。不像接口那样,抽象类中的抽象方法必须显式声明为 abstract。比如:

java 复制代码
public abstract class AbstractMethods {
  abstract void doSomething();
}

试图声明一个没有实现的方法,并且没有使用 abstract 关键字。如下:

java 复制代码
public abstract class AbstractMethods {
  void doSomethingElse();
}

会报编译错误,如下:

java 复制代码
Missing method body, or declare abstract

Java 中什么时候使用抽象类

当需要实现可变状态时,使用抽象类是一个好主意。比如,Java Collections Framework 包含了 AbstractList 类,它使用了变量的状态。

在不需要维护类的状态的情况下,通常最好使用接口。

Java 中抽象类和接口的区别

从面向对象编程的角度来看,接口和抽象类的主要区别是接口不能有状态,而抽象类可以通过实例变量拥有状态。

另外一个关键的区别是类可以实现多个接口,但是只能继承一个抽象类 。这是基于多重继承(继承多个类)可能导致代码死锁所做的设计。Java 工程师们决定避免这种情况。

另外一个区别是接口可以被类实现或者其他接口扩展,但类只能被扩展。

还值得注意的是,lambda 表达式只能与函数式接口(即只有一个方法的接口)一起使用,而只有一个抽象方法的抽象类不能使用 lambda 表达式。

如下表一,总结了抽象类和接口之间的区别。

表一:Java 中比较接口和抽象类

接口 抽象类
只能有 final static 修饰的变量。接口不能更改其状态。 可以有不同类型的实例或者 static 变量,其可更改或者不可更改
一个类可以实现多个接口 一个类只能扩展一个抽象类
可以通过 implements 关键字实现。一个接口也可以通过 extend 关键字扩展接口 可以拥有实例可变字段、参数或者局部变量
只有函数式接口才能在 Java 中使用 lambda 特性 具有多于一个抽象方法的抽象类才能使用 lambda
不可以有 constructor 可以有 constructor
可以有抽象方法。可以拥有默认方法和静态方法(在 Java 8 中引入)。可以拥有带有实现的私有方法(在 Java 9 中引入)。 可以有不同类型的方法。

Java 代码挑战

我们通过 Java 代码挑战来探索接口和抽象类的主要区别。我们有如下的代码。在下面的代码中,接口和抽象类被声明,两者的代码都使用了 lambdas

java 复制代码
public class AbstractResidentEvilInterfaceChallenge {
  static int nemesisRaids = 0;
  public static void main(String[] args) {
    Zombie zombie = () -> System.out.println("Graw!!! " + nemesisRaids++);
    System.out.println("Nemesis raids: " + nemesisRaids);
    Nemesis nemesis = new Nemesis() { public void shoot() { shoots = 23; }};

    Zombie.zombie.shoot();
    zombie.shoot();
    nemesis.shoot();
    System.out.println("Nemesis shoots: " + nemesis.shoots +
        " and raids: " + nemesisRaids);
  }
}
interface Zombie {
  Zombie zombie = () -> System.out.println("Stars!!!");
  void shoot();
}
abstract class Nemesis implements Zombie {
   public int shoots = 5;
}

当运行代码之后,你认为会输出什么?请选择下面的选项:

选项 A

java 复制代码
     Compilation error at line 4

选项 B

java 复制代码
     Graw!!! 0
     Nemesis raids: 23
     Stars!!!
     Nemesis shoots: 23 and raids:1

选项 C

java 复制代码
     Nemesis raids: 0
     Stars!!!
     Graw!!! 0
     Nemesis shoots: 23 and raids: 1

选项 D

java 复制代码
     Nemesis raids: 0
     Stars!!!
     Graw!!! 1
     Nemesis shoots: 23 and raids:1

选项 E

java 复制代码
	Compilation error at line 6

点击查看答案

选项 C

注意:Zombie zombie = () -> System.out.println("Stars!!!"); 实现是一个 lambda 表达式,其实现了接口中唯一的抽象方法。

相关推荐
盼海12 分钟前
Python 循环:解锁编程中的重复艺术
java·开发语言·python
小菜日记^_^12 分钟前
增删改查基础项目总结
java·数据库·spring boot·后端·spring·servlet·tomcat
无尽的大道14 分钟前
Java反射机制详解:动态访问和操作对象
java·开发语言
说书客啊18 分钟前
计算机毕业设计 | SpringBoot咖啡商城 购物采买平台 后台管理软件(附源码)
java·数据库·人工智能·spring boot·后端·毕业设计·课程设计
Lyqfor18 分钟前
985研一学习日记 - 2024.11.9
java·数据结构·学习·docker·云原生·容器
MetaverseMan27 分钟前
停止的 Docker 容器占用的内存和其他资源
java·docker·eureka
羊小猪~~36 分钟前
前端入门一之DOM、获取元素、DOM核心、事件高级、操作元素、事件基础、节点操作
前端·javascript·css·vscode·html·浏览器·edge浏览器
T0uken1 小时前
【前端】Svelte:生命周期函数
前端
shenshenruoxi1 小时前
jmeter里判断返回参数是否为空
android·java·jmeter
Yz98761 小时前
Hadoop集群的高可用(HA)- (1、搭建namenode的高可用)
java·大数据·hive·hadoop·hdfs·zookeeper·big data