第9章 接口

接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法.

1. 抽象类和抽象方法

Java提供了一个叫做”抽象方法”的机制,这种方法是不完整的,仅有声明而没有方法体.下面是抽象方法声明所采用的语法 :

1
abstract void f();

包括抽象方法的类叫做抽象类,如果一个类包含了一个或多个抽象方法,该类必须被限定为抽象的.

如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义,如果不这样做,那么导出类也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类.

既然使某个类成为抽象类并不需要所有的方法都是抽象的,所有仅需将某些方法声明为抽象的即可.

Instrument

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//: interfaces/music4/Music4.java
// Abstract classes and methods.
package interfaces.music4;
import polymorphism.music.Note;
import static net.mindview.util.Print.*;
abstract class Instrument {
private int i; // Storage allocated for each
public abstract void play(Note n);
public String what() { return "Instrument"; }
public abstract void adjust();
}
class Wind extends Instrument {
public void play(Note n) {
print("Wind.play() " + n);
}
public String what() { return "Wind"; }
public void adjust() {}
}
class Percussion extends Instrument {
public void play(Note n) {
print("Percussion.play() " + n);
}
public String what() { return "Percussion"; }
public void adjust() {}
}
class Stringed extends Instrument {
public void play(Note n) {
print("Stringed.play() " + n);
}
public String what() { return "Stringed"; }
public void adjust() {}
}
class Brass extends Wind {
public void play(Note n) {
print("Brass.play() " + n);
}
public void adjust() { print("Brass.adjust()"); }
}
class Woodwind extends Wind {
public void play(Note n) {
print("Woodwind.play() " + n);
}
public String what() { return "Woodwind"; }
}
public class Music4 {
// Doesn't care about type, so new types
// added to the system still work right:
static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*///:~

创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器计划怎么来使用它.

2. 接口

interface关键字使抽象的概念更接近了一步,abstract关键字允许人们在类中创建一个或多个没有任何定义的方法(提供了接口的方法),但是没有提供任何相应的具体实现,这些实现是由此类的继承者创建.interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现.它允许创建者确定方法名,参数列表和返回类型,但是没有任何方法体,接口只提供了形式,而未提供任何具体实现.

一个接口表示 : 所有实现了该特定接口的类看起来都像这样.因此,任何使用某特定接口的代码都知道可以调用该接口的那些方法,而且仅需知道这些.因此,接口被用来建立类与类之间的协议.

但是,interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继承变种的特性.

想要创建一个接口,需要用interface关键字来替代class关键字.就像类一样,可以在interface关键字前面加public关键字,如果不添加public关键字,则它只具有包访问权限,这样它就只能在同一个包内可用.接口也可以包含域,但是这些域隐式地是staticfinal的.

要让类遵循某个特定接口,需要使用implements关键字,它表示 : interface只是它的外貌,但是现在我要声明它是如何工作的.除此之外,它看起来比较像继承.

interface
可以从Woodwind和Brass类中看到,一旦实现了某些接口,其实现就变成了一个普通的类,就可以按常规方法扩展它.

3. 完全解耦

只有一个方法操作的是类而非接口,那么你就只能使用这个类及其子类.如果你想要将这个方法应用于不在此继承结构中的某个类,那么就不行了.接口可以在很大程度上放宽这种限制.

例如,假设有一个Processor类,它由一个name()方法;另外还有一个Process()方法,该方法接受输入参数,修改它的值,然后产生输出.这个类作为基类而被扩展,用来创建各种不同类型的Processor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//: interfaces/classprocessor/Apply.java
package interfaces.classprocessor;
import java.util.*;
import static net.mindview.util.Print.*;
class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) { return input; }
}
class Upcase extends Processor {
String process(Object input) { // Covariant return
return ((String)input).toUpperCase();
}
}
class Downcase extends Processor {
String process(Object input) {
return ((String)input).toLowerCase();
}
}
class Splitter extends Processor {
String process(Object input) {
// The split() argument divides a String into pieces:
return Arrays.toString(((String)input).split(" "));
}
}
public class Apply {
public static void process(Processor p, Object s) {
print("Using Processor " + p.name());
print(p.process(s));
}
public static 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);
}
} /* Output:
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对象上,然后打印结果.像本例这样,创建一个能够根据所传递参数对象的不同而具有不同行为的方法,被称为策略设计模式.这类方法包含所要执行的算法中固定不变的部分,而”策略”包含变化的部分.策略就是传递进去的参数对象,它包含要执行的代码.这里,Processor对象就是一个策略,在main()中可以看到有三种不同类型的策略应用到了String类型的s对象上.

split()方法是Stirng类的一部分,它接受String类型的对象,并以传递进来的参数作为边界,并将String对象分割开,然后返回一个数组String[],它在这里被用来当作创建String数组的快捷方式.

4. Java中的多重继承

接口不仅仅只是一种更纯粹形式的抽象类,因为接口是根本没有任何具体实现的,也就是说,没有任何与接口相关的存储;在C++中,组合多个类的接口被称作为多重继承.但在java中,你可以执行相同的行为,但是只有一个类可以有具体实现,因此,通过组合多个接口.

在导出类中,不强制要求必须有一个是抽象的或”具体的”基类.如果要从一个非接口的类继承,那么只能有一个类去继承.其余的基元素都必须是接口.需要将所有的接口名都置于implements关键字之后,用逗号将它们一一分割开.可以继承任意多个接口,并可以向上转型为每个接口.因为每一个接口都是一个独立的类型.

5. 通过继承来扩展接口

通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//: interfaces/HorrorShow.java
// Extending an interface with inheritance.
interface Monster {
void menace();
}
interface DangerousMonster extends Monster {
void destroy();
}
interface Lethal {
void kill();
}
class DragonZilla implements DangerousMonster {
public void menace() {}
public void destroy() {}
}
interface Vampire extends DangerousMonster, Lethal {
void drinkBlood();
}
class VeryBadVampire implements Vampire {
public void menace() {}
public void destroy() {}
public void kill() {}
public void drinkBlood() {}
}
public class HorrorShow {
static void u(Monster b) { b.menace(); }
static void v(DangerousMonster d) {
d.menace();
d.destroy();
}
static void w(Lethal l) { l.kill(); }
public static void main(String[] args) {
DangerousMonster barney = new DragonZilla();
u(barney);
v(barney);
Vampire vlad = new VeryBadVampire();
u(vlad);
v(vlad);
w(vlad);
}
} ///:~

DangerousMonster 是Monster的直接扩展,它产生了一个新接口.DragonZilla中实现了这个接口.

在Vampire中使用的语法仅适用于接口继承.一般情况下,只可以将extends用于单一类,但是可以引用多个基类接口.只需要用逗号将接口名一一分割开即可.

5.1 组合接口时的名字冲突

在打算组合的不同接口中使用相同的方法名通常会造成代码可读性的混乱,请尽量避免这种情况.

6. 适配接口

接口的一种常见用法就是策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口.你主要的声明就是 : 你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口.

7. 接口中的域

因为我们放入接口中的任何域都自动是static和final的,所以接口就成为一种很便捷的用来创建常量组的工具.

7.1 初始化接口中的域

在接口中定义的域不能为”空final”,但是可以被非常量表达式初始化,如 :

1
2
3
4
5
6
7
8
9
10
11
12
//: interfaces/RandVals.java
// Initializing interface fields with
// non-constant initializers.
import java.util.*;
public interface RandVals {
Random RAND = new Random(47);
int RANDOM_INT = RAND.nextInt(10);
long RANDOM_LONG = RAND.nextLong() * 10;
float RANDOM_FLOAT = RAND.nextLong() * 10;
double RANDOM_DOUBLE = RAND.nextDouble() * 10;
} ///:~

既然域是static的,它们就可以在类第一次被加载时初始化,这发生在任何域首次被访问时.这就给了一个简单的测试 : 以下的例子证明了接口中的域隐式地是static和final

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//: interfaces/TestRandVals.java
import static net.mindview.util.Print.*;
public class TestRandVals {
public static void main(String[] args) {
print(RandVals.RANDOM_INT);
print(RandVals.RANDOM_LONG);
print(RandVals.RANDOM_FLOAT);
print(RandVals.RANDOM_DOUBLE);
}
} /* Output:
8
-32032247016559954
-8.5939291E18
5.779976127815049
*///:~

