原文链接: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
接口包含一个合约,可以由 ArrayList
、Vector
、LinkedList
和其他类实现。
为了使用多态,我们可以简单声明变量类型为 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
在这个案例中,ArrayList
、LinkedList
和 Vector
的实现方法都不同,这是使用接口很好的场景。如果你留意到很多类都属于具有相同方法操作但行为不同的的父类,那么使用接口是一个好主意。
接下来,我们看看一些我们可以用接口实现的事情。
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";
}
注意,这两个变量都是隐式的 final
和 static
。这意味着它们是常量,不依赖于实例,并且不能被更改。
如果我们尝试更改 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
引入默认方法时,一些开发者认为它们与抽象类相同。然而,这并不正确,因为接口不能具有状态。
一个默认的方法可以实现,但是抽象方法不能。默认方法是对 Lambdas
和 streams
的伟大创新,但是我们应该谨慎使用它们。
在 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 表达式,其实现了接口中唯一的抽象方法。