Java 中的抽象类和接口
抽象类和接口都是分离接口与实现的手段,而 Java 直接在语法上为两者提供了支持,很多其他 OOP 类语言是通过间接的方式实现这种概念的(如C++、python等)。
需要提前说明的一点注意是,无论是抽象类还是接口,都需要依赖继承或类似继承的方式类完成具体的实现,且通过多态进行灵活应用,所以正如在之前介绍继承和多态时所说的那样,大多数时候,不需要它们工作也能完成,必要时再考虑使用它们。
从功能上来说,抽象类和接口都是对基类的一种抽象,接口比抽象类的抽象程度更高。下面就具体介绍两者的语法机制和简单例子。
1. 抽象类
一个类只要包含至少一个 抽象方法(abstract method),那么它就是 抽象类(abstract class)。Java 通过 abstract 关键字定义抽象。抽象方法是 abstract 修饰的没有方法体的方法。所以一个简单的抽象类就应该像下面这样:
abstract class AbsClass {
abstract void absMethod();
// private abstract illegalMethod();
}
Java 从语法上阻止从抽象类直接创建对象,所以这样做不会通过编译。如果一个新类继承自某个抽象类,那么它必须实现其中的所有抽象方法,才能成为一个普通类,如果没有这样做,编译器会认为它还是一个抽象类,并提醒我们为其加上 abstract 关键字。由此也可以知道,抽象类中是可以有一般的非抽象方法的。当然,我们还可以主动地为普通类加上 abstract 关键字,这样可以阻止创建该类的对象。
抽象类也是类,所以可以像类一样的控制抽象类或其中任何成员的访问权限。但是注意, private abstract 是不符合语法的,原因也显而易见,我们不可能在继承类中为这种方法完成定义。
2. 接口
首先介绍 Java8 之前的接口定义,因为它更纯粹。接口使用 interface 关键字定义,由此得到的是一个完全只包含形式的类型,其中不存在任何实现。就像下面这样:
public interface PureInterface {
void method();
int method1();
String method2();
}
要是在这些方法前加上 abstract 是不影响什么的,但这无异于戴了斗笠又撑伞。接口的作用就是建立协议,它表示所有实现了这个接口的类都长这样。在代码中和接口打交道而不是和具体的类,这使得代码有更好的复用性,可以降低程序之间的耦合度。
在 Java8 之后,接口中允许包含 默认方法 和 静态方法,也可以包含属性,属性被隐式地声明为 static 和 final 的。
接口的访问权限有两种:public(接口名与文件名相同时) 和 默认权限(包访问)。
应用接口的方式:通过 implements 关键字使一个类遵循接口的外形,使用方式和 extends 一样。这里就不特别举例了,可以从后面的例子中看到。需要知道的是,在类中实现的接口方法必须是 public 的,这也很容易理解,所谓接口就是要展示给外部的,不然就不能称之为接口了。从语法角度来说,如果来自接口的方法不是 public 的,那么就只能是包访问权限,那么在继承时,访问权限就降低了,这是编译器不能允许的事情。
默认方法
在 Java8 之后可以使用 default 关键字可以在接口中为方法提供默认的实现,之前 default 关键字的用途只限于在 switch 语句中使用。添加默认方法不会影响到实现了该接口的那些类的使用,而且这些类也同时拥有了该默认方法。注意,只要使用了 default 关键字修饰方法,就必须实现该方法,哪怕只是定义一个空的方法体。下面给一个例子:
interface Effable {
void speak();
void sing();
void what();
default void express() {
System.out.println("Effable.express()");
}
}
class Robot implements Effable {
@Override
public void speak() {
System.out.println("Hello.");
}
// @Override
// void sing() {}; // error: Robot中的sing()无法实现Effable中的sing() 正在尝试分配更低的访问权限; 以前为public
@Override
public void sing() {
System.out.println("Ah~~Ah~~.");
}
@Override
public void what() {
System.out.println("Robot.");
}
public static void main(String[] args) {
Robot im = new Robot();
im.speak();
im.sing();
im.express();
}
}
//output
//Hello.
//Ah~~Ah~~.
//Effable.express()
可以看到,如果不使用 public 实现 idea2 方法,会出现编译错误。Theory 接口的默认方法 idea3 被直接传递给了 Electromagnetism 类,因此后者可以直接使用 idea3 方法。
在接口中定义静态方法的作用是什么呢?简而言之,为了避免重复实现单一功能。也许存在这种情况,接口中的某个方法在其每个实现的版本中的行为都没什么变化,但是按照最初的接口实现方式(Java8之前)我们依然要在接口的每个实现中重复地实现这一不变的功能,这样显得太笨拙了。于是静态方法加入到了接口中来。
静态方法
静态方法一般是作为工具方法,当它被放入接口中时,接口就可以被当成工具包类使用。这里就不再过多叙述了。
接口的多继承特性
Java 中的类只能继承自一个类(包括抽象类),但可以同时实现多个接口。多继承的出现是实现多个接口与定义默认方法共同作用的结果。带有默认方法的接口就像一个基类,组合了多个带有默认方法的接口也就得到了多个接口中的行为。但接口不能拥有一般的属性,静态属性不能向下传递,因此状态不支持多继承。结合上一个例子,我们在创建一个 Mobile 接口,让 Robot 类实现 Effable 和 Mobile 两个接口。
interface Mobile {
void move();
void stop();
void what();
default void radiate() {
System.out.println("I'm generating heat.");
}
}
class Robot implements Effable, Mobile {
@Override
public void speak() {
System.out.println("Hello.");
}
@Override
public void sing() {
System.out.println("Ah~~Ah~~.");
}
@Override
public void move() {
System.out.println("Walking.");
}
@Override
public void stop() {
System.out.println("Stand.");
}
@Override
public void what() {
System.out.println("Robot.");
}
public static void main(String[] args) {
Robot ed = new Robot();
ed.express();
ed.radiate();
}
}
注意被继承的“基类”接口中的默认方法的签名(方法名和参数列表)不能相同,否则会发生编译错误。要是碰到了这种情况,则需要通过重写解决签名冲突。而接口方法相同则不会有什么问题,在实现时它们成为了一个方法,就像上例中的 what() 方法一样。
派生类可以同时继承自非接口类和接口(只能继承一个非接口类)。如下例,考虑在前面例子的基础上增加一个基类,
class Machine {
public void operate(){}
}
class Robot extends Machine implements Effable, Mobile {
@Override
public void operate() {
System.out.println("I'm working.");
}
@Override
public void speak() {
System.out.println("Hello.");
}
@Override
public void sing() {
System.out.println("Ah~~Ah~~.");
}
@Override
public void move() {
System.out.println("Walking.");
}
@Override
public void stop() {
System.out.println("Stand.");
}
}
public class TestBench {
public static void effableTest(Effable x) {
x.speak();
x.sing();
}
public static void mobileTest(Mobile x) {
x.move();
x.stop();
}
public static void operateTest(Machine x) {
x.operate();
}
public static void main(String[] args) {
Robot ak = new Robot();
effableTest(ak);
mobileTest(ak);
operateTest(ak);
}
}
//output
//Hello.
//Ah~~Ah~~.
//Walking.
//Stand
//I'm working.
上面的 Robot 同时结合了 Machine 类和 Effable/Mobile 接口。被继承的类要放在被实现的接口前面,反过来写会出现编译错误。在 TestBench 中的三个方法分别以三种不同的类或接口作为参数,而创建的 Robot 对象 ak 可以被传入三者中的每个方法,说明了 ak 可以向上转型为每一种接口或类。这给程序设计带了了很大的灵活性。
接口的可扩展性
接口本身可以通过继承(extends)扩展为新的接口,以此添加新的方法。而且一个接口是可以继承多个接口的。如下例,
interface Inputable {
void input();
default void pressToBoot() {
System.out.println("Starting up.");
}
}
interface Outputable {
void output();
default void powerLight() {
System.out.println("Light on.");
}
}
interface Interactive extends Inputable, Outputable {
default void play() {
System.out.println("Playing.");
}
}
public class SmartPhone implements Interactive {
@Override
public void input() {
System.out.println("click");
}
@Override
public void output() {
System.out.println("display");
}
public static void main(String[] args) {
SmartPhone ph = new SmartPhone();
ph.input();
ph.output();
ph.play();
ph.pressToBoot();
ph.powerLight();
}
}
//output
//Cick
//Display
//Playing
//Starting up
//Light on
可以发现 Interactive 通过 extends 继承了一个以上的基类接口,这在类继承中是不被允许的。
3. 接口和抽象类的区别
- 使用方式上:一个类可以组合自多个接口,但只能继承自一个抽象类。
- 惯用方式:接口通常代表事物的一种性质,惯用形容词命名,如 Iterable、Serializable,抽象类通常代表事物的类别,如Machine、Organism。
- 方法:接口中的所有方法(除了默认方法)都需要被实现,抽象类中的抽象方法必须在子类中被实现。
- 属性:接口中不能包含属性(除了静态属性),抽象类可以拥有属性,就和普通类一样。
- 构造器:接口没有构造器,抽象类可以拥有构造器。
- 访问权限:接口被隐式指定为 public,抽象类可以是 public、protected 或 包访问权限的。
关于如何在抽象类和接口中进行选择的问题,可以根据它们之间的区别来作判断。在实际应用中可能更倾向于使用接口而不是抽象类,因为接口更加抽象,这使得代码之间耦合度更低、复用性更好。
存在很多使用接口的很好的设计模式,在实际应用中也很有用。但在设计程序时,最好不要一开始就使用接口进行抽象,当使用它成为必要时,再对代码进行相应的重构。一开始就添加各种抽象特性和各种间接实现,反而会增加程序的复杂性,结果就是自己觉得这很优雅,别人却看不懂你在做什么。