1.抽象类和方法
Java 提供了一个叫做抽象方法的机制,这个方法是不完整的:它只有声明没有方法体。 下面是抽象方法的声明语法:
abstract void f();
包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,那么类本身也必须限定为抽象的, 否则,编译器会报错。如:
abstract class Basic {
abstract void unimplemented();
}
注意:当试图创建这个类的对象时,Java并不会创建抽象类的对象。
如果创建一个继承抽象类的新类并为之创建对象,那么就必须为基类的所有抽象方法提供方法定义。 如果不这么做(可以选择不做),新类仍然是一个抽象类,编译器会强制我们为新类加上 abstract 关键字。
abstract class Basic2 extends Basic {
int f() {
return 111;
}
abstract void g() {
// unimplemented() still not implemented
}
}
如果不想创建一个类的对象时,可以将该类(可以不包含任何抽象方法)指明为 abstract。
而为了创建继承于抽象类的新类,这个新类必须提供所有抽象方法的定义。
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(); //可实例化类 Instantiable
}
}
创建抽象类和抽象方法是有帮助的,因为它们使得类的抽象性很明确,并能告知用户和编译器使用意图。 抽象类同时也是一种有用的重构工具,使用它们使得我们很容易地将沿着继承层级结构上移公共方法。
2.接口创建
2.1 Java 8 之前
Java 8 之前的接口只允许创建抽象方法(不用为方法加上 abstract 关键字)。如:
public interface PureInterface {
int m1();
void m2();
double m3();
}
在 Java 8 之前我们可以这么说:interface 关键字产生一个完全抽象的类,没有提供任何实现。 我们只能描述类应该像什么,做什么,但不能描述怎么做,即只能决定方法名、参数列表和返回类型, 但是无法确定方法体。接口只提供形式,通常来说没有实现,只在某些受限制的情况下可以有实现。
2.2 Java 8
Java 8 允许接口包含默认方法和静态方法。接口的基本概念仍然没变,介于类型之上、实现之下。 接口与抽象类最明显的区别可能就是使用上的惯用方式。 接口的典型使用是代表一个类的类型或一个形容词,如 Runnable 或 Serializable, 而抽象类通常是类层次结构的一部分或一件事物的类型,如 String 或 ActionHero。
2.3 接口实例
interface Concept { // Package access
void idea1();
void idea2();
}
class Implementation implements Concept {
@Override
public void idea1() {
System.out.println("idea1");
}
@Override
public void idea2() {
System.out.println("idea2");
}
}
3.默认方法
上面提过:如果我们在接口中增加一个新方法 newMethod(),
而在实现接口的类中没有实现它,编译器就会报错。
Java 8 为关键字 default 增加了一个新的用途(之前只用于 switch 语句和注解中)。 当在接口中使用它时,任何实现接口却没有定义方法的时候可以使用 default 创建的方法体。如:
interface InterfaceWithDefault {
void firstMethod();
void secondMethod();
default void newMethod() {
System.out.println("newMethod");
}
}
public class AnImplementation implements InterfaceWithDefault {
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();
}
}
尽管 Implementation2 中未定义 newMethod(),但是可以使用 newMethod() 了。
默认方法在不破坏已使用接口的代码的情况下,在接口中增加新的方法。默认方法有时也被称为守卫方法或虚拟扩展方法。
4.多继承
多继承意味着一个类可能从多个父类型中继承特征和特性。
Java 过去是一种严格要求单继承的语言:只能继承自一个类(或抽象类),但可以实现任意多个接口。
Java 通过默认方法具有了某种多继承的特性。结合带有默认方法的接口意味着结合了多个基类中的行为。 因为接口中仍然不允许存在属性(只有静态属性,不适用),所以属性仍然只会来自单个基类或抽象类, 也就是说,不会存在状态的多继承。如:
import java.util.*;
interface One {
default void first() {
System.out.println("first");
}
}
interface Two {
default void second() {
System.out.println("second");
}
}
interface Three {
default void third() {
System.out.println("third");
}
}
class MI implements One, Two, Three {}
public class MultipleInheritance {
public static void main(String[] args) {
MI mi = new MI();
mi.first();
mi.second();
mi.third();
}
}
另外两个接口中有相同的方法名但是签名不同——方法签名包括方法名和参数类型,编译器可以区分两种方法。类可以成功实现两个接口。 但返回类型不是方法签名的一部分,因此不能用来区分方法。两个接口中有相同的方法名签名相同,返回类型不同,编译器无法区分两种方法,编译失败。 为了解决这个问题,需要覆写冲突的方法。如:
import java.util.*;
interface Jim1 {
default void jim() {
System.out.println("Jim1::jim");
}
}
interface Jim2 {
default void jim() {
System.out.println("Jim2::jim");
}
}
public class Jim implements Jim1, Jim2 {
@Override
public void jim() {
Jim2.super.jim();
}
public static void main(String[] args) {
new Jim().jim();
}
}
5.接口中的静态方法
Java 8 允许在接口中添加静态方法,这么做可以将工具功能置于接口中。如:
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);
}
}
import java.util.*;
import Operations;
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());
}
6.抽象类和接口
Java 8 引入 default 方法之后,选择用抽象类还是用接口变得令人困惑。下表做了明确的区分:
| 特性 | 接口 | 抽象类 |
|---|---|---|
| 组合 | 新类可以组合多个接口 | 只能继承单一抽象类 |
| 状态 | 不能包含属性(除了静态属性,不支持对象状态) | 可以包含属性,非抽象方法可能引用这些属性 |
| 默认方法 & 抽象方法 | 不需没有构造器要在子类中实现默认方法。默认方法可以引用其他接口的方法 | 必须在子类中实现抽象方法 |
| 构造器 | 没有构造器 | 可以有构造器 |
| 可见性 | 隐式 public | 可以是 protected 或友元 |
实际经验:尽可能地抽象。
- 更倾向使用接口而不是抽象类,只有当必要时才使用抽象类。
- 除非必须使用,否则不要用接口和抽象类。大多数时候,普通类足够完成任务,如果不行的话,再考虑移动到接口或抽象类中。
7.总结
几乎任何时候,创建类都可以替代为创建一个接口和工厂。很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。 这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。
任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构,而不是到处添加额外的间接层,从而带来额外的复杂性。
恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。 接口是一个伟大的工具,但它们容易被滥用。