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";
}
请注意,这两个变量都是隐式的 final
和 static
. 这意味着它们是常量,不依赖于实例,并且不能更改。
如果我们试图改变变量 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.shoots
和 nemesisRaids
. 因此,输出将是:
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 接口教程,包括在何处以及在何处不使用默认方法、静态方法和私有方法。 如果你喜欢…