第9

本章我们讲述Java最具有革新性的两个特点:包和接口。包(package)是类的容器,用来保存划分类名空间。例如,一个包允许你创建一个名为List的类,你可以把它保存在你自己的包中而不用考虑和其他地方的某个名为List的类相冲突。包以分层方式保存并被明确的引入新的类定义。

在前面的章节你已经了解了怎样在类中定义数据接口的方法。通过运用关键字interface,Java允许你充分抽象它实现的接口。用接口,你可以定义一系列的被一个类或多个类执行的方法。接口自己不定义任何实现。尽管它们与抽象类相似,接口有一个特殊的功能:类可以实现多个接口。与之相反,类只能继承一个超类(抽象类或其他)。

包和接口是Java程序的两个基本组成。一般来说,Java源程序可以包含下面的四个内部部分的任何一个(或所有)。

· 单个接口声明(可选)

· 任意数目的引入语句(可选)

· 单个公共类声明(必须)

· 对包来说是私有的任意数目的类(可选)

其中只有一个——单个公共类声明——在前面的程序中被用到。本章将探究剩下的三个部分。

9.1 

在前面的章节,每个例题类名从相同的名称空间获得。意思是说为避免名称冲突每个类都必须用惟一的名称。看来没有管理名称空间的办法,你可能觉得不方便,因为每个单独的类都有描述性的名称。你还需要有确保你选用的类名是独特的且不和其他程序员选择的类名相冲突的方法(假想一小组程序员为用“Foobar”作类名而争斗。或者,设想整个Internet团体为谁最先为类取名为“Espresso”而争论)。感谢上帝,Java提供了把类名空间划分为更多易管理的块的机制。这种机制就是包。包既是命名机制也是可见度控制机制。你可以在包内定义类,而且在包外的代码不能访问该类。这使你的类相互之间有隐私,但不被其他世界所知。

9.1.1   定义包

创建一个包是很简单的:只要包含一个package命令作为一个Java源文件的第一句就可以了。该文件中定义的任何类将属于指定的包。package语句定义了一个存储类的名字空间。如果你省略package 语句,类名被输入一个默认的没有名称的包(这是为什么在以前你不用担心包的问题的原因)。尽管默认包对于短的例子程序很好用,但对于实际的应用程序它是不适当的。多数情况,需要为自己的代码定义一个包。

下面是package 声明的通用形式:

package pkg;

这里,pkg 是包名。例如,下面的声明创建了一个名为MyPackage的包。

package MyPackage;

Java用文件系统目录来存储包。例如,任何你声明的MyPackage中的一部分的类的.class文件被存储在一个MyPackage 目录中。记住这种情况是很重要的,目录名必须和包名严格匹配。

多个文件可以包含相同package声明。package声明仅仅指定了文件中定义的文件属于哪一个包。它不

 

拒绝其他文件的其他方法成为相同包的一部分。多数实际的包伸展到很多文件。

你可以创建包层次。为做到这点,只要将每个包名与它的上层包名用点号“.”分隔开就可以了。一个多级包的声明的通用形式如下:

package pkg1[.pkg2[.pkg3]];

包层次一定要在Java开发系统的文件系统中有所反映。例如,一个由下面语句定义的包:

package java.awt.image;

需要在你的UNIX、Windows或Macintosh文件系统的 java/awt/image, java/awt/image或 java:awt:image中分别保存。一定要仔细选用包名。你不能在没有对保存类的目录重命名的情况下重命名一个包。

9.1.2   理解类路径(CLASSPATH )

在介绍运用包的例子之前,关于类路径环境变量的简单讨论是必要的。当包从访问控制和名称-空间-冲突中解决很多问题时,在编译和运行程序时它们导致某些古怪的难点。这是因为Java编译器考虑的特定位置作为包层次的根被类路径(CLASSPATH)控制。直到现在,你在同样的未命名的默认包中保存所有的类。这样做允许你仅仅通过在命令行键入类名编译源文件和运行Java解释器,并得到结果。这种情况下它还会工作是因为默认的当前工作目录(.)通常在类路径环境变量中为Java运行时间默认定义。然而,当有包参与时,事情就不这么简单。下面来说明这个原因。

