接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。这种机制在编程语言中并不通用,Java语言提供了关键字说明人们认为这些思想是重要的。还有另一个概念叫做抽象类,它是介于普通的类与接口之间的一个中庸之道。尽管在构造某些具有未实现方法的类时,你第一想到的可能是接口,但是抽象类仍旧是一种用于此目的的一种重要而必须的工具。因为你不可能总是使用纯接口。
1. 抽象类和抽象方法
在之前章节关于多态的例子中,基类方法往往没有具体的操作,它存在的目的是为了它所有的子类提供一个统一的接口。通过向上转型、动态绑定等操作来实现子类的各自多样化的功能。不同的子类可以用不同的方式表示此接口,通用接口建立起一种基本的形式,以表示所有子类的共同部分。我们将这种基类称作抽象基类,或者抽象类。
如果我们创建了一个这样的抽象类,因为它的方法存在的意义只是提供一个接口,那么说明这个基类的对象没有任何意义。并且我们还想阻止使用者创建基类的对象,所以我们可以在抽象类中的方法返回一些错误的内容,但是这样做有两个问题,一方面错误要在运行时才能抛出,另一方面错误带来的影响会不可估计。为此Java提供了一种新的方法,叫做抽象方法,这种方法是不完整的,它只有方法的声明,没有方法体。表示该方法是抽象的,不需要在基类中实现的,只是作为所有子类的一个公共的接口。抽象方法使用abstract关键字声明:
abstract void f();
包含上述抽象方法的类叫做抽象类,如果一个类中包含一个或多个抽象方法,那么这个类就必须被定义为抽象的,也就是抽象类。抽象类的关键字也是abstract。抽象类是不安全的,因为它不完整,所以当它试图创建对象的时候,编译器会报错。如果一个类继承自抽象类,并想创建新类的对象,那么这个类必须为基类中的所有抽象方法提供具体的方法体,否则的话这个类也要被声明为抽象类。我们也可能会创建一个没有任何抽象方法的抽象类,比如当我们一个类中的任何方法都没有存在的意义时,或者也没有被声明为abstract的意义时,恰好我们又需要让这个类不能够创建对象,那么这时一个没有任何抽象方法的抽象类就产生了它的作用。
下面通过一个“乐器”的例子看一下抽象类和抽象方法的具体使用:
abstract class Instrument{
private int i;
public String what(){
return "Instrumet";
}
public abstract void play(String s);
public void adjust(){};
}
class Wind extends Instrument{
public String what(){
return "Wind";
}
public void play(String s){
System.out.println("Wind.play" + s);
}
public void adjust(){};
}
class Percussion extends Instrument{
public String what(){
return "Percussion";
}
public void play(String s){
System.out.println("Percussion.play" + s);
}
public void adjust(){};
}
class Stringed extends Instrument{
public String what(){
return "Stringed";
}
public void play(String s){
System.out.println("Stringed.play" + s);
}
public void adjust(){};
}
class Brass extends Instrument{
public String what(){
return "Brass";
}
public void play(String s){
System.out.println("Brass.play" + s);
}
public void adjust(){};
}
class WoodWind extends Instrument{
public String what(){
return "WoodWind";
}
public void play(String s){
System.out.println("WoodWind.play" + s);
}
public void adjust(){};
}
public class Music{
static void tune(Instrument i ){
i.play("finish");
}
static void tuneAll(Instrument[] e){
for(Instrument i:e){
tune(i);
}
}
public static void main(String[] args) {
Instrument[] iArray = {
new Wind(),new Percussion(),new Brass(),new Stringed(),new WoodWind()
};
tuneAll(iArray);
}
}
运行结果:
Wind.playfinish
Percussion.playfinish
Brass.playfinish
Stringed.playfinish
WoodWind.playfinish
如上可见,创建抽象类和抽象方法非常的有用,因为他们使类的抽象性更加明确,并告诉用户和编译器打算怎么样使用他们。抽象类还是一个很有用的重构工具,因为他们使得我们可以很容易的将公共方法沿着继承的层次向上移动。
2. 接口
interface关键字使抽象的概念更加迈进了一步,abstract关键字允许我们在类中创建一个没有方法实现的方法,方法的具体实现由继承它的子类创建。interface关键字产生一个完全抽象的类,它根本就没有提供任何具体的实现,它允许创建者确定具体的方法名,参数列表和返回值,但是没有任何方法体,接口只提供了对外的形式,但是没有提供任何具体实现。一个接口表示“所有实现该接口的类看起来都一样”,所以任何使用特定接口的类都知道他们可以调用哪些方法,因此接口用来实现类与类之间的一种协议关系。
接口的创建使用interface关键字代替class关键字,访问权限控制与class相同,接口可以包含域,但是这些域隐式的被定义为static和final。
要想具体的实现某一个接口的方法,需要创建一个类,并使用implements关键字关联所要实现的方法的接口,表示我们是这个接口的内部实现。当要实现一个接口的时候,在接口中被定义的方法必须是public,否则只能得到默认的包访问权限,这在继承关系中会使访问权限被降低,这是编译器不允许的。接口中的方法只有声明存在,这是编译器允许在接口中存在的唯一事物。
修改抽象类中的示例为接口的形式:
interface Instrument{
public void play(String s);
}
class Wind implements Instrument{
public String what(){
return "Wind";
}
public void play(String s){
System.out.println("Wind.play" + s);
}
public void adjust(){};
}
class Percussion implements Instrument{
public String what(){
return "Percussion";
}
public void play(String s){
System.out.println("Percussion.play" + s);
}
public void adjust(){};
}
class Stringed implements Instrument{
public String what(){
return "Stringed";
}
public void play(String s){
System.out.println("Stringed.play" + s);
}
public void adjust(){};
}
class Brass implements Instrument{
public String what(){
return "Brass";
}
public void play(String s){
System.out.println("Brass.play" + s);
}
public void adjust(){};
}
class WoodWind implements Instrument{
public String what(){
return "WoodWind";
}
public void play(String s){
System.out.println("WoodWind.play" + s);
}
public void adjust(){};
}
public class Music{
static void tune(Instrument i ){
i.play("finish");
}
static void tuneAll(Instrument[] e){
for(Instrument i:e){
tune(i);
}
}
public static void main(String[] args) {
Instrument[] iArray = {
new Wind(),new Percussion(),new Brass(),new Stringed(),new WoodWind()
};
tuneAll(iArray);
}
}
运行结果:
Wind.playfinish
Percussion.playfinish
Brass.playfinish
Stringed.playfinish
WoodWind.playfinish
3. 完全解耦
只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想将这个方法应用在不在此继承结构中的某个类,那么使用接口将很大程度的放宽这种限制。因此,它可以使我们编写可复用性更好的代码。
例如,有一个Processor类,它有一个name()方法,还有一个process()方法,该方法接受输入参数,修改输入的值然后进行输出。这个类作为基类被扩展,子类创建各种不同类型的Processor,在本例中,Processor子类通过process()方法修改String对象的值,返回类型可以是协变类型,而非参数类型。
class Processor{
public String name(){
return getClass().getSimpleName();
}
Object process(Object input){
return input;
}
}
class UpCase extends Processor{
String process(Object input){
return input.toString().toUpperCase();
}
}
class DownCase extends Processor{
String process(Object input){
return input.toString().toLowerCase();
}
}
class Splitter extends Processor{
String process(Object input){
return Arrays.toString(input.toString().split(" "));
}
}
public class Apply{
public static void process(Processor p,Object s){
System.out.println("Using Processor:" + p.name());
System.out.println(p.process(s));
}
public static final 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);
}
运行结果:
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对象,然后打印。像这种,根据继承关系,创建一个能够根据所传递的参数对象不同而具有不同行为的方法,称为策略设计模式。这类方法包含索要执行的方法中固定不变的部分(如本例的name()方法),而“策略”包含变化的部分(如本例的process())方法。策略就是传递的参数对象,它包含要执行的代码。这类Processor对象就是一个策略,在main()方法中可以看到三种不同类型的策略应用到Obejct对象上。
3.1 适配器模式
下面有如下4个类,他们看起来也适用Apply.process()
public class Filter{
public String name(){
return getClass().getSimpleName();
}
public WaveForm process(WaveForm input){
return input;
}
}
Filter类,看上去与Processor类似,都有name()方法和process()方法,区别在于方法的参数类型和返回类型不同。
public class HighPass extends Filter{
double cutoff;
public HighPass(double cutoff){
this.cutoff = cutoff;
}
public WaveForm prcess(WaveForm input){
return input;
}
}
public class BandPass extends Filter {
double lowCutoff,highCutoff;
public BandPass(double lowCutoff,double highCutoff){
this.lowCutoff = lowCutoff;
this.highCutoff = highCutoff;
}
public WaveForm process(WaveForm input){
return input;
}
}
public class WaveForm{
private static long counter;
private final long id = counter++;
public String toString(){
return "Wave Form" + id;
}
}
ilter与Processor类具有相同的内部接口元素(两个类的方法名都相同),但是由于Filter类并非继承自Processor类,因此当Apply.process()方法传入参数Filter的时候,由于Filter类的创建者并不知道要当做Processor类使用,并且它也不能通过向上转型的方式变成Processor类,因此不能将Filter类应用到Apply.process()方法。这主要是因为Apply.process()方法和Processor类的耦合度太高了,已经超出了所需要的程度。这就是Apply.process()方法只能接收Processor类或者其子类,而面对新的类的时候,Apply.process()方法就无能为力了,对其的复用也就被禁止了。
正如前文所说,如果操作的是接口而不是类的时候,那么这些限制就会变得松动,使得你可以复用接口的Apply.process()方法,下面是修改为接口的版本。将设计模式改成适配器设计模式
public interface Processor{
String name();
Object process (Object input);
}
此时Processor类变成了一个接口,复用代码的形式就是之前继承它的类,可以改为实现它的接口,并且Filter类,也可以编程实现Processor类的接口,这样Apply.process()方法的耦合度就降低了,并且支持了其它的类型。还有一种情况,假如一个类是被发现的,而不是被我们自己创建的,那么这个类就无法实现Processor接口,比如说,如果Filter类是在类库中的类,那么这个类就无法主动实现Processor接口,这时候可以使用适配器模式,在这个类的外部封装一层,作为适配器来实现要实现的接口。如下:
class FilterAdapter implements Processor{
Filter filter;
public FilterAdapter(Filter filter){
this.filter = filter;
}
public String name(){
return filter.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);
}
}
在这种使用适配器的方式中,FilterAdapter的构造器接受了Filter参数,然后生成对应接口Processor的对象。
本节主要的内容是使用接口的方式将只有基类和其子类的使用方法解耦出来,便于程序更好的进行复用。
4. Java中的多重继承
组合多个类的接口行为被称为多重继承
在Java中,导出类不强制要求必须有一个是抽象的或者“具体的”(没有任何抽象方法)基类。如果要从一个非接口的类继承,那么只能从一个类去继承。其他的基元素都必须是接口。可以继承任意多个接口,并可以向上转型为每一个接口,因为每一个接口都是一个独立类型。
使用接口的核心原因:
- 为了能够向上转型为多个基类型
- 防止客户端程序员创建该类的对象
5. 通过继承来扩展接口
接口也可以继承!没错,通过继承可以很容易的在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口。
5.1 组合接口时的名字冲突
上面例子中,CanFight和ActionCharacter都有一个void fight方法,二者方法相同,相同的方法不会出现问题。如果他们的签名或返回类型不同,就会出现错误。在打算组合的不同接口中使用的方法名通常会造成代码可读性的混乱,应避免。
6. 适配接口
接口最吸引人的原因之一就是允许同一接口具有多个不同的具体实现。因此,接口的一种常见用法就是前面提到的策略设计模式。 方法接受一个指定的接口:可以用任何对象来调用方法,只要对象遵循接口。
例如:Scanner类的构造器接受的就是一个Readable接口。
private Scanner(Readable source, Pattern pattern) {
assert source != null : "source should not be null";
assert pattern != null : "pattern should not be null";
this.source = source;
delimPattern = pattern;
buf = CharBuffer.allocate(BUFFER_SIZE);
buf.limit(0);
matcher = delimPattern.matcher(buf);
matcher.useTransparentBounds(true);
matcher.useAnchoringBounds(false);
useLocale(Locale.getDefault(Locale.Category.FORMAT));
}
Readable没有用作Java标准类库中其他任何方法的参数,它是单独为Scanner创建的,以使得Scanner不必将其参数限制为某一个特定类。通过这种方式,Scanner可以作用于更多的类型。如果你创建了一个新的类,并想让Scanner作用于它,那就让它成为Readable:
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;
}
@Override
public int read(CharBuffer cb) throws IOException {
if(count-- == 0){
return -1;
}
cb.append(capitals[rand.nextInt(capitals.length)]);
for(int i = 0; i < 4; i++){
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的类,怎么才能让Scanner作用于它?:
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 ++){
System.out.println(rd.next() + " ");
}
}
}/*
0.7271157860730044
0.5309454508634242
0.16020656493302599
0.18847866977771732
0.5166020801268457
0.2678662084200585
0.2613610344283964
*/
再次使用了适配器模式,但在本例中,被适配的类可以通过继承和实现Readable接口来创建。因此,通过使用interface关键字提供的伪多重继承机制,我们可以生成既是RandomDoubles又是Readable的新类:
public class AdaptedRandomDoubles extends RandomDoubles implements Readable{
private int count;
public AdaptedRandomDoubles(int count) {
this.count = count;
}
@Override
public int read(CharBuffer cb) throws IOException {
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.println(s.nextDouble() + " ");
}
}
}/*Output:
0.7271157860730044
0.5309454508634242
0.16020656493302599
0.18847866977771732
0.5166020801268457
0.2678662084200585
0.2613610344283964
*/
因为在这种方式中,我们可以在任何现有类之上添加新的接口,所以这意味着让方法接受接口类型,是一种让任何类都可以对该方法进行适配的方式。这就是使用接口而不是类的强大之处。
7. 接口中的域
接口中的任何域都自动是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在Java SE5之前,这是产生与C或C++的enum(枚举类型)具有相同效果的类型的唯一途径。
public interface Months {
int JANUARY = 1, FEBRUARY = 2, MARCH = 3,
APRIL= 4, MAY= 5, JUNE= 6, JULY= 7,
AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
NOVEMBER = 11, DECEMBER = 12;
}
接口中的域自动是public的,所以没有显式地指明这一点。
8. 嵌套接口
接口可以嵌套在类或者其它的接口中,个人觉得这种设计会使程序变得更加复杂不易读。
9. 接口与工厂
接口是实现多重继承的重要途径,而生成遵循某个接口对象的典型方式就是工厂方法设计模式。由此可见设计模式的重要性,我自己最近也在学习这一块的内容。希望能够有所提高。
使用工厂方法与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上来说,我们通过这种方式可以将我们的代码与接口的实现完全分离,这就使我们可以透明的将某个实现替换成另一个实现。如下示例:
package com.basic.java.factory;
/**
* locate com.basic.java.factory
* Created by MasterTj on 2018/12/12.
* 工厂设计模式
*/
interface Service{
void method1();
void method2();
}
interface ServiceFactory{
Service getService();
}
class Implementation1 implements Service{
Implementation1() {
// TODO Auto-generated constructor stub
}
@Override
public void method1() {
// TODO Auto-generated method stub
System.out.println("Implementation1 method1");
}
@Override
public void method2() {
// TODO Auto-generated method stub
System.out.println("Implementation1 method2");
}
}
class Implementation1Factory implements ServiceFactory{
@Override
public Service getService() {
// TODO Auto-generated method stub
return new Implementation1();
}
}
class Implementation2 implements Service{
Implementation2() {
// TODO Auto-generated constructor stub
}
@Override
public void method1() {
// TODO Auto-generated method stub
System.out.println("Implementation2 method1");
}
@Override
public void method2() {
// TODO Auto-generated method stub
System.out.println("Implementation2 method2");
}
}
class Implementation2Factory implements ServiceFactory{
@Override
public Service getService() {
// TODO Auto-generated method stub
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 Implementation1Factory());
serviceConsumer(new Implementation2Factory());
}
}
10 总结
抽象类跟接口是将具体方法更加抽象的一种形式,这一章节主要讲了抽象类、抽象方法的形式以及使用场景,比较重要的一点是关于接口的使用,如何解耦,接口可以多重继承,接口可以嵌套等应用场景。关于这一章节中的设计模式,还要继续深入研究下去。