本章深入介绍Java的面向对象的高级特性,即接口、内部类和抽象类。接口是Java语言中非常重要的概念,Java的接口不仅可以用来实现多继承关系,也可以用来实现回调机制;在JDK l.1版本之后,引入了内部类这个概念;抽象类自身没有具体对象,需要派生出子类后再创建子类的对象。
本章主要内容
& 接口定义和使用方法
& 内部类的使用及继承
& 抽象类的概念
接口(Interface)也有人翻译为界面,是用来实现类与类之间多重继承功能的一种结构,是相对独立的完成特定功能的属性集合。
当编写的程序有两个或更多类时,而且这些类中又有一些相同的完成特定功能的属性和方法时,如果在每个类中都编写这些相同的属性和方法,不仅重复而且使用不方便。此时可以编写一个接口,这个接口包含这些相同的属性和方法,然后分别编写类来实现它,这些类都继承了接口的属性和方法。
凡是需要实现这种特定功能的类,都可以继承并使用这个接口。一个类只能直接继承一个父类,但可以同时实现若干个接口。利用接口实际上就获得了多个特殊父类的属性,即实现了多重继承。
Java的接口是用来组织程序中的各种类和调节它们之间相互关系的一种结构,在语法上与类有些相似。它定义了若干个抽象方法和常量,形成一个属性集合,该属性集合通常对应了某一组功能。接口仅是对实现某特定功能的一组对外接口和规范的定义,而这个功能的真正实现是在继承这个接口的各类中完成的。
接口是由常量和抽象方法组成的特殊类。定义一个接口与定义一个类是类似的。接口的定义包括两个部分:接口声明和接口体。Java中声明接口的语法如下:
[public] interface 接口名[extends 父接口名表] {
域类型 域名=常量值; //常量域声明
返回值 方法名(参数表); //抽象方法声明
}
接口声明中有两个部分是必需的:interface关键字和接口的名字。用public修饰的接口是公共接口,可以被所有的类和接口使用;没有public修饰符的接口则只能被同一个包中的其他类和接口使用。
像类之间可以继承一样,接口也具有继承性,子接口可继承父接口的所有属性和方法。但是,类只能继承一个父类,而接口可以继承多个父接口,在“父接口名表”中以逗号分隔所有的父接口名。图7-1给出了接口的一个实例,并给出了这个接口声明的各个部分的含义。
接口体 方法声明 常量声明 接口声明
图7-1 StockWatcher接口的各个部分及其含义
该接口定义了3个常量,它们是所监视的股票符号。这个接口还定义了valueChanged( )方法。实现这个接口的类将为这个方法提供具体的实现。
因为所有定义在接口中的常量都默认为public、static和final,所有定义在接口中的方法都默认为public和abstract,所以不需要用修饰符限定它们。
假如已经编写了一个StockMonitor类,这个类的功能是监督股票的价格。它可以执行一个方法来让其他对象注册,以便得到通知。该类允许其他类来调用它的watchStock()方法,从而知道什么时候特定的股票的价格发生改变。
public class StockMonitor{
public void watchStock(StockWatcher watcher,
String tickerSymbol, double delta)
{……}
}
这个方法的第一个参数watcher为StockWatcher对象,watcher对象所属的类必须实现StockWatcher接口;其他两个参数提供了股票的代号和观察改变的数目。当StockMonitor类检测到一个感兴趣的变化时,它就会调用watcher对象的valueChanged( )方法。watchStock( )方法要通过第一个参数的数据类型确保所有替代watcher参数的对象实现valueChanged( )方法。
通过使用接口类型作为参数,替代watcher参数的对象类可以是Applet或Thread等各种类型的类。
为了使用接口,要编写实现接口的类。如果一个类实现一个接口,那么这个类就应提供在接口中定义的所有抽象方法的具体实现。
为了声明一个类来实现某一个接口,在类的声明中要包括一条implements语句。因为Java支持接口的多继承,一个类可以实现多个接口,因此可以在implements后面列出要实现的多个接口,这些接口以逗号分隔。
【例7-1】本例是一个Applet类,它实现StockWatcher接口。
public class StockApplet extends Applet implements StockWatcher{
……
public void valueChanged(String tickerSymbol, double newValue){
if (tickerSymbol.equals(sunTicker))
{ ……}
else if (tickerSymbol.equals(oracleTicker))
{ ……}
else if (tickerSymbol.equals(ciscoTicker))
{ …… }
}
}
这个类引用了定义在StockWatcher接口中的常量,如oracleTicker、sunTicker等。因为实现接口的类继承了接口中定义的常量,所以可以使用一般的变量名字来引用常量,也可以用下面的语句的方式,在其他任何类中使用接口常量。
StockWatcher.sunTicker
StockApplet类实现StockWatcher接口,因此它应提供valueChanged方法的实现。当一个类实现一个接口中的抽象方法时,这个方法的名字和参数类型及数目必须与接口中的方法匹配。
下面归纳一下实现接口时应注意的问题。
· 在类的声明部分,用implements关键字声明该类将要实现哪些接口。
· 类在实现抽象方法时,必须用public修饰符。
· 除抽象类以外,在类的定义部分必须为接口中所有的抽象方法定义方法体,且方法首部应该与接口中的定义完全一致。
· 若实现某接口的类是抽象类,则它可以不实现该接口所有的方法。但是对于这个抽象类的任何一个非抽象子类,不允许存在未被实现的接口方法,即非抽象类中不能存在抽象方法。
定义一个接口,实际上是定义了一个新的引用数据类型。在可以使用接口类型之外的其他类型的名字(如变量声明、方法参数等)的地方,都可使用这个接口名。例如,前面在StockMonitor类中的watchStock( )方法中的第一个参数的数据类型为StockWatcher接口,只有实现StockWatcher接口的类对象可以替代watcher形参。
此外,应该注意:接口不能覆盖,即不能有多个版本。假如想在StockWatcher中增加一个汇集当前股票价格的方法,可以定义一个新的接口版本,如例7-2所示。
【例7-2】使用接口示例。
public interface StockWatcher{
final String sunTicker = "SUNW";
final String oracleTicker = "ORCL";
final String ciscoTicker = "CSCO";
void valueChanged(String tickerSymbol,double newValue);
void currentValue(String tickerSymbol,double newValue);
}
如果作了这个改变的话,实现旧版本的StockWatcher接口的所有类都将中断,因为它们没有实现这个接口的所有方法。
为了达到以上增加一个方法的目的,可以创建新的接口来继承老接口。例如,可以创建一个StockWatcher接口的子接口StockTracker:
public interface StockTracker extends StockWatcher{
void currentValue(String tickerSymbol,double newValue);
}
【例7-3】例7-2属于示意性的例子,对于说明某一个部分的语法机制很有作用。本例给出使用接口的一个完整实例。
// 接口的声明
interface Speaker{
public void speak ();
public void announce (String str);
}
// 接口的实现
class Philosopher implements Speaker{
private String philosophy;
// 初始化哲学家的哲理
public Philosopher (String philosophy) {
this.philosophy = philosophy;
}
// "唠叨"哲学家的哲理
public void speak (){
System.out.println (philosophy);
}
// 发表一个宣言
public void announce (String announcement){
System.out.println (announcement);
}
// 反复"唠叨"哲学家的哲理
public void pontificate (){
for (int count=1; count <= 5; count++)
System.out.println (philosophy);
}
}
// 接口的实现
class Dog implements Speaker{
// 发表狗的哲理
public void speak (){
System.out.println ("woof");
}
// 发表狗的哲理和宣言
public void announce (String announcement) {
System.out.println ("woof: " + announcement);
}
}
// 演示使用一个接口多态性
class Talking{
// 初始化Speaker接口的一个引用
// 先后指向两个不同类的对象,调用它们的公共方法
public static void main (String[] args) {
Speaker current;
current = new Dog();
current.speak();
current = new Philosopher ("I think, therefore I am.");
current.speak();
((Philosopher) current).pontificate();
}
}
图7-2 一个接口的完整实例的执行结果 |
另外还要指出,在这个程序最后一行,不能直接用current. pontificate( )进行调用。因为current对象属于Speaker类型,而通过Speaker接口只能请求到该接口所包含的方法的调用,所以,在上面的一个语句中可以通过current对象调用speak( )方法,而在这里必须将current进行类型转换(Cast),变成类Philosopher的对象,再调用pontificate( )方法。上述程序的执行结果如图7-2所示。
可以使用接口来引入多个类的共享常量,这样做只需要简单地声明包含变量初始化想要的值的接口就可以了。如果一个类中包含那个接口(就是说已经实现了接口时),所有的这些变量名都将作为常量看待。这与在C或C++中用头文件来创建大量的#defined常量或const声明相似。如果接口不包含方法,那么任何包含这个接口的类实际并不实现什么。这就像类在类名字空间引入这些常量作final变量。下面的例子运用了这种技术来实现一个自动的“作决策者”。
【例7-4】接口变量实例。
import java.util.Random ;
imterface ShareConstants {
int NO=0;
int YES=1;
int MAYBE=2;
int LATER=3;
int SOON=4;
int NEVER=5;
}
class Question implements SharedConstants {
Random rand = new Random( );
int ask( ) {
int prob = (int)(100*rand.nextDouble( ));
if (prob<30)
rerurn NO; //30%
else if (prob<60)
return YES; //30%
else if (prob<75)
return LATER; //15%
else if (prob<98)
return SOON; //13%
else
return NEVER; //2%
}
}
class AskMe implements ShareConstants {
static void answer (int result) {
switch (result) {
case NO:
System.out.println("No");
break ;
case YES:
System.out.println("Yes");’
break ;
case MAYBE:
System.out.println("Maybe");
break ;
case LATER:
System.out.println("Later");
break ;
case SOON:
System.out.println("Soon");
break ;
case NEVER:
System.out.println("Never");
break ;
}
}
public static void main (String args[ ] ) {
Question q = new Question ( );
answer(q.ask ( ));
answer(q.ask ( ));
answer(q.ask ( ));
answer(q.ask ( ));
}
}
注意:该程序利用了Java的一个标准类:Random。该类提供伪随机数,并包含若干个方法,通过这些方法可以获得程序所需形式的随机数。
该例中,用到了nextDouble ( )方法。它返回0.0~1.0之间的随机数。
该例中,还定义了两个类Question和AskMe。这两个类都实现了SharedConstants接口,接口中定义了NO、YES、MAYBE、SOON、LATER和NEVER变量。每个类中,代码就像自己定义或继承了它们一样直接引用了这些变量。下面是该程序的输出示例。注意每次运行结果不同。
Later
Soon
No
Yes
软件模块之间总是存在着一定的接口,通过接口形成相互调用关系。习惯上,常把调用者称为客户方,被调用者称为服务方。从调用方式上,可以把它们分为3类:同步调用、回调和异步调用,如图7-3所示,其中A表示客户方,B表示服务方。
图7-3 软件接口间的3种调用方式
同步调用是一种阻塞式调用,客户方要等待服务方执行完毕才返回,它是一种单向调用。
回调是一种双向调用模式,也就是说,服务方在被调用时也会调用客户方。
异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,服务方在收到某种信息或发生某种事件时,会主动通知客户方,即通过接口调用客户方。
同步调用是三者当中最简单的,而回调又常常是异步调用的基础。回调和异步调用的关系非常紧密,通常使用回调机制来实现异步消息的注册,通过异步调用来实现消息的通知。
对于一般的结构化语言,可以通过回调函数来实现回调。回调函数也是一个函数或过程,不过它是一个由调用方自己实现,供被调用方使用的特殊函数。
在面向对象的语言中,回调则是通过接口或抽象类来实现的。实现这种接口的类称为回调类,回调类的对象称为回调对象。
这里用一个实际例子来说明回调的概念和过程,并通过实例程序说明Java语言回调机制的实现。
客户方需要将数据对象排序,通过接口调用服务方排序器。调用时必须传递两个重要的参数给服务方接口,即需排序的数据对象和比较器。
比较器能够依据不同的排序规则对数据对象进行比较。它使用两个参数,分别代表两个可比较的数据对象o1 和o2。当数据对象o1 小于o2 时,比较器输出负值;当数据对象o1 等于o2时,比较器输出0;当数据对象o1大于o2 时,比较器输出正值。
排序器根据比较器的输出结果,对数据对象进行排序,并将排序后的数据对象返回给客户(调用者)。
不同的比较器实现了不同的排序规则,表示了不同的排序数据对象的策略,具体实现由客户方完成。客户方通过传递不同的比较器给服务方排序器,可以得到数据对象的不同的排列顺序。由于服务方排序器需要调用客户方比较器进行数据对象的比较,因此形成了回调。
在C的标准库中,qsort 函数使用了比较器(comparator)函数指针。比较器函数用来比较要排序的数据对象,通过传入不同的函数指针值,调用不同的比较器,得到不同的排列顺序。
Java是完全面向对象的语言,没有函数指针;但Java也能提供同样的功能,所实现的是接口模式的回调机制。
在java.util 包中定义的Comparator 接口用来表示比较的策略模式。每个实现接口的比较器都覆盖了接口中的compare ( )方法,实现具体的排序规则,表示了一种排序策略。从另一个角度说,每个实现了Comparator 接口的比较器,实质上都是一个回调类。不同的回调类实现了不同的排序规则,表示了不同的排序策略。所有实现Comparator 接口的回调类的实例(回调对象),其类型都是Comparator,并被作为参数传递给服务方的排序器。通过应用不同的排序规则,客户方获得数据对象不同的排列顺序。
正如可以对类进行的操作那样,也可以将接口组织成一个层次结构。当一个接口从另一个接口继承而来时,“子接口”会得到它的“超接口”中所声明的全部方法定义和常量。为了扩展某个接口,需要使用关键词extends,这正如在类定义中所作的那样。
interface Fruitlike extends Foodlike {
// ……
}
注意:与类有所不问,接口的层次结构设有与Object类的对等物;这个层次不是从任何单个点发展下来的。接口可以完全独立地存在,或者从另一个接口继承而来。
还要注意到,与类层次有所不同的是:接口层次可以多重继承。例如,单个接口可以扩展任意所需数量的接口(在定义的extends部分中使用逗号将它们分隔开),而且新的接口会包含它的所有父接口中的方法和常量。下面是一个叫做BusyInterface的接口定义,它从许多其他的接口继承而来。
public interface BusyInterface extends Runnable,Growable,Fruitlike,
observable {
//……
}
在多重继承接口中,管理方法命名冲突的规则与使用多个接口的类里的规则相同;只在返回值上有所区别的方法会导致一个编译器错误。