接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
例如,Comparable 接口:
java.lang.Comparable
public interface Comparable {
int compareTo(Object o); // parameter has type T
}
// since Java SE 5.0
public interface Comparable<T> {
int compareTo(T o); // parameter has type T
// like to
public int compareTo(T o); // public 自动省略,且只能为 public
}
上述接口只有一个方法,而有些接口可以包含多个方法(Java SE 8 后,可以提供默认方法和静态方法),也可以定义常量。但是接口不能含有实例域。
提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实例域的抽象类。当然,抽象类还是和接口是由区别的。
Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了 Comparable 接口。例如,让 Employee 实现 Comparable 接口(比较 salary):
class Employee implements Comparable {
...
// 这里修饰符必须为 public ,因为 接口中的该方法也是 public (默认为 public)
public int compareTo(Object otherObject) {
Employee other = (Employee) otherObject;
return Double.compare(salary, other.salary);
}
}
当然还有泛型 Comparable 接口版本:
class Employee implements Comparable<Employee> {
...
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}
当然,目前最好使用泛型版本的。
在继承方面,可能会出现和 equals 的问题:
class Manager extends Employee {
...
// 因为 Employee 实现的是 Comparable<Employee>,所以子类实现 compareTo() 的参数也必须为 Employee
public int compareTo(Employee other) {
Manager other Manager = (Manager) other;
...
}
}
如果上述那样实现的话,就会违反「反对称」规则:e.compareTo(m)
没有问题,但 m.compareTo(e)
就会抛出 ClassCastException
解决方法与 equals 差不多,有两种:
- 如果比较方式不一样,就不运行不同类之间的比较,例如:
if (getClass() != other.getClass()) throw new ClassCastException();
- 如果子类,父类比较方式相同,就应该在父类中提供通用算法,并将方法声明为 final。
接口的特性
接口不是类,不能使用new运算符实例化一个接口,然而,尽管不能构造接口的对象,却能声明接口的变量。接口变量必须引用实现了接口的类对象。
x = new Comparable(...); // ERROR
Comparable x;
x = new Employee(...);
// or
Comparable x = new Employee(...);
if (x instanceof Comparable) // OK
当然,接口也可以继承:
假设有一个 Moveable 接口:
public interface Moveable {
void move(double x, double y);
}
然后拓展一个叫 Powered 的接口:
public interface Powered extends Moveable {
double milesPerGallon();
}
接口中不能包含实例域,但是可以包含常量,并且常量必须赋值:
public public interface Powered extends Moveable {
double milesPerGallon();
double SPEED_LIMIT = 95; // 这里必须赋值
// like
public static final double SPEED_LIMIT = 95;
}
尽管每个类只能够拥有一个超类,但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性。
接口与抽象类
抽象类基本可以完成和接口一样的事情,但是为什么还要有接口呢。这是因为抽象类一次只能拓展一个类,限制了使用。但是每个类可以实现多个接口。
静态方法
从 Java SE 8 开始,运行在接口中增加静态方法。
在之前,通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如 Collection/Collections 或 Path/Paths。
比如,在 Path 类中,其中只包含两个工厂方法。可以由一个字符串序列构造一个文件或目录的路径,如 Paths.get(“jdk1.8.0”,”jre”,”bin”)。在 Java SE 8 中,可以为 Path 接口增加以下方法:
public interface Path {
public static Path get(String first, String...more) {
return FileSystem.getDefault().getPath(first, more);
}
...
}
这样一来,Paths 类就不再是必要的了。
默认方法
可以为接口方法提供一个默认实现。必须用 default 修饰符标记这样一个方法:
public interface Comparable<T> {
default int compareTo(T o) { return 0; }
}
默认方法可以让你在实现该接口时,不用实现该方法,当然,也可以在实现该接口的类中,重新实现该默认方法。
默认方法还可以调用其他方法:
public interface Collection {
int size();
default boolean isEmpty() {
return size() == 0
}
...
}
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,Java 是这样解决的:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
比如有两个接口:
interface Named {
default String getName() {return getClass.getName() + "_" + HashCode();}
}
interface Person {
default String getName() {return "Preson";}
}
如果同时继承这两个接口,就必须覆盖 getName()
来解决二义性:
class Student implements Person, Named {
public String getName() {
return Person.super.getName();
}
}
如果两个接口都没有为共享方法提供默认实现,那么这里就不存在冲突。
另一种情况,一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法:
class Student extends Person implements Named { ... }
在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。这就是「类优先」规则。
千万不要让一个默认方法重新定义 Object 类中的某个方法。例如,不能为 toString 或 equals 定义默认方法,尽管对于 List 之类的接口这可能很有吸引力。由于「类优先」规则,这样的方法绝对无法超越 Object.toString 或 Objects.equals。