假设你在一个test包中创建了一个名为PackTest 的类。因为你的目录结构必须与包相匹配,你创建一个名为test的目录并把PackTest.java装入该目录。然后使test 成为当前目录并编译PackTest.java。这导致PackTest.class被存放在test目录下。当你试图运行PackTest时,java 解释器报告一个与“不能发现PackTest类”相似的错误消息。这是因为该类现在被保存在test包中。不再能简单用PackTest来引用。必须通过列举包层次来引用该类。引用包层次时用逗号将包名隔开。该类现在必须叫做test.PackTest。然而,如果你试图用test.PackTest,你将仍然收到一个与“不能发现test.PackTest类”相似的出错消息。

仍然收到错误消息的原因隐藏在类路径变量中。记住,类路径设置顶层类层次。问题在于当前工作目录下不存在test子目录,因为你是工作在test目录本身。

在这个问题上你有两个选择:改变目录到上一级然后用java test.PackTest,或者在类路径环境变量增加你的开发类层次结构的顶层。然后可以用java test.PackTest,Java将发现正确的.class文件。例如,如果你的源代码在目录C:/myjava下,那么设置类路径为:

.;C:/myjava;C:/java/classes

9.1.3   一个简短的包的例子

记住前面的讨论,试试下面简单的包:

// A simple package 
 package MyPack; 
 class Balance { 
  String name; 
  double bal; 
  Balance(String n, double b) { 
  
     name = n;    bal = b; } void show() {    if(bal<0)      System.out.print("--> ");    System.out.println(name + ": $" + bal); }
 

  } 
 class AccountBalance { 
  public static void main(String args[]) { 
  
     Balance current[] = new Balance[3];    current[0] = new Balance("K. J. Fielding", 123.23);    current[1] = new Balance("Will Tell", 157.02);    current[2] = new Balance("Tom Jackson", -12.33);    for(int i=0; i<3; i++) current[i].show(); }}

称该文件名为 AccountBalance.java,把它存放在MyPack目录中。

接着,编译文件。确信结果文件.class同样在MyPack 目录中。然后用下面的命令行执行AccountBalance 类:

java MyPack.AccountBalance

记住,当你执行该命令时你必须在MyPack的上级目录,或者把类路径环境变量设置成合适的值。

如上所述,AccountBalance现在是MyPack包的一部分。这意味着它不能自己执行。也就是说你不能用下面的命令行:

java AccountBalance

AccountBalance必须和它的包名一起使用。

9.2 

前面已经学习了Java的访问控制机制的很多方面和它的访问说明符。例如,你已经知道一个类的private成员仅可以被该类的其他成员访问。包增加了访问控制的另一个维度。如你所看到的,Java提供很多级别的保护以使在类、子类和包中有完善的访问控制。

类和包都是封装和容纳名称空间和变量及方法范围的方法。包就像盛装类和下级包的容器。类就像是数据和代码的容器。类是Java的最小的抽象单元。因为类和包的相互影响,Java将类成员的可见度分为四个种类:

· 相同包中的子类

· 相同包中的非子类

· 不同包中的子类

· 既不在相同包又不在相同子类中的类

三个访问控制符,private、public和protected,提供了多种方法来产生这些种类所需访问的多个级别,表9-1总结了它们之间的相互作用。

表9-1  类成员访问

 

Private 成员

默认的成员

Protected 成员

Public 成员

同一类中可见

同一个包中对子类可见

同一个包中对非子类可见

不同包中对子类可见

不同的包中对非子类可见

 

Java的访问控制机制看上去很复杂,我们可以按下面方法简化它。任何声明为public的内容可以被从任何地方访问。被声明成private的成员不能被该类外看到。如果一个成员不含有一个明确的访问说明,它对于子类或该包中的其他类是可见的。这是默认访问。如果你希望一个元素在当前包外可见,但仅仅是元素所在类的子类直接可见,把元素定义成protected。

表9-1仅适用于类成员。一个类只可能有两个访问级别:默认的或是公共的。如果一个类声明成public,它可以被任何其他代码访问。如果该类默认访问控制符,它仅可以被相同包中的其他代码访问。

9.2.1   一个访问的例子

