《Java编程思想》读书笔记:第九章 接口
目录
9.1抽象类和抽象方法
9.2接口
9.3完全解耦
9.4Java的多重继承
9.5通过继承来扩展接口
9.5.1组合接口时的名字冲突
9.6适配接口
9.7接口中的域
9.7.1初始化接口中的域
9.8嵌套接口
9.9接口与工厂
9.1抽象类和抽象方法
在第8章所有“乐器”的例子中,基类Instrument中的方法往往是“哑”(dummy)方法。若要调用这些方法,就会出现一些错误。这是因为Instrument类的目的是为它的所有导出类创建一个通用接口。
在那些示例中,建立这个通用接口的唯一理由是,不同的子类可以用不同的方式表示此接口。通用接口建立起一种基本形式,以此表示所有导出类的共同部分。另一种说法是将Instrument类称作抽象基类,或简称抽象类。
如果我们只有一个像Instrument这样的抽象类,那么该类的对象几乎没有任何意义。我们创建抽象类是希望通过这个通用接口操纵一系列类。因此,Instrument只是表示了一个接口,没有具体的实现内容,因此,创建一个Instrument对象没有什么意义,并且我们可能还想阻止使用者这样做。通过让Instrument中的所有方都产生错误,就可以实现这个目的。但是这样做会将错误信息延迟到运行时才获得,并且需要在客户端进行可靠、详尽的测试。所以最好是在编译时捕获这些问题。
为此,Java提供一个叫做抽象方法的机制,这种方法是不完整的,仅有声明而没有方法体。下面是抽象方法声明所采用的语法:
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。(否则,编译器就会报错。)
如果一个抽象类不完整,那么当我们试图产生该类的对象时,编译器会怎样处理呢?由于为抽象类创建对象是不安全的,所以我们会从编译器那里得到一条出错消息。这样,编译器会确保抽象类的纯粹性,我们不必担心会误用它。
如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。
我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这样做就很有用了。
第8章Instrument类可以很容易地转化成abstract类。既然使某个类成为抽象类并不需要所有的方法都是抽象的,所以仅需将某些方法声明为抽象的即可。如下图所示:
下面是修改过的“管炫乐器”的例子,其中采用了抽象类和抽象方法:
//interfaces/music4/Music4.java
package interfaces.music4;
import polymorphism.music.Note;
import static net.mindview.uti1.Print.*;
abstract class Instrument {
private int i;
public abstract void play (Note n) ;
public string what() { return "Instrument" ; }
public abstract void adjust();
}
class wind extends Instrument {
public void play (Note n) {
print ("wind.playo" + n);
}
public string what() { return "wind" ; }
public void adjust() {}
}
class Percussion extends Instrument {
public void play (Note n) {
print ("Percussion.play()" + n);
}
public string what() { return "Percussion" ; }
public void adjust() {}
}
class stringed extends Instrument {
public void play (Note n) {
print ("stringed.play()" + n) ;
}
public string what() {return "Stringed" ;}
public void adjust() {}
}
class Brass extends wind {
public void play (Note n) {
print ( "Brass.play() " +n);
}
public void adjust() {
print ( "Brass.adjust()");
}
}
class Woodwind extends wind {
public void play (Note n) {
print ( "woodwind.play()" + n) ;
}
public String what() { return "woodwind" ; }
}
public class Husic4() {
static void tune(Instrument i) {
//...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument [] e) {
for(Instrument i : e)
tune(i);
}
public static void main(string[]args) {
Instrument [] orchestra = {
new wind(),
new Percussiono(),
new Stringed(),
new Brass(),
new woodwind()
};
tuneAll(orchestra);
}
}
Output:
Wind.play() HIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() HIDDLE_C
我们可以看出,除了基类,实际上并没有什么改变
创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样来使用它们。抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继承层次结构向上移动
9.2接口
interface关键字使抽象的概念更向前迈进了一步。abstract关键字允许人们在类中创建一个或多个没有任何定义的方法——提供了接口部分,但是没有提供任何相应的具体实现,这些实现是由此类的继承者创建的。interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现。它允许创建者确定方法名、参数列表和返回类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现。
一个接口表示:“所有实现了该特定接口的类看起来都像这样”。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。因此,接口被用来建立类与类之间的协议。(某些面向对象编程语言使用关键字protocol来完成这一功能。)
但是,interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特性。
要想创建一个接口,需要用interface关键字来替代class关键字。就像类一样,可以在interface关键字前面添加public关键字(但仅限于该接口在与其同名的文件中被定义)。如果不添加public关键字,则它只具有包访问权限,这样它就只能在同一个包内可用。接口也可以包含域,但是这些域隐式地是static和final的。
要让一个类遵循某个特定接口(或者是一组接口),需要使用implements关键字,它表示:“interface只是它的外貌,但是现在我要声明它是如何工作的。”除此之外,它看起来还很像继承。“乐器”示例的图说明了这一点:
可以从Woodwind和Brass类中看到,一旦实现了某个接口,其实现就变成了一个普通的类,就可以按常规方式扩展它。
可以选择在接口中显式地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为是public的;否则,它们将只能得到默认的包访问权限,这样在方法被继承的过程中,其可访问权限就被降低了,这是Java编译器所不允许的。
读者可以在修改过的Instrument的例子中看到这一点。要注意的是,在接口中的每一个方法确实都只是一个声明,这是编译器所允许的在接口中唯一能够存在的事物。此外,在Instrument中没有任何方法被声明为是public的,但是它们自动就都是public的:
//:interfaces/music5/Music5.java
package interfaces.music5;
import polymorphism.music.Note;
import static net.mindview.uti1.Print.* ;
interface Instrument {
int VAiUE = 5;
void play(Note n);
void adjust();
}
class Wind implements Instrument {
public void play(Note n) {
print(this + ".play()" +n);
}
public String toString() { return "wind" ; }
public void adjust() { print(this + ".adjust()");}
}
class Percussion implements Instrument {
public void play(Note n) {
print(this + ".play() " + n);
}
public String toString() { return "Percussion" ; }
public void adjust() { print (this + ".adjust()");}
}
class Stringed implements Instrument {
public void play (Note n) {
print (this + ".play( " + n);
}
public String toString() { return "Stringed" ;}
public void adjust() { print(this + ".adjust()"); }
}
class Brass extends Wind {
public string toString() { return "Brass"; }
}
class Woodwind extends Wind {
public String toString() { return "woodwind"; }
)
public class Husic5 {
static void tune(instrument i){
//...
i.play (Note.HIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i):
}
public static void main(String[] args) {
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass (),
new Woodwind()
};
tuneA1l(orchestra) ;
}
}
Output :
Wind.play() HIDDLE_C
Percussion.play() MIDDLE__C
Stringed.play() HIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
此实例的这个版本还有另外一处改动: what()方法已经被修改为toString()方法,因为toString()的逻辑正是what()要实现的逻辑。由于toString()方法是根类Object的一部分,因此它不需要出现在接口中。
余下的代码其工作方式都是相同的。无论是将其向上转型为称为Instrument的普通类,还是称为Instrument的抽象类,或是称为Instrument的接口,都不会有问题。它的行为都是相同的。事实上,你可以在tune()方法中看到,没有任何依据来证明Instrument是一个普通类、抽象类,还是一个接口。
9.3完全解耦
只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想要将这个方法应用于不在此继承结构中的某个类,那么你就会触霉头了。接口可以在很大程度上放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。
例如,假设有一个Processor类,它有一个name()方法。另外还有一个process()方法,该方法接受输入参数,修改它的值,然后产生输出。这个类做为基类而被扩展,用来创建各种不同类型的Processor。在本例中,Processor的子类将修改String对象(注意,返回类型可以是协变类型,而非参数类型):
//:interfaces/classprocessor/Apply.java
package interfaces.classprocessor ;
import java.util.* ;
import static net.mindview.uti1.Print.* ;
class Processor {
public string name() {
return getclass().getsimpleName() ;
}
Object process(0bject input) { return input; }
}
class upcase extends Processor {
String process(0bject input) {
return ((String)input).toUppercase();
}
}
class Downcase extends Processor {
String process(object input) {
return ((String)input).toLowercase() ;
}
}
class splitter extends Processor {
String process(object input){
return Arrays.toString(((String)input).split(" "));
}
}
public class Apply {
public static void process(Processor p, 0bject s) {
print ( "Using Processor " + p.name() ;
print(p.process(s));
}
public static string s =
"Disagreement with beliefs is by definition incorrect" ;
public static void main(String[] args) {
process (new upcase().s);
process (new Downcase().s);
process(new Splitter().s);
}
}Output:
Using Processor Upcase
DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT
Using Processor Downcase
disagreement with beliefs IS by definition incorrect
Using Processor splitter
[Disagreement, with, beliefs, is, by, definition, incorrect]
Apply.process()方法可以接受任何类型的Processor,并将其应用到个Object对象上,然后打印结果。像本例这样,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里,Processor对象就是一个策略,在main()中可以看到有三种不同类型的策略应用到了String类型的s对象上。
split()方法是String类的一部分,它接受String类型的对象,并以传递进来的参数作为边界,将该String对象分隔开,然后返回一个数组String[]。它在这里被用来当作创建String数组的快捷方式。
现在假设我们发现了一组电子滤波器,它们看起来好像适用于Apply.process()方法:
//:interfaces/filters/waveform.java
package interfaces.filters;
public class waveform {
private static long counter ;
private final long id = counter++;
public String toString() { return " waveform " + id;}
}
//:interfaces/filters/Filter.java
package interfaces.filters;
public class Filter {
public string name() {
return getClass().getsimpleName();
}
public waveform process(waveform input) { return input; }
}
//:interfaces/fi1ters/LowPass.java
package interfaces.filters;
public class LowPass extends Filter {
double cutoff;
public LowPass(double cutoff) { this.cutoff = cutoff; }
public waveform process (waveform input) { return input; }
}
//:interfaces/filters/HighPass.java
package interfaces.filters;
public class HighPass extends Filter {
double cutoff;
public HighPass(double cutoff) { this.cutoff = cutoff; }
public waveform process(Waveform input) { return input; }
}
//:interfaces/filters/BandPass.java
package interfaces.filters;
public class BandPass extends Filter {
double lowcutoff,highCutoff;
public BandPass(doubie lowCut,double highcut) {
lowCutoff =lowCut ;
highcutoff = highCut;
}
public waveform process(waveform fnput) { return ihput; }
}
Filter与Processor具有相同的接口元素,但是因为它并非继承自Processor——因为Filter类的创建者压根不清楚你想要将它用作Processor——因此你不能将Filter用于Apply.process()方法,即便这样做可以正常运行。这里主要是因为Apply.process()方法和Processor之间的耦合过紧,已经超出了所需要的程度,这就使得应该复用Apply.process()的代码时,复用却被禁止了。另外还需要注意的是它们的输入和输出都是Waveform。
但是,如果Processor是一个接口,那么这些限制就会变得松动,使得你可以复用结构该接口的Apply.process()。下面是Processor和Apply的修改版本:
//:interfaces/interfaceprocessor/Processor.java
package interfaces interfaceprocessor :
public interface Processor {
string name(;
Object process(object input) ;
}
//:interfaces/interfaceprocessor/Apply.java
package interfaces interfaceprocessor ;
import static net.mindview.uti1.Print.* ;
public class Apply {
public static void process(Processor p. 0bject s) {
print("Using Processor " +p.name() ;
print(p.process(s));
}
}
复用代码的第一种方式是客户端程序员遵循该接口来编写他们自己的类,就像下面这样
//:interfaces/interfaceprocessor/StringProcessor.java
package interfaces interfaceprocessor;
import java.uti1.*;
public abstract class StringProcessor implements Processor{
public string name(){
return getc1ass().getsimpleName();
}
public abstract String process(0bject input);
public static string s =
"If she weighs the same as a duck,she's made of wood";
public static void main(String[] args){
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
Apply.process(new Splitter(), s);
}
}
class upcase extends StringProcessor {
public String process(0bject input) {
return ((string)input).toUppercase() ;
}
}
class Downcase extends StringProcessor {
public String process (0bject input) {
return ((String)input).toLowercase();
}
}
class Splitter extends StringProcessor {
public string process (0bject input) {
return Arrays.toString(((String)input).split(" "));
}
}Output :
using Processor Upcase
IF SHE WEIGHs THE' SAHE AS A DUCK, SHE 'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor splitter
[If, she, weighs, the, same, as, a. duck , she 's, made, of, wood]
但是,你经常碰到的情况是你无法修改你想要使用的类。例如,在电子滤波器的例子中,类库是被发现而非被创建的。在这些情况下,可以使用适配器设计模式。适配器中的代码将接受你所拥有的接口,并产生你所需要的接口,就像下面这样:
//: interfaces/interfaceprocessor/FilterProcessor.java
package interfaces.interfaceprocessor;
import interfaces.filters.* ;
class FilterAdapter implements Processor {
Filter filter;
public FilterAdapter(Filter filter) {
this.filter = filter;
}
public string name() { return fi1ter.name(); }
public waveform process(Object input) {
return filter.process((waveform)input) ;
}
}
public class FilterProcessor {
public static void main(String[] args) {
Waveform w=new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)), w);
Apply.process(new FilterAdapter(new HighPass(2.0)),w);
Apply.process(
new FilterAdapter(new BandPass(3.0,4.0)), w);
}
}
Output :
Using Processor LowPass
waveform 0
using Processor HighPass
waveform 0
Using Processor BandPass
waveform 0
在这种使用适配器的方式中,FilterAdapter的构造器接受你所拥有的接口Filter,然后生成具有你所需要的Processor接口的对象。你可能还注意到了,在FilterAdapter类中用到了代理。
将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性。
9.4Java的多重继承
接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这要高。因为接口是根本没有任何具体实现的——也就是说,没有任何与接口相关的存储,因此,也就无法阻止多个接口的组合。这一点是很有价值的,因为你有时需要去表示“一个x是一个a和一个b以及一个c”。在C++中,组合多个类的接口的行为被称作多重继承。它可能会使你背负很沉重的包袱,因为每个类都有一个具体实现。在Java中,你可以执行相同的行为,但是只有一个类可以有具体实现,因此,通过组合多个接口,C++中的问题是不会在Java中发生的:
在导出类中,不强制要求必须有一个是抽象的或“具体的”(没有任何抽象方法的)基类。如果要从一个非接口的类继承,那么只能从一个类去继承。其余的基元素都必须是接口。需要将所有的接口名都置于implements关键字之后,用逗号将它们一一隔开。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。下面的例子展示了一个具体类组合数个接口之后产生了一个新类:
//:interfaces/Adventure.java1
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class Actioncharacter {
public void fight() {}
}
class Hero extends ActionCharacter
implements CanFight, CanSwim. CanFly {
public void swim() {}
public void fly () {}
}
public class Adventure {
public static void t(CanFight x) { x.fight(); }
public static void u(canSwim x) { x.swim() ; }
public static void v(canFly x) { x.fly ; }
public static void w(Actioncharacter x) { x.fight(); }
public static void main(String[] args) {
Hero h = new Hero();
t(h);
u(h);
v(h);
w(h);
}
}
可以看到,Hero组合了具体类ActionCharacter和接口CanFight、CanSwim和CanFly。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放在前面,后面跟着的才是接口(否则编译器会报错)。
注意,CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的,而且,在Hero中并没有提供fight()的定义。可以扩展接口,但是得到的只是另一个接口。当想要创建对象时,所有的定义首先必须都存在。即使Hero没有显式地提供fight()的定义,其定义也因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。
在Adventure类中,可以看到有四个方法把上述各种接口和具体类作为参数。当Hero对象被创建时,它可以被传递给这些方法中的任何一个,这意味着它依次被向上转型为每一个接口。由于Java中这种设计接口的方式,使得这项工作并不需要程序员付出任何特别的努力。
一定要记住,前面的例子所展示的就是使用接口的核心原因:为了能够向上转型为多个基类型(以及由此而带来的灵活性)。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。这就带来了一个问题:我们应该使用接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该成为一个基类,那么第一选择应该是使它成为一个接口(该主题在本章的总结中将再次讨论)。
9.5通过继承来扩展接口
通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。这两种情况都可以获得新的接口,就像在下例中所看到的:
//:interfaces/HorrorShow.java
interface Honster {
void menace();
}
interface DangerousMonster extends Monster {
void destroy();
}
interface Lethal {
void kill();
}
class Dragonzilla implements DangerousHonster {
public void menace() {}
public void destroy() {}
}
interface Vampire extends DangerousHonster,Lethal {
void drinkBlood();
}
class veryBadvampire implements Vampire {
public void menace() {}
public void destroyo () {}
public void kill() {}
public void drinkB1ood() {}
}
public class HorrorShow {
static void u(Monster b){ b.menace(); }
static void v(DangerousHonster d) {
d.menace();
d.destroy();
}
static void w(Lethal l) { l.kill(); }
public static void main(String[] args){
DangerousMonster barney = new Dragonzjlla();
u(barney);
v(barney);
Vampire viad = new VeryBadVampire();
u(viad);
v(vlad);
w(vlad);
}
}
DangerousMonster是Monster的直接扩展,它产生了一个新接口。DragonZilla中实现了这个接口。
在Vampire中使用的语法仅适用于接口继承。一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口。就像所看到的,只需要用逗号将接口名一一分隔开即可。
9.5.1组合接口时的名字冲突
在实现多重继承时,可能会碰到一个小陷阱。在前面的例子中,CanFight和ActionCharacter都有一个相同的void fight()方法。这不是问题所在,因为该方法在二者中是相同的。相同的方法不会有什么问题,但是如果它们的签名或返回类型不同,又会怎么样呢?这有一个例子:
l : interfaces/ Interfacecollision.java
package interfaces;
interface I1 { void f(); }
interface I2 { int f(int i); }
interface I3 { int f(); }
class c {
pubiic int f() { return 1; }
}
class c2 implements I1,I2 {
public void f() {}
public int f(int i) { return 1; }
}
class c3 extends C implements I2 {
public int f(int i) { return 1; }
}
class c4 extends c implements I3 {
public int f() { return 1; }
}
//Hethods differ only by return type
//! class c5 extends c implements I1 {}
//! interface I4 extends I1,I3 {}
此时困难来了,因为覆盖、实现和重载令人不快地搅在了一起,而且重载方法仅通过返回类型是区分不开的。当撤销最后两行注释时,就会出现错误。
在打算组合的不同接口中使用相同的方法名通常会造成代码可读性混乱,请尽量避免这种情况。
9.6适配接口
接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。
因此,接口的一种常见用法就是前面提到的策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你主要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口。”这使得你的方法更加灵活、通用,并更具可复用性。
例如,Java SE5的Scanner类(在第13章中就更多地了解它)的构造器接受的就是一个Readable接口。你会发现Readable没有用作Java标准类库中其他任何方法的参数,它是单独为Scanner创建的,以使得Scanner不必将其参数限制为某个特定类。通过这种方式,Scanner可以作用于更多的类型。如果你创建了一个新的类,并且想让Scanner可以作用于它,那么你就应该让它成为Readable,就像下面这样:
//:interfaces/RandomWords.java
import java.nio.* ;
import java.util .* ;
public class Randomwords implements Readable {
private static Random rand = new Random(47);
private static final char[] capitals =
"ABCDEFGHIJKLMNOPQRSTUVwXYz".toCharArray () ;
private static final char[] lowers =
"abcdefghijklmnopqrstuvwxyz".tocharArray ();
private static final char[] vowels =
"aeiou".toCharArray();
private int count ;
public Randomwords(int count) { this.count = count; }
public int read (charBuffer cb){
if (count-- == 0)
return -1;
cb.append(capitals [rand. nextInt(capitals.length)]);
for ( int i = 0; i < 4;1++) {
cb.append(vowels [rand .nextInt (vowels.length) ] );
cb. append ( lowers [rand.nextInt ( lowers.length)]);
}
cb.append(" ");
return 10;
}
public static void main(String[] args) {
scanner s = new Scanner (new Randomwords (10)) ;
while(s.hasNext())
System.out.println(s.next());
}
}Output :
Yazeruyac
Fowenucor
Goeazimom
Raeuuacio
Nuoadesiw
Hageaikux
Ruqicibui
Numasetih
Kuuuuozog
Waqizeyoy
Readable接口只要求实现read()方法,在read()内部,将输入内容添加到CharBuffer参数中(有多种方法可以实现此目的,请查看CharBuffer的文档),或者在没有任何输入时返回-1。
假设你有一个还未实现Readable的类,怎样才能让Scanner作用于它呢?下面这个类就是一个例子,它可以产生随机浮点数:
//: interfaces/RandomDoubles. java
import java.util.*;
public class RandomDoubles {
private static Random rand = new Random (47):
public double next() { return rand.nextDouble()∶ }
public static void main(String[]args) {
RandomDoubles rd = new RandomDoubles ();
for(int i = 0; i < 7; i ++)
Systen.out.print (rd.next() + " ");
}
}Output :
0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964
我们再次使用了适配器模式,但是在本例中,被适配的类可以通过继承和实现Readable接口来创建。因此,通过使用interface关键字提供的伪多重继承机制,我们可以生成既是Random-Doubles又是Readable的新类:
//:interfaces/AdaptedRandomDoubles.java
import java.nio.*;
import java.uti1.* ;
public class AdaptedRandomDoubles extends RandomDoubles
implements Readable {
private int count ;
public AdaptedRandomDoubles(int count) {
this.count = count;
}
public int read(charBuffer cb) {
if(count-- ==0)
return -1;
string result = Double.tostring(next()) + " ";
cb.append(result); ·
return result.length();
}
public static void main(String[] args){
Scanner s = new Scanner (new AdaptedRandomDoubles(7));
while(s.hasNextDouble()
System.out.print(s.nextDouble() +" ");
}
}Output :
0.7271157860730044 0.5309454508634242 0.16020656493302599
0.18847866977771732 0.5166020801268457 0.2678662084200585
0.2613610344283964
因为在这种方式中,我们可以在任何现有类之上添加新的接口,所以这意味着让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是类的强大之处。
9.7接口中的域
因为你放入接口中的任何域都自动是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在Java SE5之前,这是产生与C或C++中的enum(枚举类型)具有相同效果的类型的唯一途径。因此在Java SE5之前的代码中你会看到下面这样的代码:
//:interfaces/Honths.java
package interfaces ;
public interface Honths {
int
JANUARY = 1,FEBRUARY = 2,HARCH = 3,
APRIL = 4,HAY = 5,JUNE = 6,JULY = 7,
AUGUST = 8,SEPTENBER = 9,0CTOBER = 10,
NOVEMBER =11,DECENBER = 12;
}
请注意,Java中标识具有常量初始化值的static final时,会使用大写字母的风格(在一个标识符中用下划线来分隔多个单词)。接口中的域自动是public的,所以没有显式地指明这一点。
有了Java SE5,你就可以使用更加强大而灵活的enum关键字,因此,使用接口来群组常量已经显得没什么意义了。但是,当你阅读遗留的代码时,在许多情况下你可能还是会碰到这种旧的习惯用法,在第19章中可以看到更多的关于使用enum的细节说明。
9.7.1初始化接口中的域
在接口中定义的域不能是“空final”,但是可以被非常量表达式初始化。例如
//:interfaces/Randvals.java
import java.uti1.*;
public interface RandVals {
Random RAND = new Random (47);
int RANDOH_INT =RAND.nextInt(10);
1ong RANDOM_LONG = RAND.nextLong()*10;
float RANDOM_FLOAT =RAND.nextLong()*10;
double RANDOH_DOUBLE = RAND.nextDouble()*10;
}
既然域是static的,它们就可以在类第一次被加载时初始化,这发生在任何域首次被访问时。
//:interfaces/TestRandVals.java
import static net.mindview.util.Print.* ;
public class TestRandvals {
public static void main(String[] args) {
print (Randvals.RANDOM_INT);
print (Randval s.RANDON_LONG);
print (Randval s.RANDON_FLOAT);
print (Randvals.RANDOM_DOUBLE);
}
}Output :
8
-32032247016559954
-8.5939291E18
5.779976127815049
当然,这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内。
9.8嵌套接口
接口可以嵌套在类或其他接口中。这揭示了许多非常有趣的特性:
//: interfaces/nesting/NestingInterfaces.java
package interfaces.nesting;
class A {
interface B {
void f();
}
public class BImp implements B {
public void f() {}
}
private class BImp2 implements B {
public void f() {}
}
public interface c {
void f();
}
class CImp implements c {
public void f() {}
}
private class cImp2 implements c {
public void f() {}
}
private interface D {
void f();
}
private class DImp implements D {
public void f() {}
}
public class DImp2 implements D {
public void f() {}
}
public D getD() {
return new D1mp2();
}
private D dRef;
public void receiveD(D d) {
dRef = d;
dRef.f();
}
}
interface E {
interface G {
void f();
}
public interface H {
void f();
}
void g();
//! private interface I {}
}
public class NestingInterfaces {
public class BImpimplements A,B{
public void f(){}
}
class CImp implements A,C {
public void f() {}
}
//! class DImp implements A,D {
//! public void f() {}
//! }
class EImp implements E {
public void g() {}
}
class EGImp implements E.G {
public void f() {}
}
class EImp2 imp1ements E {
public void g() {}
class EG implements E,G {
public void f() {}
}
}
public static void main(String[] args){
A a = new A();
//! A.D ad = a.getD();
//! A.DImp2 di2 = a.getD();
//! a.getD().f();
A a2 =new A();
a2.receiveD(a.getD());
}
}
在类中嵌套接口的语法是相当显而易见的,就像非嵌套接口一样,可以拥有public和“包访问”两种可视性。
作为一种新添加的方式,接口也可以被实现为private的,就像在A.D中所看到的(相同的语法既适用于嵌套接口,也适用于嵌套类)。那么private的嵌套接口能带来什么好处呢?读者可能会猜想,它只能够被实现为DImp中的一个private内部类,但是A.DImp2展示了它同样可以被实现为public类。但是,A.DImp2只能被其自身所使用。你无法说它实现了一个private接口D。因此,实现一个private接口只是一种方式,它可以强制该接口中的方法定义不要添加任何类型信息(也就是说,不允许向上转型)。
getD()方法使我们陷入了一个进退两难的境地,这个问题与private接口相关:它是一个返回对private接口的引用的public方法。你对这个方法的返回值能做些什么呢?在main()中,可以看到数次尝试使用返回值的行为都失败了。只有一种方式可成功,那就是将返回值交给有权使用它的对象。在本例中,是另一个A通过receiveD()方法来实现的。
接口E说明接口彼此之间也可以嵌套。然而,作用于接口的各种规则,特别是所有的接口元素都必须是public的,在此都会被严格执行。因此,嵌套在另一个接口中的接口自动就是public的,而不能声明为private的。
NestingInterfaces展示了嵌套接口的各种实现方式。特别要注意的是,当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能在定义它的类之外被实现。
添加这些特性的最初原因可能是出于对严格的语法一致性的考虑,但是我总认为,一旦你了解了某种特性,就总能够找到它的用武之地。
9.9接口与工厂
接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。
//: interfaces/Factories.java
import static net.mindview.uti1.Print.* ;
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
service getservice();
}
class Implementation1 implements service {
Implementation1() {}
public void methodi() {
print( "Implementation1 method1");
}
public void method2() {
print( "Implementation1 method2");
}
}
class Implementation1Factory implements ServiceFactory {
public Service getService(){
return new Implementation1();
}
}
class Implementation2 implements service {
Implementation2() {}
public void method1() {
print ( " Implementation2 method1");
}
public void method2() {
print ( "Implementation2 method2");
}
}
class Implementation2Factory implements ServiceFactory {
public service getService() {
return new Implementation2();
}
}
public class Factories {
public static void serviceConsumer (ServiceFactory fact) {
service s = fact.getservice(;
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsumer (new Implementation1Factoryo);
serviceConsumer (new Implementation2Factoryo);
}
}Output :
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
如果不是用工厂方法,你的代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器。
为什么我们想要添加这种额外级别的间接性呢?一个常见的原因是想要创建框架:假设你正在创建一个对弈游戏系统,例如,在相同的棋盘上下国际象棋和西洋跳棋:
//:interfaces/Games.java
import static net.mindview.uti1.Print.*;
interface Game { boolean move(); }
interface GameFactory { Game getGame(); }
class Checkers implements Game {
private int moves = 0;
private static final int NOVES = 3;
public boolean move() {
print ( "Checkers move " + moves);
return ++moves != HOVES;
}
}
class CheckersFactory implements GameFactory {
public Game getGame() { return new Checkers(); }
}
class Chess implements Game {
private int moves = 0;
private static final int MovEs = 4;
public boolean move() {
print ( "Chess move "+_moves) ;
return ++moves != HOVES;
}
}
class ChessFactory implements GameFactory {
public Game getGame() { return new Chess(); }
}
public class Games {
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while(s.move())
;
}
public static void main(String[]args) {
playGame(new CheckersFactory());
playGame(new ChessFactory());
}
}
Output :
checkers move 0
Checkers move 1
checkers move 2
chess move 0
chess move 1
Chess move 2
Chess move 3
如果Games类表示一段复杂的代码,那么这种方式就允许你在不同类型的游戏中复用这段代码。你可以再想象一些能够从这个模式中受益的更加精巧的游戏。
在下一章中,你将可以看到另一种更加优雅的工厂实现方式,那就是使用匿名内部类。