接口和内部类为我们提供了一种接口与实现分离的更加结构化的方法;
首先需要学习抽象类,它是普通的类与接口之间的一种中庸之道;因为不可能总是使用纯接口,所以抽象类仍旧有着重要的作用。
- 抽象类和抽象方法
- 接口
- 完全解耦
- Java中的多重继承
- 通过继承来扩展接口
- 适配接口
- 接口中的域
- 嵌套接口
- 接口与工厂
1.抽象类和抽象方法
在上一章的例子中,基类Instrument中的方法往往是“哑”(dump)方法。Instrument类的目的是为它的所有导出类创建一个通用接口。
建立通用接口的理由:
不同的子类可以用不同的方式表示此接口。通接口建立起一种基本形式,以此表示所有导出类的共同部分;
另一种说法是将Instrument类称作抽象基类,简称抽象类。
如果只有一个像Instrument这样的抽象类,那么该类的对象几乎没有任何意义,创建抽象类是希望通过这个通用接口操纵一系列类。
因此,Instrument只是表示了一个接口,没有具体的实现内容;实际上,Java禁止使用这创建抽象类对象。在编译时会捕获这些问题:
为此,Java提供一个叫做抽象方法的机制,这种方法是不完整的;仅有声明而没有方法体。语法如下:
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象类。(否则,编译器就会报错。)
由于为抽象类创建对象是不安全的,所以我们会从编译器那里得到一条出错消息,这样编译器会确保抽象类的纯粹性,不必担心误用它。
如果从一个抽象类继承,并想创建该新类的对象,那么必须为基类中的所有抽象方法提供方法定义。如果不这么做(也可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。
我们也能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这样做就很有用了。
如何将上一章的Instrument类转化成abstract类(抽象类并不需要所有的方法都是抽象的,只要有一个抽象方法就是抽象类):
这里将Instrument类中的play()和adjust()方法变成abstract方法,所以这是一个抽象类,必须以abstract声明该类,且导出类必须实现这两个抽象方法,否则导出类仍然是抽象类;
可以看到,除了基类,几乎没什么变化。
创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样来使用它们。抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继承层次结构向上移动。
总结:
抽象方法:前面由abstract声明,只要方法体,没有具体实现
抽象类:含有一个或多个抽象方法的抽象类,但一般不包括全部都是抽象方法,全部都是抽象方法接下来会介绍是接口;抽象类需要使用abstract声明
抽象类可以被继承,但不能创建对象,导出类必须实现所有的抽象方法,否则仍然是抽象类,且必须加abstract声明
2.接口
interface关键字使抽象方法的概念更向前迈进了一步。abstract关键字允许人们在类中创建一个或多个没有任何定义的方法--提供了接口部分,但是没有提供任何任何相应的具体实现,这些实现是由此类的继承者创建的。
interface这个关键字产生一个完全抽象的类,它根本没有提供任何具体实现。它允许创建者确定方法名、参数列表和返回类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现。
任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。因此,接口被用来建立类与类之间的协议
要想创建一个接口,需要用interface关键字来替代calss关键字,就像类一样,可以在interface关键字前面添加public关键字(但仅限于该接口在与其同名的文件中被定义)。如果不添加public关键字,则它只具有包访问权限,这样就只能在同一个包内可用。接口也可以包含域,但是这些域隐式的是static和final的。
要让一个类遵循某个特定接口(或者是一组接口),需要使用implements关键字,它表示:“interface只是它的外貌,但是现在我要声明它是如何工作的”,除此之外,它看起来还很像继承。"乐器"示例的图说明了这一点:
在程序里,无法证明Instrument是一个普通类、抽象类或者还是一个接口。
3.完全解耦
只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果想要将这个方法应用于不在此继承结构中的某个类,那就会出现问题。接口可以在很大程度上放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。
例如:假设有一个Processor类,他有一个main()方法;另外还有一个process()方法,该方法接受输入参数,修改它的值,然后产生输出。这个类作为基类而被扩展,用来创建各种不同类型的Processor。在本例中,Processor的子类将修改String对象(返回类型可以是协变类型,而非参数类型):
Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后打印结果。
像本例这样,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。
这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。
在这里,Processor对象就是一个策略,在main()中可以看到有三种不同类型的策略应用到了String类型的s对象上。
string.toUpperCase();针对String类型,将String全部转换成大写;
string.toLowerCase()针对String类型,将String全部转换成小写;
split()方法是String类的一部分,它接受String类型的对象,并以传递进来的参数作为边界,将该String对象分隔开,然后返回一个数组String[]。可以当做创建String数组的快捷方法。
再看一个例子:
现在假设发现了一组电子滤波器,它们看起来好像适用于Apply.process()方法(好像适用是因为这组滤波器的方法名和参数都相同,也就是耦合过紧):
Filter和Processor具有相同的接口元素,但是因为它并非继承自Processor--因为Filter类的创建者压根不清楚你想要将他用作Processor--因此不能将Filter用于Apply.process()方法,即便这样做可以正常运行。
这里主要是因为Apply.process()方法和Processor之间的耦合过紧,已经超出了所需要的程度,这就使得应该复用Apply.process()的代码时,复用却被禁止了。
另外还需要注意的是,它们的输入和输出都是Waveform。
但是,如果Processor是一个接口,那么这些限制就会变得松动,使得你可以复用结构该接口的Apply.process()。下面是Processor和Apply的修改版本:
复用代码的第一种方式是客户端程序员遵循该接口来编写它们自己的类,就像下面这样:
但是经常碰到的情况是无法修改你想要使用的类,例如:在电子滤波器中,类库是发现的,并不是自己创建的,在这些情况下,可以使用适配器设计模式。适配器中的代码将接受你所拥有的接口,并产生你所需要的接口,就像下面这样:
在这种使用适配器的方式中,FilterAdapter的构造器接受你所拥有的接口Filter,然后生成具有你所需要的Processor借口的对象。
在FilterAdapter类中还用到了代理;
将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具有可复用性。
4.Java中的多重继承
接口不仅仅是一种更纯粹形式的抽象类,接口没有任何具体实现--也就是说,没有任何与接口相关的存储;因此,也就无法组织多个接口的组合。
在C++中,组合多个类的接口的行为被称作多重继承,这会造成很大的压力,因为每个类都有一个具体实现;
在Java中,可以执行相同的行为,但是只有一个类可以有具体实现;因此,通过组合多个接口,C++中的问题是不会在Java中发生的。
在导出类中,不强制要求必须有一个是抽象的或“具体的”基类。如果要从一个非接口的类继承,那么只能从一个类去继承,其余的基元素必须是接口。
需要将所有的接口名都置于implements关键字之后,用逗号将它们一一隔开。
可以继承任意多个接口,并可以向上转型为每个接口,因为每个接口都是一个独立类型。
下面例子展示了一个具体类组合数个接口之后产生一个新类:
Hero组合了具体类ActionCharacter和接口CanFight、CanSwim和CanFly。当通过这种方式讲一个具体类和多个接口组合到一起时,这个具体类必须放在前面,后面跟着的才是接口。
这里注意到,CanFight接口与ActionCharacter类中的fight()方法的特征签名是一样的,而且,在Hero中并没有提供fight()的定义。
可以扩展接口,但是得到的只是另一个接口。
当想要创建对象时,所有的定义首先必须都存在,即使Hero没有显式地提供fight()的定义,其定义也因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。
在Adventure类中,可以看到有四个方法把上述各种接口和具体类作为参数,当Hero对象被创建时,它可以被传递给这些方法中的任何一个,这意味着它一次被向上转型为每一个接口。Java的这种设计接口的方式,使得这项工作并不需要程序员付出任何特别的努力。
使用接口的核心原因:
1).为了能够向上转型为多个基类型(以及由此带来的灵活性);
2).防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口(这与使用抽象类原因相同)
这带来的一个问题是,应该使用接口还是抽象类?
如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,若知道某事物应该成为一个基类,那么第一选择应该是接口。
5.通过继承来扩展接口
通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口,这两种方式都可以获得新的接口。
DangerousMonster是Monster的直接扩展,它产生了一个新接口。DragonZilla中实现了这个接口;
在Vampire中使用的语法仅适用于接口继承,一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口,只需用逗号将接口名一一分隔开即可。
组合接口时的名字冲突:
在实现多重继承时,会碰到一个小陷阱,在前面的例子中,CanFight和ActionCharacter都有一个相同的void fight()方法。问题不是它们方法相同,问题是,如果它们的签名(参数)或返回类型不同,会怎么样呢?
此时困难来了,因为覆盖、实现和重载令人不快的搅在一起,而且重载方法仅通过返回类型是区分不开的。当撤销最后两行的注释时,下列错误消息说明了这一切:
在打算组合的不同接口中使用相同的方法名通常会造成代码可读性的混乱。
6.适配接口
接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。
因此,接口的一种常见用法就是前面提到的策略设计模式。此时你编写一个执行某些操作的方法,而该方法接受一个同样是你指定的接口。你主要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口”。这使得方法更加灵活、通用,并更具有可复用性。
例如,Java SE5的Scanner类的构造器接受的就是一个Readable接口,Readable没有用作Java标准类库中其他任何方法的参数,它是单独为Scanner创建的,以使得Scanner不必将其参数限制为某个特定类。通过这种方式,Scanner可以作用于更多的类型。如果你创建了一个新的类,并且想让Scanner可以作用于它,那么你就应该让它称为Readable,就像下面这样:
也就是说,在Scanner构造器内部,接受一个参数,这个参数是Readable的一个对象;
Readable接口可以实现read()方法,在read()内部,将输入内容添加到CharBuffer参数中,或者在没有任何输入时返回-1;
假设你还有一个还未实现Readable的类,怎样才能让Scanner作用于它呢?下面这个类就是一个例子,它可以产生随机浮点数:
还未实现Readable是指,没有实现read()方法?
再次使用了适配器模式,在这里,被适配的类可以通过继承和实现Readable接口来创建。因此,通过使用interface关键字提供的伪多重继承机制,可以生成的既是RandomDoubles又是Readable的新类:
在这种方式中,可以在任何现有类之上添加新的接口,所以这意味着让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是类的强大之处。
7.接口中的域
因为放入接口中的任何域都是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在Java SE5之前,这是产生与C++中的enum(枚举类型)具有相同效果的类型的唯一途径。因此,在Java SE5之前的代码会看到下面这样的代码:
Java中标识具有常量初始值的static final时,会使用大写字母的风格(在一个标识符中用下划线来分隔多个单词);接口中的域自动是public的,所以没有显式地指明这一点。
有了Java SE5,既可以使用更加强大而灵活的enum关键字,因此,使用接口来群组常量已经显得没什么意义了。
初始化接口中的域:
在接口定义的域不能是“空final”,但是可以被非常量表达式初始化。例如:
既然,域是static的,它们就可以在类第一次被加载时初始化,这发生在任何域首次访问时。下面是一个测试:
当然,这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内。
8.嵌套接口
接口可以嵌套在类或其他接口中。这揭示了许多有趣的特性:
在类中嵌套接口的语法是相当显而易见的,就像非嵌套接口一样,可以拥有public和“包访问”两种可视性。
作为一种新添加的方式,接口也可以被实现为private的,就像在A.D中所看到的(相同的语法既适用于嵌套接口,也适用与嵌套类)。那么private的嵌套接口能带来什么好处呢?
可能会猜想,它只能够被实现为DImp中的一个private内部类,但是A.DImp2展示了它同样可以被实现为public类。但是,A.DImp2只能被其自身所使用。
你无法说他实现了一个private接口D。因此,实现一个private接口只是一种方式,他可以强制该接口中的方法定义不要添加任何类型信息(也就是说,不允许向上转型)。
getD()方法使我们陷入一个进退两难的境地,这个问题与private接口相关:它是一个返回对private接口的引用的public方法。你对这个方法的返回值能做什么呢?在main()中,可以看到数次尝试使用返回值的行为都失败了。只有一种方式可成功,那就是将返回值交给有权使用它的对象(在这里只有D和A有权使用DImp2类,但前面不是说不允许向上转型吗?)。在本例中,是另一个A通过receiveD()方法来实现。
接口E说明接口接口彼此之间也可以嵌套,然而,作用域接口的各种规则,特别是所有的接口元素都必须是public的,在此都会被严格执行。因此,嵌套在另一个接口中的接口自动就是public的,而不能被声明为private的。
NestingInterface展示了嵌套接口的各种实现方式。特别要注意的是,当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且private接口不能在定义它的类之外被实现。
添加这些特性的最初原因可能是处于对严格的语法一致性的考虑,但是作者认为,一旦了解了某种特性,就总能够找到他的用武之地。
9.接口与工厂
接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。
这与直接调用构造器不同,在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。
理论上,通过这种方式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。下面的实例展示了工厂方法的结构:
如果不是用工厂方法,你的代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器。
为什么想要创建这种额外级别的间接性?一个常见的原因就是想要创建框架:假设你正在创建一个对弈游戏系统,例如,在相同的棋盘上下国际象棋和西洋跳棋:
如果Games类表示一段复杂的代码,那么这种方式就允许你在不同类型的游戏中复用这段代码。你可以想象一些能够从这个模式中受益的更加精巧的游戏。
下一章的工厂实现方式是:内部类。