知识就是力量

当前位置:首页 > 技巧


Java中的抽象类和接口介绍

2022-10-07

什么时候应该选择抽象类而不是 Java 中的接口?接受挑战!了解这些 Java 语言元素之间的差异以及如何在您的程序中使用它们。

在 Java 代码中,甚至在 Java 开发工具包 (JDK) 本身中,都有大量的抽象类和接口。每个代码元素都有一个基本用途:

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

接口的性质

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

何时使用接口

接口对于[解耦代码和实现多态性非常有用。我们在JDK中可以看到的一个例子是List接口:


public interface List extends Collection {

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!

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

常量变量

另一个要记住的规则是接口只能包含常量变量。所以下面的代码就OK了:


public class Challenger {

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

}

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

如果我们尝试更改 Challenger 界面中的变量,例如,像这样:


Challenger.number = 8;

我们将触发这样的编译错误:


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();

}

试图声明一个没有实现的方法,也没有抽象关键字,像这样:


public abstract class AbstractMethods {
void doSomethingElse();
}

导致如下编译错误:


Missing method body, or declare abstract

何时使用抽象类

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

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

实践中的抽象类

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

抽象类和接口的区别

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

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

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

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

参加 Java 代码挑战赛!

让我们通过 Java 代码挑战来探索接口和抽象类之间的主要区别。下面我们提供了代码挑战,您也可以通过视频的形式观看抽象类和接口挑战

在下面的代码中,同时声明了一个接口和一个抽象类,并且在代码中使用了 lambda:


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 代码挑战展示了许多关于接口、抽象方法等的重要概念。逐行浏览代码将使我们对输出中发生的很多事情有所了解。

代码挑战的第一行包含一个用于 Zombie 接口的 lambda 表达式。请注意,在这个 lambda 中,我们添加了一个静态字段。实例字段也适用于此,但不适用于在 lambda 之外声明的局部变量。所以到目前为止这段代码编译得很好。另请注意,lambda 表达式尚未执行,因此 nemesisRaids 字段还不会递增。

此时,我们将打印 nemesisRaids 字段,该字段没有递增,因为尚未调用 lambda 表达式,只是声明了。所以这条线的输出是:


Nemesis raids: 0

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

在​​​Zombie​​​接口内部,我们使用lambda表达式声明​​​​zombie​​​​​静态Zombie​​​接口。所以当我们调用zombie shoot方法时,我们打印如下:


Stars!!!

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


Graw!!! 0

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

最后我们打印出nemesis.shoots和nemesisRaids的值。所以输出将是:

Nemesis shoots: 23 and raids: 1

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


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