接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。
这种机制在编程语言中并不通用。例如,C++对这些概念只有间接的支持。在java中存在语言关键字这个事实表明人们认为这些思想是很重要的,以至于要提供对它们的直接支持。
首先,我们将学习抽象类,它是普通的类与接口之间的一种中庸之道。尽管在构建具有某些未实现方法的类时,你的第一想法可能是创建接口,但是抽象类仍旧是用于此目的的一种重要而必须的工具。因为你不可能总是使用纯接口。
一、抽象类和抽象方法
在“乐器”的例子中,基类Instrument中的方法往往是“哑”(dummy)方法。若要调用这些方法,就会出现一些错误。这是因为Instrument类的目的是为它的所有导出类创建一个通用接口。
在那些示例中,建立这个通用接口的唯一理由是,不同的子类可以用不同的方式表示此接口。通用接口建立起一种基本形式,以此表示所有导出类的共同部分。另一种说法是将Instrument类称作抽象基类,或简称抽象类。
如果我们只有一个像Instrument这样的抽象方法,那么该类的对象几乎没有任何意义。我们创建抽象类是希望通过这个通用接口操纵一系列类。因此,Instrument只是表示了一个接口,没有具体的实现内容;因此,创建一个Instrument对象没有什么意义,并且我们可能还想阻止使用者这样做。通过让Instrument中的所有方都产生错误,就可以实现这个目的。但是这样做会将错误信息延迟到运行时才获得,并且需要在客户端进行可靠、详尽的测试。所以最好是在编译时捕获这些问题。
为此,Java提供一个叫做抽象方法的机制,这种方法是不完整的;仅有声明而没有方法体。下面是抽象方法声明所采用的语法:
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。(否则,编译器会报错。)
如果一个抽象类不完整,那么我们试图产生该类的对象时,编译器会怎样处理呢?由于为抽象类创建对象是不安全的,所以我们会从编译器那里得到一条出错信息。这样,编译器会确保类的纯粹性,我们不必担心会误用它。
如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。
我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这样做就很有用了。
Instrument类可以很容易地转化成abstract类。既然使某个类成为抽象类并不需要所有的方法都是抽象的,所以仅需将某些方法声明为抽象的即可。如下图所示:
下面是修改过的“管弦乐器”的例子,其中采用了抽象类和抽象方法:
/**
* 乐器
*/
abstract class Instrument {
private int i;
public abstract void play(Note n);
String what() {
return "Instrument";
}
// 调整
abstract void adjust();
}
class Wind extends Instrument {
public void play(Note n) {
System.out.println("Wind的play()" + n);
}
String what() {
return "Wind";
}
void adjust() {
System.out.println("Adjusting Wind");
}
}
/**
* 敲打乐器
*/
class Percussion extends Instrument {
public void play(Note n) {
System.out.println("Percussion的play()" + n);
}
String what() {
return "Percussion";
}
void adjust() {
System.out.println("Adjusting Percussion");
}
}
/**
* 弦乐器
*/
class Stringed extends Instrument {
public void play(Note n) {
System.out.println("Stringed的play()" + n);
}
String what() {
return "Stringed";
}
void adjust() {
System.out.println("Adjusting Stringed");
}
}
/**
* 管乐
*/
class Brass extends Wind {
public void play(Note n) {
System.out.println("Brass的play()" + n);
}
void adjust() {
System.out.println("Adjusting Brass");
}
}
/**
* 木管乐器
*/
class Woodwind extends Wind {
public void play(Note n) {
System.out.println("Woodwind的play()" + n);
}
String what() {
return "Woodwind";
}
}
public class Music4 {
static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for (Instrument instrument : e)
tune(instrument);
}
public static void main(String[] args) {
Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new Woodwind() };
tuneAll(orchestra);
}
}
我们可以看出,除了基类,实际上并没有什么改变。
创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样来使用它们。抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继承层次结构向上移动。
二、接口
interface关键字使抽象的概念更向前迈进了一步。abstract关键字允许人们在类中创建一个或多个没有任何定义的方法--提供了接口部分,但是没有提供任何相应的具体实现,这些实现是由此类的继承者创建的。interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现。它允许创建者确定方法名,参数列表和返回类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现。
一个接口表示:“所有实现了该特定接口的类看起来就像这样”。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。因此,接口被用来建立类与类之间的协议。
但是,interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特性。
要想创建一个接口,需要用interface关键字来替代class关键字。就像类一样,可以在interface关键字前面添加public关键字(但仅限于该接口在与其同名的文件中被定义)。如果不添加public关键字,则它只具有包访问权限,这样它就只能在同一个包内可用。接口也可以包含域,但是这些域隐式地是static和fianl的。
要让一个类遵循某个特定接口(或者是一组接口),需要使用implements关键字,它表示:“interface只是它的外貌,但是现在我要声明它是如何工作的。”除此之外,它看起来还很像继承。“乐器”示例图说明了这一点:
可以从Woodwind和Brass类中看到,一旦实现了某个接口,其实现就变成了一个普通的类,就可以按照常规方式扩展它。
可以选择在接口中显式地将方法声明为public,但即使你不这样做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为是public的;否则,它们将只能得到默认的包访问权限,这样在方法被继承的过程中,其可访问权限就降低了,这是java编译器所不允许的。
可以在修改过的Instrument的例子中看到这一点。要注意的是,在接口中的每一个方法确实都只是一个声明,这是编译器所允许的在接口中唯一能够存在的事物。此外,在Instrument中没有任何方法被声明为是public的,但是它们自动就都是public的:
/**
* 乐器
*/
interface Instrument {
int VALUE = 5;// static & final
void play(Note n);
// 调整
void adjust();
}
class Wind implements Instrument {
public void play(Note n) {
System.out.println(this + "的play()" + n);
}
public String toString() {
return "Wind";
}
public void adjust() {
System.out.println(this + "的adjust()");
}
}
/**
* 敲打乐器
*/
class Percussion implements Instrument {
public void play(Note n) {
System.out.println(this + "的play()" + n);
}
public String toString() {
return "Percussion";
}
public void adjust() {
System.out.println(this + "的adjust()");
}
}
/**
* 弦乐器
*/
class Stringed implements Instrument {
public void play(Note n) {
System.out.println(this + "的play()" + n);
}
public String toString() {
return "Stringed";
}
public void adjust() {
System.out.println(this + "的adjust()");
}
}
/**
* 管乐
*/
class Brass extends Wind {
public String toString() {
return "Brass";
}
}
/**
* 木管乐器
*/
class Woodwind extends Wind {
public String toString() {
return "Woodwind";
}
}
public class Music4 {
static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for (Instrument instrument : e)
tune(instrument);
}
public static void main(String[] args) {
Instrument[] orchestra = { new Wind(), new Percussion(), new Stringed(), new Brass(), new Woodwind() };
tuneAll(orchestra);
}
}
此实例的这个版本还有另一处改动:what()方法已经被修改为toString()方法,因为toString()的逻辑正是what()要实现的逻辑。由于toString()方法是根类Object的一部分,因此它不需要出现在接口中。
余下的代码其工作方式都是相同的。无论是将其向上转型为称为Instrument的普通类,还是称为Instrument的抽象类,或是称为Instrument接口,都不会有问题。它的行为都是相同的。事实上,你可以在tune()方法中看到,没有任何依据来证明Instrument是一个普通类、抽象类,还是一个接口。
如果本文对您有很大的帮助,还请点赞关注一下。