Java 中的抽象类与接口

抽象类和接口在 Java 代码中非常丰富,甚至在 Java 开发工具包 (JDK) 本身中也是如此。 每个代码元素都有一个基本目的:

    接口是一种代码契约,必须由具体的类来实现。 抽象类类似于普通类,区别在于它们可以包含抽象方法,抽象方法是没有主体的方法。 抽象类不能被实例化。

许多开发人员认为接口和抽象类很相似,但实际上它们有很大的不同。 让我们探讨一下它们之间的主要区别。

接口的本质

从本质上讲,接口是一个契约,因此它依赖于一个实现来达到它的目的。 接口永远不能有状态,所以它不能使用可变的实例变量。 接口只能使用最终变量。

何时使用接口

接口对于解耦代码和实现多态性非常有用。 我们可以在 JDK 中看到一个示例,其中 List 界面:


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

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

您可能已经注意到,这段代码很短而且描述性很强。 我们可以很容易地看到方法签名,我们将使用它来使用具体类实现接口中的方法。

List 接口包含一个可以由 ArrayList, Vector, LinkedList和其他类。

要使用多态性,我们可以简单地声明我们的变量类型 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

在这种情况下,实现方法为 ArrayList, LinkedList, 和 Vector 都是不同的,这是使用接口的一个很好的场景。 如果您注意到许多类属于具有相同方法操作但行为不同的父类,那么使用接口是个好主意。

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

覆盖接口方法

请记住,接口是一种必须由具体类实现的契约。 接口方法是隐式抽象的,也需要具体的类实现。

这是一个例子:


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!

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

常数变量

另一个要记住的规则是接口只能包含常量变量。 因此,下面的代码是可以的:


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 8 中引入默认方法时,一些开发人员认为它们与抽象类相同。 然而,这不是真的,因为接口不能有状态。

默认方法可以有实现,而抽象方法不能。 默认方法是 lambda 和流的伟大创新的结果,但我们应该谨慎使用它们。

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


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!

关于默认方法需要注意的重要一点是每个默认方法都需要一个实现。 默认方法不能是静态的。

现在,让我们继续讨论抽象类。

抽象类的本质

抽象类可以具有带有实例变量的状态。 这意味着可以使用和改变实例变量。 这是一个例子:


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

抽象类中的抽象方法

就像接口一样,抽象类可以有抽象方法。 抽象方法是没有主体的方法。 与接口不同,抽象类中的抽象方法必须显式声明为抽象的。 这是一个例子:


public abstract class AbstractMethods {

  abstract void doSomething();

}

试图声明一个没有实现的方法,并且没有 abstract 关键字,像这样:


public abstract class AbstractMethods {
   void doSomethingElse();
}

导致编译错误,如下所示:


Missing method body, or declare abstract

何时使用抽象类

当您需要实现可变状态时,使用抽象类是个好主意。 例如,Java 集合框架包括使用变量状态的 AbstractList 类。

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

实践中的抽象类

设计模式模板方法是使用抽象类的一个很好的例子。 模板方法模式在具体方法中操作实例变量。

抽象类和接口的区别

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

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

另一个区别是接口可以由类实现,也可以由接口扩展,而类只能扩展。

同样重要的是要注意,lambda 表达式只能与功能接口(意味着只有一种方法的接口)一起使用,而只有一种抽象方法的抽象类不能使用 lambda。

表 1 总结了抽象类和接口之间的区别。

表 1. 比较接口和抽象类

接口

抽象类

只能有最终静态变量。 接口永远不能改变它自己的状态。

可以有任何类型的实例或静态变量,可变的或不可变的。

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

一个类只能扩展一个抽象类。

可以用 implements 关键词。 接口还可以 extend 接口。

只能延长。

只能对方法使用静态最终字段、参数或局部变量。

可以有实例可变字段、参数或局部变量。

只有函数式接口可以使用 Java 中的 lambda 特性。

只有一个抽象方法的抽象类不能使用 lambda。

不能有构造函数。

可以有构造函数。

可以有抽象方法。

可以有默认方法和静态方法(在 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

Java代码挑战视频

您是否为此挑战选择了正确的输出? 观看视频或继续阅读以找出答案。

了解接口和抽象类和方法

这个 Java 代码挑战展示了许多关于接口、抽象方法等的重要概念。 逐行执行代码将告诉我们很多关于输出中发生的事情。

代码挑战的第一行包含一个 lambda 表达式,用于 Zombie 界面。 请注意,在这个 lambda 中,我们正在递增一个静态字段。 实例字段也可以在这里工作,但在 lambda 外部声明的局部变量不会。 因此,到目前为止,代码可以正常编译。 另请注意,lambda 表达式尚未执行,因此 nemesisRaids 字段不会增加。

此时,我们将打印 nemesisRaids 字段,它不会递增,因为 lambda 表达式还没有被调用,只是被声明了。 因此,该行的输出将是:


Nemesis raids: 0

这个 Java 代码挑战中的另一个有趣的概念是我们正在使用匿名内部类。 这基本上意味着任何将实现方法的类 Nemesis 抽象类。 我们并没有真正实例化 Nemesis 抽象类,因为它实际上是一个匿名类。 另请注意,第一个具体类在扩展它们时将始终必须实现抽象方法。

在 – 的里面 Zombie 接口,我们有 zombie static Zombie 使用 lambda 表达式声明的接口。 因此,当我们调用 zombie shoot 方法,我们打印以下内容:


Stars!!!

下一行代码调用我们在开始时创建的 lambda 表达式。 因此, nemesisRaids 变量将递增。 但是,因为我们使用的是后自增运算符,所以它只会在这段代码语句之后自增。 下一个输出将是:


Graw!!! 0 

现在,我们将调用 shoot 方法来自 nemesis 这将改变它 shoots 实例变量到 23. 请注意,这部分代码演示了接口和抽象类之间的最大区别。

最后,我们打印值 nemesis.shootsnemesisRaids. 因此,输出将是:

Nemesis shoots: 23 and raids: 1

综上所述,正确的输出是选项C:


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

了解有关 Java 的更多信息

    获得更多快速代码提示:阅读 InfoWorld Java Challengers 系列中 Rafael 的所有文章。 有关在 Java 程序中使用接口的更深入介绍,请参阅 Java 101 Java 接口教程,包括在何处以及在何处不使用默认方法、静态方法和私有方法。 如果你喜欢…

阅读更多

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注