接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。
抽象类,一种介于普通类和接口之间的折中手段。尽管你的第一想法是创建接口,但是对于构建具有属性和未实现方法的类来说,抽象类也是重要且必要的工具。你不可能总是使用纯粹的接口。
1、抽象方法和类
Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。下面是抽象方法的声明语法:
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的,否则,编译器会报错。
// interface/Basic.java
abstract class Basic {
abstract void unimplemented();
}
如果一个抽象类是不完整的,当试图创建这个类的对象时,Java 会怎么做呢?它不会创建抽象类的对象,所以我们只会得到编译器的错误信息。这样保证了抽象类的纯粹性,我们不用担心误用它。
如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字。
// interfaces/Basic2.java
abstract class Basic2 extends Basic {
int f() {
return 111;
}
abstract void g() {
// unimplemented() still not implemented
}
}
可以将一个不包含任何抽象方法的类指明为 abstract,在类中的抽象方法没啥意义但想阻止创建类的对象时,这么做就很有用。
为了创建可初始化的类,就要继承抽象类,并提供所有抽象方法的定义
// interfaces/Instantiable.java
abstract class Uninstantiable {
abstract void f();
abstract int g();
}
public class Instantiable extends Uninstantiable {
@Override
void f() {
System.out.println("f()");
}
@Override
int g() {
return 22;
}
public static void main(String[] args) {
Uninstantiable ui = new Instantiable();
}
}
留意 @Override 的使用。没有这个注解的话,如果你没有定义相同的方法名或签名,抽象机制会认为你没有实现抽象方法从而产生编译时错误。因此,你可能认为这里的 @Override 是多余的。但是,@Override 还提示了这个方法被覆写——我认为这是有用的,所以我会使用 @Override,即使在没有这个注解,编译器告诉我错误的时候。
接口只允许 public 方法,如果不加访问修饰符的话,接口的方法不是 friendly 而是 public。然而,抽象类允许每件事。除了 private abstract,因为这没有什么意义。
2、接口
使用 interface 关键字创建接口。
Java 8 允许接口包含默认方法和静态方法。
接口的基本概念仍然没变,介于类型之上、实现之下。接口与抽象类最明显的区别可能就是使用上的惯用方式。接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable,而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。
接口同样可以包含属性,这些属性被隐式指明为 static 和 final。
使用 implements 关键字使一个类遵循某个特定接口(或一组接口),它表示:接口只是外形,现在我要说明它是如何工作的。
2.1、默认方法
Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。默认方法比抽象类中的方法受到更多的限制,但是非常有用,我们将在“流式编程”一章中看到。
增加默认方法的极具说服力的理由是它允许在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。
interface InterfaceWithDefault {
void firstMethod();
void secondMethod();
default void newMethod() {
System.out.println("newMethod");
}
}
4.2、多继承
多年后的现在,Java 通过默认方法具有了某种多继承的特性。结合带有默认方法的接口意味着结合了多个基类中的行为。因为接口中仍然不允许存在属性(只有静态属性,不适用),所以属性仍然只会来自单个基类或抽象类,也就是说,不会存在状态的多继承。
结合多个源的实现。只要基类方法中的方法名和参数列表不同,就能工作得很好
4.3、接口中的静态方法
Java 8 允许在接口中添加静态方法。这么做能恰当地把工具功能置于接口中,从而操作接口,或者成为通用的工具。
// onjava/Operations.java
package onjava;
import java.util.*;
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 参数并按顺序运行它们。
5、抽象类和接口
抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。
有一条实际经验:
- 尽可能地抽象。因此,更倾向使用接口而不是抽象类。
- 只有当必要时才使用抽象类。
- 除非必须使用,否则不要用接口和抽象类。大多数时候,普通类已经做得很好,如果不行的话,再移动到接口或抽象类中。
6、完全解耦
当方法操纵的是一个类而非接口时,它就只能作用于那个类或其子类。
接口在很大程度上放宽了这个限制,因而使用接口可以编写复用性更好的代码。
7、使用继承扩展接口
通过继承,可以很容易在接口中增加方法声明,还可以在新接口中结合多个接口。这两种情况都可以得到新接口。
通常来说,extends 只能用于单一类,但是在构建接口时可以引用多个基类接口。注意到,接口名之间用逗号分隔。
覆写、实现和重载令人不快地搅和在一起带来了困难。同时,重载方法仅根据返回类型是区分不了的。当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。
8、接口适配
接口的一种常见用法是前面提到的策略设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法” ,这使得方法更加灵活,通用,并更具可复用性。
例如,类 Scanner 的构造器接受的是一个 Readable 接口(在“字符串”一章中学习更多相关内容)。你会发现 Readable 没有用作 Java 标准库中其他任何方法的参数——它是单独为 Scanner 创建的,因此 Scanner 没有将其参数限制为某个特定类。通过这种方式,Scanner 可以与更多的类型协作。如果你创建了一个新类并想让 Scanner 作用于它,就让它实现 Readable 接口,像这样:
import java.nio.*;
import java.util.*;
public class RandomStrings implements Readable {
private static Random rand = new Random(47);
private static final char[] CAPITALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
private static final char[] LOWERS = "abcdefghijklmnopqrstuvwxyz".toCharArray();
private static final char[] VOWELS = "aeiou".toCharArray();
private int count;
public RandomStrings(int count) {
this.count = count;
}
@Override
public int read(CharBuffer cb) {
if (count-- == 0) {
return -1; // indicates end of input
}
cb.append(CAPITALS[rand.nextInt(CAPITALS.length)]);
for (int i = 0; i < 4; i++) {
cb.append(VOWELS[rand.nextInt(VOWELS.length)]);
cb.append(LOWERS[rand.nextInt(LOWERS.length)]);
}
cb.append(" ");
return 10; // Number of characters appended
}
public static void main(String[] args) {
Scanner s = new Scanner(new RandomStrings(10));
while (s.hasNext()) {
System.out.println(s.next());
}
}
}
Readable 接口只需要实现 read() 方法(注意 @Override 注解的突出方法)。
假设你有一个类没有实现 Readable 接口,怎样才能让 Scanner 作用于它呢?下面是一个产生随机浮点数的例子:
// interfaces/RandomDoubles.java
import java.util.*;
public interface RandomDoubles {
Random RAND = new Random(47);
default double next() {
return RAND.nextDouble();
}
static void main(String[] args) {
RandomDoubles rd = new RandomDoubles(){};
for (int i = 0; i < 7; i++) {
System.out.println(rd.next() + " ");
}
}
}
我们可以再次使用适配器模式,但这里适配器类可以实现两个接口。因此,通过关键字 interface 提供的多继承,我们可以创建一个既是 RandomDoubles,又是 Readable 的类:
// interfaces/AdaptedRandomDoubles.java
// creating an adapter with inheritance
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() + " ");
}
}
}
接口中定义的字段不能是“空 final",但是可以用非常量表达式初始化。
这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域中。
接口嵌套
接口和工厂方法模式
接口是多实现的途径,而生成符合某个接口的对象的典型方式是工厂方法设计模式。不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。
// interfaces/Factories.java
interface Service {
void method1();
void method2();
}
interface ServiceFactory {
Service getService();
}
class Service1 implements Service {
Service1() {} // Package access
@Override
public void method1() {
System.out.println("Service1 method1");
}
@Override
public void method2() {
System.out.println("Service1 method2");
}
}
class Service1Factory implements ServiceFactory {
@Override
public Service getService() {
return new Service1();
}
}
class Service2 implements Service {
Service2() {} // Package access
@Override
public void method1() {
System.out.println("Service2 method1");
}
@Override
public void method2() {
System.out.println("Service2 method2");
}
}
class Service2Factory implements ServiceFactory {
@Override
public Service getService() {
return new Service2();
}
}
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 Service1Factory());
// Services are completely interchangeable:
serviceConsumer(new Service2Factory());
}
}
后面还可以看到一种更加优雅的使用匿名内部类的工厂实现方式