我们前面学过通过“隐藏实现”可以将接口与实现分离,然而它仅仅作为基础,而本章的接口以及下一章的内部类 为我们提供了一种将接口和实现分离的更加结构化的方法。
话不多说,进入正题。
1. 抽象类和抽象方法
抽象类是普通的类与接口之间的一种中庸之道。
1.1 什么是抽象类/方法
抽象方法
- 定义:指的是一些只有方法声明,而没有具体方法体的方法。
- 声明语法:通过 abstract 关键字,如
abstract void method();
抽象类:
- 定义:包含抽象方法的类叫做抽象类。
- 声明方法:通过 abstract 关键字,如
abstract class Instrument { ... }
- 要点:
- 如果一个类包含一个或多个抽象方法,该类必须被限定为 抽象的( abstract )。
- 抽象类中可以有 非抽象方法,
- 抽象类可以没有抽象方法。
- 抽象类不能被实例化
1.2 为什么需要抽象类/方法
1.2.1 抽象类/方法 的意义
回顾上一张关于"乐器"(instrument) 的例子,会发现这样一个有趣的现象:基类 Instrument 中的方法全部是“哑”方法,如果我们需要表达一个特定行为,就必须使用导出类中的方法。
这里哑方法个人理解是该方法没有实际意义,只是提供接口供子类覆盖。
虽然在上一章是作为 多态 的例子引入,但是从设计的角度上考虑,Instrument 类的目的是为它的所有导出类创建一个 通用接口。
建立该接口的唯一理由:不同的子类可以用不同的形式表示此接口。
通用接口建立起一种基本形式,以此表示所有导出类的共同方法,换句话说就是将 Instrument 类称作抽象基类。
于是我们总结出抽象类的目的:
-
通过通用接口操纵一系列类,该抽象类只是提供了一系列接口,其中没有具体的实现内容
抽象类/方法使类的抽象性明确,并告诉用户和编译器打算如何使用它们。
同时抽象类也是有用的重构工具,因为它们使得我们可以很容易的将公共方法沿着继承层次结构向上移动。
1.2.2 抽象类不能被实例化
上面我们分析了抽象类的意义,但是又出现了问题:假如我单独创建一个抽象类的对象,会发生什么?
- 从面向对象上理解:抽象类意义在于提供接口,如果此时单独创建抽象类,那它是几乎没什么意义的。
- 从安全性理解:抽象方法是不完整的,因此试图产生抽象类的对象是 不安全 的。
出于以上两点考虑,编译器会确保我们不会误用抽象类:当我们试图产生抽象类的对象时,编译器会提示错误消息。
因此我们得出结论:
- 抽象类不能被实例化
结论得出了,但是还没完。。。
1.2.3 继承抽象类
某天你可能定义了一个抽象类,然后派生出了导出类 A,这时你心想:A 只需要表现某方面的特性,并不需要抽象类中的所有特性,于是只覆盖了基类的部分抽象方法。嗯,然后你就拿着 A 去创建对象了。
然后你就会惊讶的发现:程序出错了!为什么会这样呢?
事实上,如果抽象类的派生类中有任何一个抽象方法未定义,那么该导出类就同样是抽象类。编译器会强制我们用 abstract 关键字来指明该导出类是抽象类。
1.2.4 没有抽象方法的抽象类
抽象类可以没有抽象方法,这个特性记住吧,原书并未给出任何解释。
本节主要是介绍一下 没有抽象方法的抽象类,意义何在?
-
价值在于:不需要实例化,也不需要通过不同的对象来保存不同的状态。
因为该抽象类已经把方法都实现了。
-
使用场景:用在各种 工具类 中,如果它的所有方法都是静态的,那么定义为抽象的,就会避免我们实例化这个工具类。
2. 接口
接口 (interface) 使抽象的概念更进一步。
2.1 什么是接口(interface)
简单来说,接口(interface 关键字) 是一个完全抽象的类,它是抽象方法的集合。
2.1.1 特性
- 只是一种形式,本身不能做任何事情 -- 无法被实例化
- 实现类可以向上转型为接口,以此实现类似”多重继承“的特性
- 某类”实现“接口时,需要实现接口中全部的方法
- 接口中的方法默认为 public
- 接口中的域隐式地是 static 和 final 的
- 接口可以指明访问权限,类似 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() 的代码。
问题:
- Apply.process() 和 Processor 耦合度太高。
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 多个接口。
- 在接口中添加新方法
- 组合多个接口中的方法
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 接口的其他事项
接口与多重继承
- 一个实现类可以同时实现多个接口
-
实现类可以向上转型为接口
多重继承指 C++ 中的概念:组合多个类的接口的行为。
多重继承并非一个类同时继承多个类,不要混淆。
接口中的域:
- 两点:
- 接口中的任何域都自动是 static 和 final 的
- 接口中的域自动是public 的 。
- 初始化
- 接口中定义的域不能是 空final,但可以被非常量表达式初始化。
- 在类第一次被加载时初始化,发生在任何域首次被访问时。
接口的适配:
- 主要就是适配器模式,见 2.2.3 例3.
接口与工厂:
- 设计模式 - 工厂方法。
接口的嵌套
- 嵌套接口:定义在类或接口中的接口。
- 修饰符限制:
- 不论定义在接口,还是类中,嵌套接口默认强制是
static
。这意味着,嵌套接口是没有局部的嵌套接口。 - 接口定义在类中,可以使用四种访问权限,定义在接口种,则只有
public
- 不论定义在接口,还是类中,嵌套接口默认强制是
总结
任何抽象性都应该是应需求而产生的。必需时应该重构接口,而不是到处添加额外的类。
恰当的设计原则是优先选择类而不是接口。从类开始,如果接口变得必需,那么就进行重构。
本章结束,共勉。
网友评论