接口
接口的主要作用是将接口和实现分离的更加结构化的方法,它和抽象类十分相似。
抽象类
抽象类更像是介于普通类和接口直接的一种存在。
特点
- 如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。
- 如果一个抽象类是不完整的,当试图创建这个类的对象时,Java 不会创建抽象类的对象,所以我们只会得到编译器的错误信息。这样保证了抽象类的纯粹性,我们不用担心误用它。
- 如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字
- 可以将一个不包含任何抽象方法的类指明为 abstract,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用
- 为了创建可初始化的类,就要继承抽象类,并提供所有抽象方法的定义
从上可以发现抽象类本身就是用来继承的,也是某一种规范。这一点和接口很相似。
访问权限:
事实上的访问权限是“friendly”。接口会将方法致命为public并且只允许指定为public,不加任何修饰符的时候默认就是public。然而抽象类允许每件事。
但是将抽象方法修饰为private是没有意义的,因为正如前面所说抽象类本身就是用来继承的。private就会导致该方法在子类中无法被合法的定义。
此外抽象类和接口不同是的抽象类可以包含属性,并且非抽象方法可以引用这些方法。但是接口的属性默认就是final和static的,不支持对象状态。
接口
特点
java8之前接口只允许抽象方法(只能定义不能实现)。类似于下面:
// from <<on java 8>>
// interfaces/PureInterface.java
// Interface only looked like this before Java 8
public interface PureInterface {
int m1();
void m2();
double m3();
}
java8之前接口更像是一个纯粹的不带任何属性的抽象类。正如它的名字接口一样只是在定义规范。interface创建接口是需要加上关键字public,否则接口只有包访问权限。
接口可以包含属性但默认指明为static和final。
默认方法
deafault关键字是java8引入的关键字它可以让接口的方法拥有默认实现。但是由于笔者认为因为接口不含有对象转态的属性所以,本身限制还是比较大的。
interface InterfaceWithDefault {
void firstMethod();
void secondMethod();
default void newMethod() {
System.out.println("newMethod");
}
}
接口实现:
使用implements 关键字:
interface AnInterface {
void firstMethod();
void secondMethod();
}
public class AnImplementation implements AnInterface {
public void firstMethod() {
System.out.println("firstMethod");
}
public void secondMethod() {
System.out.println("secondMethod");
}
public static void main(String[] args) {
AnInterface i = new AnImplementation();
i.firstMethod();
i.secondMethod();
}
}
多继承
本身java是不支持多继承,原因是C++的影响(钻石继承问题)。但是引入默认方法之后,从那种程度上变向支持了多继承。但是因为接口本身的限制(接口不存在对象状态的属性)。所以多继承的只是方法而不是状态。这样也可以避免多继承的一些缺陷。
接口中的静态方法
Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具
public interface Operations {
void execute();
static void runOps(Operations... ops) {
for (Operations op: ops) {
op.execute();
}
}
static void show(String msg) {
System.out.println(msg);
}
}
书中说这是模板方法设计模式的一个版本。runOps()
是一个模版方法。runOps()
使用可变参数列表,因而我们可以传入任意多的 Operation 参数并按顺序运行它们:
class Bing implements Operations {
@Override
public void execute() {
Operations.show("Bing");
}
}
class Crack implements Operations {
@Override
public void execute() {
Operations.show("Crack");
}
}
class Twist implements Operations {
@Override
public void execute() {
Operations.show("Twist");
}
}
public class Machine {
public static void main(String[] args) {
Operations.runOps(
new Bing(), new Crack(), new Twist());
}
}
在中说:这个特性是一项改善,因为它允许把静态方法放在更合适的地方
我个人理解是静态方法本身只和类绑定,不随对象。因此对于只有静态方法的类来说。没有必要去创建对象,而接口就满足这个特点,因为接口不可以实例化。
这也只是我的粗浅理解。个人对这一部分还是不太理解,可能后面设计模式那边会有更深的体会,也希望读者有更好理解的可以告诉我。
抽象类和接口
区别
特性 | 接口 | 抽象类 |
---|---|---|
组合 | 新类可以组合多个接口 | 只能继承单一抽象类 |
状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 |
默认方法 和 抽象方法 | 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 |
构造器 | 没有构造器 | 可以有构造器 |
可见性 | 隐式 public | 可以是 protected 或友元 |
安全解耦
“当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类。如果想把方法应用于那个继承层级结构之外的类,就会触霉头。接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码”
// interfaces/Applicator.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) {
// split() divides a String into pieces:
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);
}
}
输出:
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]
我看到这个例子最开始的反映还是这不就是策略模式吗?为什么说接口可以更好的复用代码?紧接着如下例子:
public interface Processor {
default String name() {
return getClass().getSimpleName();
}
Object process(Object input);
}
// interfaces/interfaceprocessor/Applicator.java
package interfaces.interfaceprocessor;
public class Applicator {
public static void apply(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
}
遵循接口编写类
// interfaces/interfaceprocessor/StringProcessor.java
// {java interfaces.interfaceprocessor.StringProcessor}
package interfaces.interfaceprocessor;
import java.util.*;
interface StringProcessor extends Processor {
@Override
String process(Object input); // [1]
String S = "If she weighs the same as a duck, she's made of wood"; // [2]
static void main(String[] args) { // [3]
Applicator.apply(new Upcase(), S);
Applicator.apply(new Downcase(), S);
Applicator.apply(new Splitter(), S);
}
}
class Upcase implements StringProcessor {
// 返回协变类型
@Override
public String process(Object input) {
return ((String) input).toUpperCase();
}
}
class Downcase implements StringProcessor {
@Override
public String process(Object input) {
return ((String) input).toLowerCase();
}
}
class Splitter implements StringProcessor {
@Override
public String process(Object input) {
return Arrays.toString(((String) input).split(" "));
}
}
输出:
Using Processor Upcase
IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD
Using Processor Downcase
if she weighs the same as a duck, she's made of wood
Using Processor Splitter
[If, she, weighs, the, same, as, a, duck,, she's, made, of, wood]
说实话书中将Processor改成接口后我并没有看出复用性有提高。直到下面这个例子:
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);
}
}
Filter类与Processor类具有相同的接口元素,但是它不继承Processor。 因此你不能将 Applicator 的 apply()
方法应用在 Filter 类上。主要是因为Applicator apply()
方法和 Processor 过于耦合,这阻止了 Applicator 的 apply()
方法被复用。另外要注意的一点是 Filter 类中 process()
方法的输入输出都是 Waveform。
但是利用适配器模式就可以轻易产生需要的Processor接口的对象。并且协变允许我们从process()方法中产生一个WaceForm而非Object对象。到这里我才明白协变的意义。
但是在深入思考一下如果Processor类只是包含方法定义而没有实现。是否子类继承它之后依然可以使用适配器模式呢?经过笔者实验也是可以做到的呢。但是这样做我们会损失什么呢。或许正如书中作者所说“当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类。如果想把方法应用于那个继承层级结构之外的类,就会触霉头。”我们最终还是通过继承使其变成了Processor的子类。一个类可以实现多个接口却只能继承一个类。
多接口结合
如图一个类可以继承一个抽象类并且实现多个接口,且这个类皆可向上转型。
// interfaces/Adventure.java
// Multiple interfaces
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); // Treat it as a CanFight
u(h); // Treat it as a CanSwim
v(h); // Treat it as a CanFly
w(h); // Treat it as an ActionCharacter
}
}
Hero类就可以向上转型成为它所继承的类以及实现的接口。这样灵活性就很高,对于策略模式来说。因为很多时候不可能一个接口描述所有情况。更多的时候A类有x,y,z行为,但是B类只有x,y行为。所以利用多个接口组合是一个常用的套路。
这里作何顺便给出抽象类和接口如何选择的答案之一:应该使用接口还是抽象类呢?如果创建不带任何方法定义或成员变量的基类,就选择接口而不是抽象类。事实上,如果知道某事物是一个基类,可以考虑用接口实现它(这个主题在本章总结会再次讨论)。
接口适配
再次使用适配器模式,但这里适配器类可以实现两个接口。因此,通过关键字 interface 提供的多继承,我们可以创建一个既是 RandomDoubles,又是 Readable 的类:
import java.nio.*;
import java.util.*;
public class AdaptedRandomDoubles implements RandomDoubles, Readable {
private int count;
public AdaptedRandomDoubles(int count) {
this.count = count;
}
@Override
public int read(CharBuffer cb) {
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.print(s.nextDouble() + " ");
}
}
}
输出:
0.7271157860730044 0.5309454508634242
0.16020656493302599 0.18847866977771732
0.5166020801268457 0.2678662084200585
0.2613610344283964
因为你可以以这种方式在已有类中增加新接口,所以这就意味着一个接受接口类型的方法提供了一种让任何类都可以与该方法进行适配的方式。这就是使用接口而不是类的强大之处。
到这里我算是再次深刻体会到了接口的强大之处。结合策略模式,组合,适配器模式可以达到很高的复用性。
接口嵌套
这里是我感觉最迷惑的,看下例:
package interfaces.nesting;
class A {
interface B {
void f();
}
public class BImp implements B {
@Override
public void f() {}
}
public class BImp2 implements B {
@Override
public void f() {}
}
public interface C {
void f();
}
class CImp implements C {
@Override
public void f() {}
}
private class CImp2 implements C {
@Override
public void f() {}
}
private interface D {
void f();
}
private class DImp implements D {
@Override
public void f() {}
}
public class DImp2 implements D {
@Override
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 {
@Override
public void f() {}
}
class CImp implements A.C {
@Override
public void f() {}
}
// Cannot implements a private interface except
// within that interface's defining class:
//- class DImp implements A.D {
//- public void f() {}
//- }
class EImp implements E {
@Override
public void g() {}
}
class EGImp implements E.G {
@Override
public void f() {}
}
class EImp2 implements E {
@Override
public void g() {}
class EG implements E.G {
@Override
public void f() {}
}
}
public static void main(String[] args) {
A a = new A();
// Can't access to 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());
}
}
Dimp被用来实现一个private内部类。然而 receiveD(D d) 展示了它可以被实现为 public 类,但是 A.DImp2 只能被自己使用,你无法说它实现了 private 接口 D,所以实现 private 接口是一种可以强制该接口中的方法定义不会添加任何类型信息(即不可以向上转型)的方式。
上文中加黑加粗的部分,我一直无法理解,为什么不可以向上转型?难道是private接口不可被外部访问,所以只能被内部类进行实现或者内部接口继承。即使实现的类是public修饰的依然不可被外部引用。如果说不能向上转型的话,那么内部实现一个继承结构也是可以向上转型啊。实在无法彻底理解,还得再研究研究。
接口和工厂方法模式
interface Game {
boolean move();
}
interface GameFactory {
Game getGame();
}
class Checkers implements Game {
private int moves = 0;
private static final int MOVES = 3;
@Override
public boolean move() {
System.out.println("Checkers move " + moves);
return ++moves != MOVES;
}
}
class CheckersFactory implements GameFactory {
@Override
public Game getGame() {
return new Checkers();
}
}
class Chess implements Game {
private int moves = 0;
private static final int MOVES = 4;
@Override
public boolean move() {
System.out.println("Chess move " + moves);
return ++moves != MOVES;
}
}
class ChessFactory implements GameFactory {
@Override
public Game getGame() {
return new Chess();
}
}
public class Games {
public static void playGame(GameFactory factory) {
Game s = factory.getGame();
while (s.move()) {
;
}
}
public static void main(String[] args) {
playGame(new CheckersFactory());
playGame(new ChessFactory());
}
}
输出:
Checkers move 0
Checkers move 1
Checkers move 2
Chess move 0
Chess move 1
Chess move 2
Chess move 3
如果类 Games 表示一段很复杂的代码,那么这种方式意味着你可以在不同类型的游戏里复用这段代码。你可以再想象一些能够从这个模式中受益的更加精巧的游戏。
怎么理解上面这句话呢?我觉得还是接口和实现分离吧接口不和具体实现绑定。至于使用工厂一个常见的原因是创建框架。
小结
认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候,创建类都可以替代为创建一个接口和工厂。
很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。
确实读完这章我在哪里都想用接口和工厂,但是这就 陷入过早优化的陷阱。
任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。这种复杂性非常显著,如果你让某人去处
理这种复杂性,只是因为你意识到“以防万一”而添加新接口,而没有其他具有说服力的原因——好吧,如果我碰上了这种设计,就会质疑此人所作的所有其他设计了。
恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。
优先使用类,等到使用接口变得十分必要时,再进行重构。妄图一开始就有完美的设计是不可能的。笔者从这里受益匪浅,以前从想着一开始就设计好继承架构什么的。其实有时候也往往不必要那些东西,反而增加了复杂度。
随笔
好的设计都是重构出来的,重构应该发生在软件开发的时时刻刻,不知道这样认为对不对 哈哈哈哈哈哈。