JAVA学习之路我爱编程

Java 之路 (九) -- 接口(抽象类方法、接口、多重继承、

2018-08-09  本文已影响260人  whd_Alive

我们前面学过通过“隐藏实现”可以将接口与实现分离,然而它仅仅作为基础,而本章的接口以及下一章的内部类 为我们提供了一种将接口和实现分离的更加结构化的方法。

话不多说,进入正题。


1. 抽象类和抽象方法

抽象类是普通的类与接口之间的一种中庸之道。

1.1 什么是抽象类/方法

抽象方法

抽象类:

1.2 为什么需要抽象类/方法

1.2.1 抽象类/方法 的意义

回顾上一张关于"乐器"(instrument) 的例子,会发现这样一个有趣的现象:基类 Instrument 中的方法全部是“哑”方法,如果我们需要表达一个特定行为,就必须使用导出类中的方法。

这里哑方法个人理解是该方法没有实际意义,只是提供接口供子类覆盖。

虽然在上一章是作为 多态 的例子引入,但是从设计的角度上考虑,Instrument 类的目的是为它的所有导出类创建一个 通用接口
建立该接口的唯一理由:不同的子类可以用不同的形式表示此接口。
通用接口建立起一种基本形式,以此表示所有导出类的共同方法,换句话说就是将 Instrument 类称作抽象基类

于是我们总结出抽象类的目的:

1.2.2 抽象类不能被实例化

上面我们分析了抽象类的意义,但是又出现了问题:假如我单独创建一个抽象类的对象,会发生什么?

  1. 从面向对象上理解:抽象类意义在于提供接口,如果此时单独创建抽象类,那它是几乎没什么意义的。
  2. 从安全性理解:抽象方法是不完整的,因此试图产生抽象类的对象是 不安全 的。

出于以上两点考虑,编译器会确保我们不会误用抽象类:当我们试图产生抽象类的对象时,编译器会提示错误消息。
因此我们得出结论:

结论得出了,但是还没完。。。

1.2.3 继承抽象类

某天你可能定义了一个抽象类,然后派生出了导出类 A,这时你心想:A 只需要表现某方面的特性,并不需要抽象类中的所有特性,于是只覆盖了基类的部分抽象方法。嗯,然后你就拿着 A 去创建对象了。

然后你就会惊讶的发现:程序出错了!为什么会这样呢?

事实上,如果抽象类的派生类中有任何一个抽象方法未定义,那么该导出类就同样是抽象类。编译器会强制我们用 abstract 关键字来指明该导出类是抽象类。

1.2.4 没有抽象方法的抽象类

抽象类可以没有抽象方法,这个特性记住吧,原书并未给出任何解释。
本节主要是介绍一下 没有抽象方法的抽象类,意义何在?


2. 接口

接口 (interface) 使抽象的概念更进一步。

2.1 什么是接口(interface)

简单来说,接口(interface 关键字) 是一个完全抽象的类,它是抽象方法的集合。

2.1.1 特性

  1. 只是一种形式,本身不能做任何事情 -- 无法被实例化
  2. 实现类可以向上转型为接口,以此实现类似”多重继承“的特性
  3. 某类”实现“接口时,需要实现接口中全部的方法
  4. 接口中的方法默认为 public
  5. 接口中的域隐式地是 static 和 final 的
  6. 接口可以指明访问权限,类似 class

2.1.2 用途

被用来建立类与类之间的协议

任何使用某接口的代码都知道且仅需知道可以调用该接口的哪些方法。

2.1.3 语法

接口的创建 -- interface 关键字

[权限修饰词] interface 接口名{
    //...
    //声明抽象方法
}

接口的实现 -- implements 关键字

[权限修饰词] class 类名 implements 接口名{
    //...
    //实现接口中的全部方法
}

2.2 接口与完全解耦

此处原文中通过三个例子循序渐进的介绍如何提高代码复用性,逐步加深解耦程度。

2.2.1 例1 -- 父子类 & 向上转型 & 策略模式

代码如下:

class Processor{
    public String name(){
        return getClass().getSimpleName();
    }
    //子类中重写次此方法时用其他类型如string  int 等
    Object process(Object input){
        return input;
    }
}

class Upcase extends Processor{
    String process(Object 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,Object s){
        System.out.println("Using Processor"+p.name());
        System.out.println(p.process(s));
    }
    public static String s="this is a Sup--Sub Coupling";
    public static void main(String[] args){
        process(new Upcase(),s);
        process(new Downcase(),s);
        process(new Splitter(),s);
    }
}
//输出结果:
/*
Using ProcessorUpcase
THIS IS A SUP--SUB COUPLING
Using ProcessorDowncase
this is a sup--sub coupling
Using ProcessorSplitter
[this, is, a, Sup--Sub, Coupling]
*/

基类 Processor 中有一个 name() 方法,另外有一个 process() 方法,该方法根据接受不同输入参数,修改值后产生输出。接下来对基类进行扩展,派生出不中类型的 Processor。

Apply.process() 方法接收任何类型的 Processor,并将其应用到 Object 对象上,然后打印结果。这里其实用到了策略设计模式,策略就是传递进去的参数对象,它包含了要执行的代码。

策略设计模式:创建一个根据所传递的参数对象的不同而具有不同行为的方法。
核心就是"封装变化":方法包含所要执行的算法中固定不变的部分,"策略"包含变化的部分

此时,假如我们发现了另一组类(电子滤波器)如下:

class Waveform{
    private static long counter;
    private final long id=counter++;
    public String toString(){
        return "Waveform:"+id;
    }
}

class Filter{
    public String name(){
        return getClass().getSimpleName();
    }
    public Waveform process(Waveform input){
        return input;
    }
}
class UpFilter extends Filter{
    double cutoff;
    UpFilter(double cutoff){ this.cutoff = curoff;}
    Waveform process(Waveform input){
        return input;
    }
}
//...

能看到,Filter 和 Processor 具有相同的接口元素,但是由于二者并非继承关系,因此 Apply.process() 方法不能传入 Filter 参数(Apply.process() 方法和 Processor 紧紧绑在一起),使得不能复用Apply.process() 的代码。
问题:

2.2.2 例2 -- 定义接口

在上面的问题下,我们将 Processor 定义为接口,然后复用该接口的 Apply.process()。
代码如下:

public interface Processor{
    String name();
    Object process(Object input);
}

public class Apply {
    public static void process(Processor p,Object s){
        System.out.println("Using Processor"+p.name());
        System.out.println(p.process(s));
    }
}   

这样一来,如果我们可以修改电子滤波器 Filter 类的话,我们只需要让其实现此接口(public abstract class Filter implements Processor ),然后再派生出不同子类即可。

2.2.3 例3 -- 对接口进行适配

但是,当我们无法修改 Filter 类的时候,我们就需要使用适配器模式,接收拥有的接口,并以此产生需要的接口。
如下:

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);
      }
}

上面的三种处理方式是循序渐进的,将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具复用性。

2.3 接口的扩展 -- 通过继承

接口可以继承接口,且一个接口可以同时 extends 多个接口。

  1. 在接口中添加新方法
  2. 组合多个接口中的方法
interface A { void methodA(); }
interface B { void methodB(); }

// 在 A 接口的基础上,添加新方法
interface C extends A { void methodC(); }
// 组合 A 和 B 接口,同时添加了新方法
interface D extends A,B { void methodD(); }

class Test1 implements C {
    void methodA(){ /*..具体实现*/ }
    void methodC(){ /*..具体实现*/ }
}

class Test2 implements D {
    void methodA(){ /*..具体实现*/ }
    void methodB(){ /*..具体实现*/ }
    void methodD(){ /*..具体实现*/ }
}

需要注意:

2.4 接口的其他事项

接口与多重继承

接口中的域:

接口的适配

接口与工厂

接口的嵌套


总结

任何抽象性都应该是应需求而产生的。必需时应该重构接口,而不是到处添加额外的类。
恰当的设计原则是优先选择类而不是接口。从类开始,如果接口变得必需,那么就进行重构。

本章结束,共勉。

上一篇 下一篇

猜你喜欢

热点阅读