likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 50

原文链接:When to use abstract classes vs. interfaces in Java - 原文作者:Rafael-del-Nero

本文采用意译的方式

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

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

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

Java 中接口的本质

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

Java 中什么时候使用接口

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

public interface List<E> extends Collection<E> {

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

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

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

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

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

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

如下是对应输出:

class java.util.ArrayList
class java.util.LinkedList

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

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

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!");
  }
}

如下是上面的输入:

Challenge done!

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

Java 中的常量变量

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

public interface Challenger {

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

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

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

Challenger.number = 8;
Challenger.name = "Another Challenger";

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

Cannot assign a value to final variable 'number'
Cannot assign a value to final variable 'name'

Java 中默认的方法

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

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

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

default void forEach(Consumer<? super T> action) {
  // Code implementation here...
}

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

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

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!");
  }
}

下面是输出:

Challenger doing a challenge!

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

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

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 {  }

下面是输出:

mutated challenger

抽象类中抽象方法

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

public abstract class AbstractMethods {
  abstract void doSomething();
}

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

public abstract class AbstractMethods {
  void doSomethingElse();
}

会报编译错误,如下:

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

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

     Compilation error at line 4

选项 B

     Graw!!! 0
     Nemesis raids: 23
     Stars!!!
     Nemesis shoots: 23 and raids:1

选项 C

     Nemesis raids: 0
     Stars!!!
     Graw!!! 0
     Nemesis shoots: 23 and raids: 1

选项 D

     Nemesis raids: 0
     Stars!!!
     Graw!!! 1
     Nemesis shoots: 23 and raids:1

选项 E

	Compilation error at line 6
点击查看答案

选项 C

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

转载自:https://juejin.cn/post/7336022842855260186
评论
请登录