- 接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。
这种机制在编程语言中不常见,例如 C++ 只对这种概念有间接的支持。而在 Java 中存在这些关键字,说明这些思想很重要,Java 为它们提供了直接支持。
抽象类和方法
- Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。
abstract class Basic {
abstract void unimplemented();
}
如果一个抽象类是不完整的(只有抽象方法),当试图创建这个类的对象时,Java 不会创建抽象类的对象,我们会获得编译错误信息,通过这种方式
完全解耦
import java.util.*;
class Processor {
public String name() {
return getClass().getSimpleName();
}
public Object process(Object input) {
return input;
}
}
class Upcase extends Processor {
@Override
public String process(Object input) {
return ((String) input).toUpperCase();
}
}
class Downcase extends Processor {
@Override
public String process(Object input) {
return ((String) input).toLowerCase();
}
}
class Splitter extends Processor {
@Override
public String process(Object input) {
return Arrays.toString(((String) input).split(" "));
}
}
public class Applicator {
public static void apply(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
public static void main(String[] args) {
String s = "We are such stuff as dreams are made on";
apply(new Upcase(), s);
apply(new Downcase(), s);
apply(new Splitter(), s);
}
}
Applicator 的 apply() 方法可以接受任何类型的 Processor,并将其应用到一个 Object 对象上输出结果。像本例中这样,创建一个能根据传入的参数类型从而具备不同行为的方法称为策略设计模式。方法包含算法中不变的部分,策略包含变化的部分。策略就是传入的对象,它包含要执行的代码。在这里,Processor 对象是策略,main() 方法展示了三种不同的应用于 String s 上的策略。
输出:
Using Processor Upcase
WE ARE SUCH STUFF AS DREAMS ARE MADE ON
Using Processor Downcase
we are such stuff as dreams are made on
Using Processor Splitter
[We, are, such, stuff, as, dreams, are, made, on]
类库是被发现而不是创建的情况下,可以使用适配器设计模式(类的适配、对象的适配 联想万能充电器)。适配器允许代码接受已有的接口产生需要的接口,如下:
package interfaces.interfaceprocessor;
import interfaces.filters.*;
class FilterAdapter implements Processor {
Filter filter;
FilterAdapter(Filter filter) {
this.filter = filter;
}
@Override
public String name() {
return filter.name();
}
@Override
public Waveform process(Object input) {
return filter.process((Waveform) input);
}
}
public class FilterProcessor {
public static void main(String[] args) {
Waveform w = new Waveform();
Applicator.apply(new FilterAdapter(new LowPass(1.0)), w);
Applicator.apply(new FilterAdapter(new HighPass(2.0)), w);
Applicator.apply(new FilterAdapter(new BandPass(3.0, 4.0)), w);
}
}
输出:
Using Processor LowPass
Waveform 0
Using Processor HighPass
Waveform 0
Using Processor BandPass
Waveform 0
*在这种使用适配器的方式中,FilterAdapter 的构造器接受已有的接口 Filter,通俗来看就是在适配器中引入一个已有的接口
多接口结合
为解决“一个 x 是一个 a 和一个 b 以及一个 c问题”使用多接口结合,因为类只能继承一个,但是接口可以实现多个。所有的接口名称置于 implements 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个由多个接口组合而成的具体类产生的新类:
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 中这种接口的设计方式,使得程序员不需要付出特别的努力。
记住,前面例子展示了使用接口的核心原因之一:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因与使用抽象基类相同:防止客户端程序员创建这个类的对象,确保这仅仅只是一个接口。这带来了一个问题:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它(这个主题在本章总结会再次讨论)。
使用继承扩展接口
结合接口时的命名冲突 完全相同的方法没有问题,但是如果它们的签名或返回类型不同会报错
interface I1 {
void f();
}
interface I2 {
int f(int i);
}
interface I3 {
int f();
}
class C {
public int f() {
return 1;
}
}
class C2 implements I1, I2 {
@Override
public void f() {}
@Override
public int f(int i) {
return 1;
}
}
class C3 extends C implements I2 {
@Override
public int f(int i) {
return 1;
}
}
class C4 extends C implements I3 {
@Override
public int f() {
return 1;
}
}
重载方法仅根据返回类型是区分不了的。当不注释最后两行时,报错信息如下:
error: C5 is not abstract and does not override abstract
method f() in I1
class C5 extends C implements I1 {}
error: types I3 and I1 are incompatible; both define f(),
but with unrelated return types
interfacce I4 extends I1, I3 {}
在组合接口的过程中,应注意不同接口不同方法的命名要不同
接口适配
接口最吸引人的原因之一是相同的接口可以有多个实现。在简单情况下体现在一个方法接受接口作为参数,该接口的实现和传递对象给方法则交由你来做。
接口字段
因为接口中的字段都自动是 static 和 final 的,所以接口就成为了创建一组常量的方便的工具。在 Java 5 之前,用接口来定义枚举,自Java5 以后 有了enum关键字来定义枚举
Java 中使用大写字母的风格定义具有初始化值的 static final 变量。接口中的字段自动是 public 的,所以没有显式指明这点。 因为字段是 static 的,所以它们在类第一次被加载时初始化,这发生在任何字段首次被访问时。下面是个简单的测试:
public class TestRandVals {
public static void main(String[] args) {
System.out.println(RandVals.RANDOM_INT);
System.out.println(RandVals.RANDOM_LONG);
System.out.println(RandVals.RANDOM_FLOAT);
System.out.println(RandVals.RANDOM_DOUBLE);
}
}
输出:
8
-32032247016559954
-8.5939291E18
5.779976127815049
这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域中。
总结
认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候,创建类都可以替代为创建一个接口和工厂
|