下面的例子显示了访问修饰符的所有组合。该例有两个包和五个类。记住这两个不同包中的类需要被存储在以它们的包p1、p2命名的目录下。

第一个包定义了三个类: Protection, Derived, 和 SamePackage。第一个类以合法的保护模式定义了四个int 变量。变量n声明成默认受保护型。n_pri是private型,n_pro是protected,n_pub是public的。

该例中每一个后来的类试图访问该类一个实例中的变量。根据访问权限不编译的行用单行注释//。在每个这样的行之前都是列举该级保护将允许访问的地点的注释。

第二个类,Derived是同样包p1中Protection类的子类。这允许Derived访问Protection中的除n_pri以外的所有变量,因为它是private。第三个类,SamePackage,不是Protection的子类,但是在相同的包中,也可以访问除n_pri以外的所有变量。

下面是Protection.java文件:

package p1; 
 public class Protection { 
  int n = 1; 
  private int n_pri = 2; 
  protected int n_pro = 3; 
  public int n_pub = 4; 
  public Protection() { 
  
     System.out.println("base constructor");    System.out.println("n = " + n);    System.out.println("n_pri = " + n_pri);    System.out.println("n_pro = " + n_pro);    System.out.println("n_pub = " + n_pub); }}

下面是Derived.java文件:

