接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。
一、抽象类和抽象方法
如果我们只有一个抽象类,那么该类的对象几乎没有任何意义。我们创建抽象类是希望通过这个通用接口操纵一系列类。因此,抽象类只是表示了一个接口,没有具体的实现内容;因此创建一个抽象类对象没有什么意义,并且我们可能还想阻止使用者这样做。通过让抽象类中的所有方法都产生错误,就可以实现这个目的。但是这样做会将错误信息延迟到运行时才获得,并且需要在客户端进行可靠、详尽的测试。所以最好在编译时捕获这些问题。
为此,Java提供了一个叫做抽象方法的机制,这种方法是不完整的;仅有声明而没有方法体。
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的(否则,编译器就会报错)。如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有方法提供定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。
二、接口
abstract关键字允许人们在类中创建一个或多个没有任何定义的方法——提供了接口部分,但是没有提供任何相应的具体实现,这些实现是由此类的继承者创建的。interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现。它允许创建者确定方法名、参数列表和返回类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现。
一个接口表示:“所有实现了该特定接口的类看起来都像这样“。因此,任何使用某特定接口的代码都知道可以调用该接口的那些方法,而且仅需要知道这些。”
interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特性。
interface的创建规则和class一样,接口也可以包含域,但是这些域隐式地是static和final的。
要让一个类遵循某个特定接口(或者是一组接口),需要使用implements关键字,它表示“interface只是它的外貌,但是现在我要声明它是如何工作的。”除此之外,它看起来还是很像继承。
一旦实现了某个接口,其实现就变成了一个普通的类,就可以按常规方式扩展它。
可以选择在接口中显示地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为是public的;否则,它们将只能得到默认的包访问权限,这样在方法继承过程中,其可访问权限就被降低了,这是Java编译器所不允许的。
toString()方法是根类Object的一部分,因此它不需要出现在接口中。
三、完全解耦
只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想要将这个方法应用于不在此继承结构中的某个类,那么你就会触霉头。接口可以在很大程度上放宽这种限制,因此,它使得我们可以编写可复用性更好的代码。
策略设计模式
//: interfaces/classprocessor/Apply.java
package interfaces.classprocessor;
import java.util.*;
class Processor {
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) {
return input;
}
}
class Upcase extends Processor {
String process(Object input) { // Covariant
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) {
System.out.println("Using Processor " + p.name());
System.out.println(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);
}
}
Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上。在上面的代码中,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里,Peocessor对象就是一个策略,在main()中可以看到有三种不同类型的策略应用到了String类型的s对象上。
四、Java中的多重继承
接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这要高。因为接口是根本没有任何具体实现的——也就是说,没有任何与接口相关的存储;因此,也就无法阻止多个接口的组合。有时你需要去表示“一个x是一个a和一个b以及一个c”。在C++中组合多个类的接口的行为被称作多重继承。
在导出类中,不强制要求必须有一个是抽象的或“具体的”(没有任何抽象方法的)基类。如果要从一个非接口的类继承,那么只能从一个类去继承。其余的基元素都必须是接口。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。
使用接口的核心原因:为了能够向上转型为多个基类型(以及由此带来的灵活性)。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该成为一个基类,那么第一选择应该是使它成为一个接口。
五、通过继承来扩展接口
通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合多个接口。
六、适配接口
接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单情况中,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。
因此,接口的一种常见用法就是策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你只要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口。“ 这使得你的方法更加灵活、通用,并更可复用性。
七、接口中的域
因为你放入接口中的任何域都自动是static和final的,所以接口就成为了一种很便捷的用来组织常量的工具。接口中的域自动是public的。在接口中定义的域不能是”空白final“,但是可以被非常量表达式初始化。
八、嵌套接口
接口可以嵌套在类或其他接口中。
在类中嵌套接口的语法是相当显而易见的,就像非嵌套接口一样,可以拥有public和”包访问“两种可视性。
作为一种新添加的方式,接口也可以被实现为private的,实现一个private接口只是一种方式,它可以强制该接口中的方法定义不要添加任何类型信息(也就是说,不允许向上转型)。
接口彼此之间也可以嵌套。然而,作用于接口的各种规则,特别是所有的接口元素都必须是public的,在此都会被严格执行。因此,嵌套在另一个接口中的接口自动就是public的,而不能声明为private的。
九、接口与工厂
接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂设计模式,这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现对象。
//: interfaces/Factories.java
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Implementation1 implements Service {
Implementation1() {} // Package access
public void method1() {
System.out.println("Implementation1 method1");
}
public void method2() {
System.out.println("Implementation1 method2");
}
}
class Implementation1Factory implements ServiceFactory {
public Service getService() {
return new Implementation1();
}
}
class Implementation2 implements Service {
Implementation2() {} // Package access
public void method1() {
System.out.println("Implementation2 method1");
}
public void method2() {
System.out.println("Implementation2 method2");
}
}
class Implementation2Factory implements ServiceFactory {
public Service getService() {
return new Implementation2();
}
}
public class Factories {
public static void serviceConsmer(ServiceFactory fact) {
Service s= fact.getService(); // 在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象
s.method1();
s.method2();
}
public static void main(String[] args) {
serviceConsmer(new Implementation1Factory());
// Implementations are completely interchangeable:
serviceConsmer(new Implementation2Factory());
}
}
十、总结
”确定接口是理想选择,因而应该总是选择接口而不是具体的类。“ 这其实是一种引诱。当然,对于创建类,几乎在任何时刻,都可以替代为创建一个接口和一个工厂。许多人都掉进了这种诱惑的陷阱,只要有可能就去创建接口和工厂。这种逻辑看起来好像是因为需要使用不同的具体实现,因此总是应该添加这种抽象性。这实际上已经变成了一种草率的设计优化。任何抽象性都应该是应真正需求而产生的。当必需时,你应该重构接口而不是到处添加额外级别的间接性,并由此带来额外的复杂性。
恰当的选择应该优先选择类而不是接口。从类开始,如果接口的必需性变得非常明确,那么就进行重构。