当然,这些域不是接口的一部分,它们的值被存储在该接口的静态存储区域内.

8. 嵌套接口

接口可以嵌套在类或其他接口中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//: interfaces/nesting/NestingInterfaces.java
package interfaces.nesting;
class A {
interface B {
void f();
}
public class BImp implements B {
public void f() {}
}
private class BImp2 implements B {
public void f() {}
}
public interface C {
void f();
}
class CImp implements C {
public void f() {}
}
private class CImp2 implements C {
public void f() {}
}
private interface D {
void f();
}
private class DImp implements D {
public void f() {}
}
public class DImp2 implements D {
public void f() {}
}
public D getD() { return new DImp2(); }
private D dRef;
public void receiveD(D d) {
dRef = d;
dRef.f();
}
}
interface E {
interface G {
void f();
}
// Redundant "public":
public interface H {
void f();
}
void g();
// Cannot be private within an interface:
//! private interface I {}
}
public class NestingInterfaces {
public class BImp implements A.B {
public void f() {}
}
class CImp implements A.C {
public void f() {}
}
// Cannot implement a private interface except
// within that interface's defining class:
//! class DImp implements A.D {
//! public void f() {}
//! }
class EImp implements E {
public void g() {}
}
class EGImp implements E.G {
public void f() {}
}
class EImp2 implements E {
public void g() {}
class EG implements E.G {
public void f() {}
}
}
public static void main(String[] args) {
A a = new A();
// Can't access A.D:
//! A.D ad = a.getD();
// Doesn't return anything but A.D:
//! A.DImp2 di2 = a.getD();
// Cannot access a member of the interface:
//! a.getD().f();
// Only another A can do anything with getD():
A a2 = new A();
a2.receiveD(a.getD());
}
} ///:~

特别注意的是,当实现某个接口时,并不需要实现嵌套在其内部的任何接口.而且,private接口不能在定义它的类之外被实现.

9. 接口与工厂

接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//: interfaces/Factories.java
import static net.mindview.util.Print.*;
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
Implementation1() {} // Package access
public void method1() {print("Implementation1 method1");}
public void method2() {print("Implementation1 method2");}
}
class Implementation1Factory implements ServiceFactory {
public Service getService() {
return new Implementation1();
}
}
class Implementation2 implements Service {
Implementation2() {} // Package access
public void method1() {print("Implementation2 method1");}
public void method2() {print("Implementation2 method2");}
}
class Implementation2Factory implements ServiceFactory {
public Service getService() {
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());
// Implementations are completely interchangeable:
serviceConsumer(new Implementation2Factory());
}
} /* Output:
Implementation1 method1
Implementation1 method2
Implementation2 method1
Implementation2 method2
*///:~

如果不用工厂方法,代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器.

10. 总结

“确定接口是理想选择,因而应该总是选择接口而不是具体的类”.这其实是一种引诱.当然,对于创建类,几乎在任何时刻,都可以替代为创建一个接口和工厂.

适当的原则应该是优先选择类而不是接口,从类开始,如果接口的必须性变得非常明确,那么就进行重构.接口是一种重要的工具,但是它非常容易被滥用.