package p1; 
 class Derived extends Protection { 
  Derived() { 
  
     System.out.println("derived constructor");    System.out.println("n = " + n);// class only// System.out.println("n_pri = " + n_pri);    System.out.println("n_pro = " + n_pro);    System.out.println("n_pub = " + n_pub); }}

下面是SamePackage.java文件:

 

package p1; 
 class SamePackage { 
  SamePackage() { 

 
 
     Protection p = new Protection();    System.out.println("same package constructor");    System.out.println("n = " + p.n);// class only// System.out.println("n_pri = " + p.n_pri);    System.out.println("n_pro = " + p.n_pro);    System.out.println("n_pub = " + p.n_pub); }}

下面是另一个包p2的源代码。p2中定义的两个类重载了另两种受访问控制影响的情况。第一个类Protection2是p1.Protection的子类。这允许访问p1.Protection中除n_pri(因为它是private的)和n之外的所有变量,n是定义成默认保护型的。记住,默认型的只能允许类中或包中的代码访问。最后,OtherPackage类只访问了一个变量n_pub,它是定义成public型的。

下面是Protection2.java文件:

package p2; 
 class Protection2 extends p1.Protection { 
  Protection2() { 
  
     System.out.println("derived other package constructor");// class or package only// System.out.println("n = " + n);// class only// System.out.println("n_pri = " + n_pri);    System.out.println("n_pro = " + n_pro);    System.out.println("n_pub = " + n_pub); }}

下面是OtherPackage.java文件:

package p2; 
 class OtherPackage { 
  OtherPackage() { 
  
     p1.Protection p = new p1.Protection();    System.out.println("other package constructor");// class or package only// System.out.println("n = " + p.n);// class only// System.out.println("n_pri = " + p.n_pri);// class, subclass or package only// System.out.println("n_pro = " + p.n_pro);    System.out.println("n_pub = " + p.n_pub); }}

如果你希望试试这两个包,下面是两个可以用的测试文件。包p1的测试文件如下:

 

// Demo package p1. 
 package p1; 
 // Instantiate the various classes in p1. 
 public class Demo { 
  public static void main(String args[]) { 
  
     Protection ob1 = new Protection();    Derived ob2 = new Derived();    SamePackage ob3 = new SamePackage(); }}

p2的测试文件如下:

// Demo package p2. 
 package p2; 

 

  // Instantiate the various classes in p2. 
 public class Demo { 
  public static void main(String args[]) { 
  
     Protection2 ob1 = new Protection2();    OtherPackage ob2 = new OtherPackage(); }} 

 
9.3

包的存在是划分不同类的好的机制,了解为什么所有Java内部的类都存在包中是很简单的。在未命名的默认包中是没有核心Java类的;所有的标准类都存储在相同的包中。既然包中的类必须包含它们的包名才能完全有效,为每个你想用的包写一个长的逗点分离的包路径名是枯燥的。因为这点,Java包含了import语句来引入特定的类甚至是整个包。一旦被引入,类可以被直呼其名的引用。import语句对于程序员是很方便的而且在技术上并不需要编写完整的Java程序。如果你在程序中将要引用若干个类,那么用import 语句将会节省很多打字时间。

在Java源程序文件中,import语句紧接着package语句(如果package 语句存在),它存在于任何类定义之前,下面是import声明的通用形式:

import pkg1[.pkg2].(classname|*);

这里,pkg1是顶层包名,pkg2是在外部包中的用逗点(.)隔离的下级包名。除非是文件系统的限制,不存在对于包层次深度的实际限制。最后,你要么指定一个清楚的类名,要么指定一个星号(*),该星号表明Java编译器应该引入整个包。下面的代码段显示了所用的两种形式:

import java.util.Date; 

 

  import java.io.*;

警告:星号形式可能会增加编译时间——特别是在你引入多个大包时。因为这个原因,明确的命名你想要用到的类而不是引入整个包是一个好的方法。然而,星号形式对运行时间性能和类的大小绝对没有影响。

所有Java包含的标准Java类都存储在名为java的包中。基本语言功能被存储在java包中的java.lang包中。通常,你必须引入你所要用到的每个包或类,但是,既然Java在没有java.lang中的很多函数时是无用的,因此通过编译器为所有程序隐式引入java.lang是有必要的。这与下面的在你所有程序开头的一行是一样的:

import java.lang.*;

如果在你用星号形式引用的两个不同包中存在具有相同类名的类,编译器将保持沉默,除非你试图

 

运用其中的一个。这种情况下,你会得到一个编译时错误并且必须明确的命名指定包中的类。

任何你用到类名的地方,你可以使用它的全名,全名包括它所有的包层次。例如,下面的程序使用了一个引入语句:

import java.util.*; 
 class MyDate extends Date { 
 }

没有import 语句的例子如下:

class MyDate extends java.util.Date { 
 }

如表9-1种所示,当一个包被引入,仅仅是该包中声明成public的项目可以在引入代码中对非子类可用。例如,如果你希望前面显示的MyPack 包中的Balance类在MyPack外可以被独立的类运用,那么你需要声明它为public型,并把它存在自己的文件中,如下:

package MyPack; 

 

  /* Now, the Balance class, its constructor, and its 
  
    show() method are public. This means that they can   be used by non-subclass code outside their package.*/public class Balance { String name; double bal; public Balance(String n, double b) {    name = n;    bal = b;  } public void show() {    if(bal<0)       System.out.print("--> ");    System.out.println(name + ": $" + bal); }}

如你所见,Balance类现在是public。而且,它的构造函数和show( )方法也是public。这意味着它们可以被任何类型的MyPack包之外的代码访问。例如下面TestBalance引入了MyPack,那么它可以利用Balance类:

import MyPack.*; 
 class TestBalance { 
  public static void main(String args[]) { 
  
     /* Because Balance is public, you may use Balance       class and call its constructor. */    Balance test = new Balance("J. J. Jaspers", 99.88);    test.show(); // you may also call show() }}

作为一个试验,从Balance 类移去public 修饰符,然后编译TestBalance,和分析得到的结论一样,将会产生错误。

 

9.4  接口(interface)

用关键字interface,你可以从类的实现中抽象一个类的接口。也就是说,用interface,你可以指定一个类必须做什么,而不是规定它如何去做。接口在语句构成上与类相似,但是它们缺少实例变量,而且它们定义的方法是不含方法体的。实际上,这意味着你可以定义不用假设它们怎样实现的接口。一旦接口被定义,任何类成员可以实现一个接口。而且,一个类可以实现多个接口。

要实现一个接口,接口定义的类必须创建完整的一套方法。然而,每个类都可以自由的决定它们自己实现的细节。通过提供interface关键字,Java允许你充分利用多态性的“一个接口,多个方法”。

接口是为支持运行时动态方法解决而设计的。通常,为使一个方法可以在类间调用,两个类都必须出现在编译时间里,以便Java编译器可以检查以确保方法特殊是兼容的。这个需求导致了一个静态的不可扩展的类环境。在一个系统中不可避免会出现这类情况,函数在类层次中越堆越高以致该机制可以为越来越多的子类可用。接口的设计避免了这个问题。它们把方法或方法系列的定义从类层次中分开。因为接口是在和类不同的层次中,与类层次无关的类实现相同的接口是可行的。这是实现接口的真正原因所在。

注意:接口增添了很多应用程序所需的功能。在一种语言例如 C++中这些应用程序通常借助于多重继承来完成。

9.4.1   接口定义

接口定义很像类定义。下面是一个接口的通用形式:

access interface name { 
        return-type method-name1(parameter-list); 
        return-type method-name2(parameter-list); 
        type final-varname1 = value; 
        type final-varname2 = value; 
        // ... 
        return-type method-nameN(parameter-list); 
        type final-varnameN = value; 
 }

这里,access要么是public,要么就没有用修饰符。当没有访问修饰符时,则是默认访问范围,而接口是包中定义的惟一的可以用于其他成员的东西。当它声明为public时,则接口可以被任何代码使用。name是接口名,它可以是任何合法的标识符。注意定义的方法没有方法体。它们以参数列表后面的分号作为结束。它们本质上是抽象方法;在接口中指定的方法没有默认的实现。每个包含接口的类必需实现所有的方法。

接口声明中可以声明变量。它们一般是final 和static型的,意思是它们的值不能通过实现类而改变。它们还必须以常量值初始化。如果接口本身定义成public ,所有方法和变量都是public的。

下面是一个接口定义的例子。它声明了一个简单的接口,该接口包含一个带单个整型参数的callback( )方法。

interface Callback { 
  void callback(int param); 
 }

9.4.2   实现接口

一旦接口被定义,一个或多个类可以实现该接口。为实现一个接口,在类定义中包括implements 子句,然后创建接口定义的方法。一个包括implements 子句的类的一般形式如下:

 

access class classname [extends superclass] 
                          [implements interface [,interface...]] { 
      // class-body 
 }

这里,access要么是public的,要么是没有修饰符的。如果一个类实现多个接口,这些接口被逗号分隔。如果一个类实现两个声明了同样方法的接口,那么相同的方法将被其中任一个接口客户使用。实现接口的方法必须声明成public。而且,实现方法的类型必须严格与接口定义中指定的类型相匹配。

下面是一个小的实现Callback接口的例子程序:

class Client implements Callback { 
  // Implement Callback's interface 
  public void callback(int p) { 

 
 
     System.out.println("callback called with " + p); }}

注意callback( )用public 访问修饰符声明。

注意:当实现一个接口方法时,它必须声明成 public。

类在实现接口时定义它自己的附加的成员,既是允许的,也是常见的。例如,下面的Client版本实现了callback( )方法,并且增加了nonIfaceMeth( )方法。

class Client implements Callback { 
  // Implement Callback’s interface 
  public void callback(int p) { 
  
     System.out.println(“callback called with “ + p); } void nonIfaceMeth() {    System.out.println(“Classes that implement interfaces “ +                       “may also define other members, too.”); }}

通过接口引用实现接口

你可以把变量定义成使用接口的对象引用而不是类的类型。任何实现了所声明接口的类的实例都可以被这样的一个变量引用。当你通过这些引用调用方法时,在实际引用接口的实例的基础上,方法被正确调用。这是接口的最显著特性之一。被执行的方法在运行时动态操作,允许在调用方法代码后创建类。调用代码在完全不知“调用者”的情况下可以通过接口来调度。这个过程和第8章描述的用超类引用来访问子类对象很相似。

警告:因为 Java中在运行时动态查询方法与通常的方法调用相比会有一个非常庞大的花费,所以在对性能要求高的代码中不应该随意的使用接口。

下面的例子通过接口引用变量调用callback( )方法:

class TestIface { 
  public static void main(String args[]) { 
  
     Callback c = new Client();    c.callback(42); }}

该程序的输出如下:

callback called with 42

 

注意变量c被定义成接口类型Callback,而且被一个Client实例赋值。尽管c可以用来访问Callback()方法,它不能访问Client类中的任何其他成员。一个接口引用变量仅仅知道被它的接口定义声明的方法。因此,c不能用来访问nonIfaceMeth( ),因为它是被Client定义的,而不是由Callback定义。

前面的例子机械的显示了一个接口引用变量怎样访问一个实现对象,它没有说明这样的引用的多态功能。为演示这个用途,首先创建Callback的第二个实现,如下:

// Another implementation of Callback. 
 class AnotherClient implements Callback { 
  // Implement Callback's interface 
  public void callback(int p) { 
  
     System.out.println("Another version of callback");    System.out.println("p squared is " + (p*p)); }}

现在,试试下面的类:

class TestIface2 { 
  public static void main(String args[]) { 
  
     Callback c = new Client();    AnotherClient ob = new AnotherClient();    c.callback(42);    c = ob; // c now refers to AnotherClient object    c.callback(42); }}

程序输出如下:

callback called with 42 Another version of callback p squared is 1764

如你所见,被调用的callback( )的形式由在运行时c引用的对象类型决定。这是一个非常简单的例子,下面你将会看到另一个例子,它更实用。

局部实现

如果一个类包含一个接口但是不完全实现接口定义的方法,那么该类必须定义成abstract型。例如:

abstract class Incomplete implements Callback { 
  int a, b; 
  void show() { 
  
     System.out.println(a + " " + b); } // ...}

这里,类Incomplete没有实现callback( )方法,必须定义成抽象类。任何继承Incomplete的类都必须实现callback( )方法或者它自己定义成abstract类。

9.4.3   应用接口

为理解接口的功能,让我们看一个更实际的例子。我们曾开发过一个名为Stack的类,该类实现了一个简单的固定大小的堆栈。然而,有很多方法可以实现堆栈。例如,堆栈的大小可以固定也可以不固定。堆栈还可以保存在数组、链表和二进制树中等。无论堆栈怎样实现,堆栈的接口保持不变。也就是说,push( )和pop( )方法定义了独立实现细节的堆栈的接口。因为堆栈的接口与它的实现是分离的,很容易定

 

义堆栈接口,而不用管每个定义实现细节。让我们看下面的两个例子。

首先,下面定义了一个整数堆栈接口,把它保存在一个IntStack.java文件中。该接口将被两个堆栈实现使用。

// Define an integer stack interface. 
 interface IntStack { 
  void push(int item); // store an item 
  int pop(); // retrieve an item 
 }

下面的程序创建了一个名为FixedStack的类,该类实现一个固定长度的整数堆栈:

// An implementation of IntStack that uses fixed storage. 
 class FixedStack implements IntStack { 
  private int stck[]; 
  private int tos; 
  // allocate and initialize stack 
  FixedStack(int size) { 
  
     stck = new int[size];    tos = -1; } // Push an item onto the stack public void push(int item) {    if(tos==stck.length-1) // use length member      System.out.println("Stack is full.");    else       stck[++tos] = item; } 

 

   // Pop an item from the stack 
  public int pop() { 
  
     if(tos < 0) {      System.out.println("Stack underflow.");      return 0;    }    else       return stck[tos--]; }}class IFTest { public static void main(String args[]) {    FixedStack mystack1 = new FixedStack(5);    FixedStack mystack2 = new FixedStack(8);    // push some numbers onto the stack    for(int i=0; i<5; i++) mystack1.push(i);    for(int i=0; i<8; i++) mystack2.push(i);    // pop those numbers off the stack    System.out.println("Stack in mystack1:");    for(int i=0; i<5; i++)        System.out.println(mystack1.pop());    System.out.println("Stack in mystack2:");    for(int i=0; i<8; i++)        System.out.println(mystack2.pop()); }}

下面是IntStack 的另一个实现。通过运用相同的接口定义IntStack 创建了一个动态堆栈。这种实现中,

 

每一个栈都以一个初始长度建造。如果初始化长度被超出,那么堆栈的大小将增加。每一次需要更多的空间,堆栈的大小成倍增长。

// Implement a "growable" stack. 
 class DynStack implements IntStack { 
  private int stck[]; 
  private int tos; 

 

   // allocate and initialize stack 
  DynStack(int size) { 
  
     stck = new int[size];    tos = -1; } // Push an item onto the stack public void push(int item) {    // if stack is full, allocate a larger stack    if(tos==stck.length-1) {      int temp[] = new int[stck.length * 2]; // double size      for(int i=0; i<stck.length; i++) temp[i] = stck[i];      stck = temp;      stck[++tos] = item;    }    else       stck[++tos] = item; } // Pop an item from the stack public int pop() {    if(tos < 0) {      System.out.println("Stack underflow.");      return 0;    }    else       return stck[tos--]; }}class IFTest2 { public static void main(String args[]) {    DynStack mystack1 = new DynStack(5);    DynStack mystack2 = new DynStack(8);    // these loops cause each stack to grow    for(int i=0; i<12; i++) mystack1.push(i);    for(int i=0; i<20; i++) mystack2.push(i);    System.out.println("Stack in mystack1:");    for(int i=0; i<12; i++)        System.out.println(mystack1.pop()); 

 
 
     System.out.println("Stack in mystack2:");    for(int i=0; i<20; i++)        System.out.println(mystack2.pop()); }}

下面的类运用了FixedStack 和DynStack 实现。它通过一个接口引用完成。意思是说对 push( ) 和 pop( )的调用在运行时解决而不是在编译时解决。

/* Create an interface variable and 
  
    access stacks through it.*/class IFTest3 {  

 

  public static void main(String args[]) { 
  
     IntStack mystack; // create an interface reference variable    DynStack ds = new DynStack(5);    FixedStack fs = new FixedStack(8);    mystack = ds; // load dynamic stack    // push some numbers onto the stack    for(int i=0; i<12; i++) mystack.push(i);    mystack = fs; // load fixed stack    for(int i=0; i<8; i++) mystack.push(i);    mystack = ds;     System.out.println("Values in dynamic stack:");    for(int i=0; i<12; i++)        System.out.println(mystack.pop());    mystack = fs;    System.out.println("Values in fixed stack:");    for(int i=0; i<8; i++)        System.out.println(mystack.pop()); }}

该程序中,mystack是IntStack接口的一个引用。因此,当它引用ds时,它使用DynStack实现所定义的push( )和pop( )方法。当它引用fs时,它使用FixedStack定义的push( )和pop( )方法。已经解释过,这些决定是在运行时做出的。通过接口引用变量获得接口的多重实现是Java完成运行时多态的最有力的方法。

9.4.4   接口中的变量

你可以使用接口来引入多个类的共享常量,这样做只需要简单的声明包含变量初始化想要的值的接口就可以了。如果你的一个类中包含那个接口(就是说当你实现了接口时),所有的这些变量名都将作为常量看待。这与在C/C++中用头文件来创建大量的 #defined 常量或const 声明相似。如果接口不包含方法,那么任何包含这样接口的类实际并不实现什么。这就像类在类名字空间引入这些常量作final变量。下面的例子运用了这种技术来实现一个自动的“作决策者”:

import java.util.Random; 
 interface SharedConstants { 
  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)      return 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 SharedConstants { 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

9.4.5   接口可以扩展

接口可以通过运用关键字extends被其他接口继承。语法与继承类是一样的。当一个类实现一个继承了另一个接口的接口时,它必须实现接口继承链表中定义的所有方法。下面是一个例子:

// One interface can extend another. 
 interface A { 
  void meth1(); 
  void meth2(); 
 } 
 // B now includes meth1() and meth2() -- it adds meth3(). 
 
 

  interface B extends A { 
  void meth3(); 
 } 
 // This class must implement all of A and B 
 class MyClass implements B { 
  public void meth1() { 
  
     System.out.println("Implement meth1()."); } public void meth2() {    System.out.println("Implement meth2()."); }  public void meth3() {    System.out.println("Implement meth3()."); }
 

  } 
 class IFExtend { 
  public static void main(String arg[]) { 
  
     MyClass ob = new MyClass();    ob.meth1();    ob.meth2();    ob.meth3(); }}

作为一个实验你也许希望移走MyClass中meth1( )的实现。这将导致编译时错误。前面讲过,任何实现接口的类必须实现该接口定义的所有方法,包括从其他接口继承的任何方法。

尽管我们在本书中包括的例子没有很频繁的用到包和接口,这两个工具是Java编程环境中的重要部分。实质上所有用Java编写的实际的程序和小应用程序都被包含在包中。一个数字也可能实现接口。因此,游刃有余的运用这些工具是非常有用的。