概念及定义
在 Java 程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
换一个角度来说,接口是一系列方法构成的一个特殊的“集合”,一个接口实现一个主要的功能,而这一系列方法共同完成了这个功能的实现。定义了接口以后,可以用接口对类的功能进行扩展,即给类附加这个这个接口所实现的功能。
// Comparable 接口
public interface Comparable
{
int compareTo(Object other);
}
// 将类声明为实现某个接口
class Employee implements Comparable
{
...
public int compareTo(Object otherObject)
{
Employee other = (Employee) otherObject;
return Double.compare(salary, other.salary);
}
}
接口的特点:
- 定义接口时,使用
interface
关键字。 - 接口定义中,不能含有实例域。
- 接口定义中,只声明方法,不实现方法,所有的方法均为
public
类型。 - 接口的所有方法实现均在类定义中完成。
- 接口扩展类时,使用
implements
关键字。
Comparable x; // OK
x = new Employee(. . .); // OK provided Employee implements Comparable
if (anObject instanceof Comparable) { . . . }
// 接口扩展
public interface Moveable
{
void move(double x, double y);
}
public interface Powered extends Moveable
{
double milesPerCallon();
double SPEED_LIHIT = 95; // a public static final constant
}
接口的特性:
- 接口不能进行实例化,但可以声明接口变量,并引用被该接口扩展的类。
- 可以使用
instanceof
关键字判断类是否被该接口扩展 - 与类相似,接口也可以用
extends
进行扩展。 - 接口中虽然不能定义实例域和方法实现,但可以声明并初始化静态常量。
- 一个类只能继承自一个超类,但可以扩展多个接口,这对于扩展类的功能实现了较大的便利性。
接口与抽象类
看到这里,我们会发现接口与抽象类有很多的相似点,比如:都是预先声明方法;不能够创建实例;都是在类中实现具体的方法······事实上二者还是存在一定差别的。
在介绍继承时就已经说明,继承表达的是 is-a
关系,即子类一定属于超类,但超类不一定是子类。抽象类定义的抽象方法均由子类来实现,这些方法表达的是类或其子类所特有的一系列方法。
而接口从命名也能看出(Comparable
、Movable
),表达的是某一特殊的功能,但不一定是该类所特有,可用于扩展多个完全不同的类,具体实现方法也可以根据类的特点进行调整。
接口与抽象类针对类的扩展最大的区别在于,一个类只能继承自一个抽象类(超类),但却可以同时拥有多个扩展接口。这一特性极大地提高了类功能扩展的便捷性和高效性。
静态方法与默认方法
静态方法
在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。
目前为止, 通常的做法都是将静态方法放在伴随类中。在标准库中, 你会看到成对出现的接口和实用工具类, 如 Collection
/Collections
或 Path
/Paths
。
在 Java SE 8 中,可以为 Path 接口增加以下方法:
public interface Path
{
public static Path get(String first, String... more)
{
return Fi1eSystems.getDefault().getPath(first, more);
}
}
这样一来,Paths
类就不再是必要的了。不过整个 Java 库都以这种方式重构也是不太可能的,但是实现你自己的接口时,不再需要为实用工具方法另外提供一个伴随类。
默认方法
可以为接口方法提供一个默认实现,必须用 default
修饰符标记这样一个方法。
public interface MouseListener
{
default void mousedieked(MouseEvent event) {}
default void mousePressed(MouseEvent event) {}
default void mouseReleased(MouseEvent event) {}
default void mouseEntered(MouseEvent event) {}
default void mouseExited(MouseEvent event) {}
}
如果希望在发生鼠标点击事件时得到通知,就要实现一个包含 5 个方法的接口。大多数情况下,你只需要关心其中的 1、2 个事件类型。在 Java SE 8 中,可以把所有方法声明为默认方法,这些默认方法什么也不做。这样一来,实现这个接口的程序员只需要为他们真正关心的事件覆盖相应的监听器。默认方法可以调用任何其他方法。
默认方法的一个重要用法是“接口演化” (interface evolution) ,以 Collection
接口为例,这个接口作为 Java 的一部分已经有很多年了。假设很久以前你提供了这样一个类:
public class Bag implements Collection
后来,在 JavaSE 8 中,又为这个接口增加了一个 stream
方法。假设 stream
方法不是一个默认方法。那么 Bag
类将不能编译,因为它没有实现这个新方法。为接口增加一个非默认方法不能保证 “源代码兼容”。
不过,假设不重新编译这个类,而只是使用原先的一个包含这个类的 JAR 文件。这个类仍能正常加载,尽管没有这个新方法。程序仍然可以正常构造 Bag
实例,不会有意外发生。不过,如果程序在一个 Bag
实例上调用 stream
方法,就会出现一个 AbstractMethodError
。
将方法实现为一个默认方法就可以解决这两个问题。Bag
类又能正常编译了。另外如果没有重新编译而直接加载这个类,并在一个 Bag
实例上调用 stream
方法,将调用 Collection.stream
方法。
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,这样会产生一个 “二义性” 冲突的问题。在 Java 中定义了这样的冲突解决优先级:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
interface Named
{
default String getName() { return getClass() .getName() + "_" + hashCodeO ; }
}
class Student implements Person, Named
{
public String getName()
{
return Person.super.getName();
}
...
}
类会继承 Person
和 Named
接口提供的两个不一致的 getName
方法。并不是从中选择一个,Java 编译器会报告一个错误,让程序员来解决这个二义性。只需要在 Student
类中提供一个 getName
方法。
Java 设计者更强调一致性。两个接口如何冲突并不重要。现在假设 Named
接口没有为 getName
提供默认实现,如果至少有一个接口提供了一个实现,编译器就会报告错误,而程序员就必须解决这个二义性。
class Student extends Person implements Named { . . . }
在这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略。在我们的例子中,Student
从 Person
继承了 getName
方法,Named
接口是否为 getName
提供了默认实现并不会带来什么区别。这正是 “类优先” 规则。
示例
接口与回调
回调 (callback) 是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。例如,可以指出在按下鼠标或选择某个菜单项时应该采取什么行动。
在 java.swing
包中有一个 Timer
类,可以使用它在到达给定的时间间隔时发出通告。例如,假如程序中有一个时钟,就可以请求每秒钟获得一个通告,以便更新时钟的表盘。在构造定时器时,需要设置一个时间间隔,并告之定时器,当到达时间间隔时需要做些什么操作。
定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了 java.awt.event
包的 ActionListener
接口。当到达指定的时间间隔时,定时器就调用 actionPerformed
方法。假设希望每隔 10 秒钟打印一条信息 “At the tone, the time is . . .”,然后响一声,就应该定义一个实现 ActionListener
接口的类,然后将需要执行的语句放在 actionPerformed
方法中。
public interface ActionListener
{
void actionPerfomed(ActionEvent event);
}
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is " + new Date());
Toolkit.getDefaultToolkit().beep();
}
}
接下来,构造这个类的一个对象,并将它传递给 Timer
构造器。Timer 构造器的第一个参数是发出通告的时间间隔,它的单位是毫秒,这里希望每隔 10 秒钟通告一次,第二个参数是监听器对象。最后,启动定时器。
ActionListener listener = new TimePrinter();
Timer t = new Timer(10000, listener);
t.start();
上述程序逻辑:
ActionListener
接口扩展TimePrinter
类,定义ActionListener
变量引用TimePrinter
类对象。- 构造
Timer
类对象,传入参数为定时时间和ActionListener
接口引用的对象。 - 计时到后,根据传入接口,回调接口声明的方法,并调用类对象中实现的该方法。
对象克隆
如果希望 copy 是一个新对象,它的初始状态与 original 相同, 但是之后它们各自会有自己不同的状态, 这种情况下就可以使用 clone
方法。
clone
方法是 Object
的一个 protected
方法,这说明你的代码不能直接调用这个方法。只有 Employee
类可以克隆 Employee
对象。Object
类该如何实现 clone
?它对于这个对象一无所知,所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。
不难看出,一般的 clone
方法都是按照浅拷贝的方式,而浅拷贝有可能会在某些不经意的地方带来意外错误。
关于浅拷贝和深拷贝
浅拷贝,如上图所示,新生成一个类对象,并将原对象的实例域全部赋给克隆对象的实例域。对于基本数据类型,此操作没有什么影响;对于实例域引用的其他对象,克隆对象对应的实例域会引用与原对象相同的类对象。当我们希望对克隆对象进行修改时,由于引用的是同一个对象,很可能也会将原对象的状态一同改变。
为了解决这个问题,我们需要自定义 clone
方法,实现深拷贝。所谓深拷贝,不仅仅生成克隆对象,对于克隆对象中的所有实例域也生成各自新的克隆对象。“深拷贝” 实现了真正意义上的“克隆”操作,即生成一个与原对象一模一样,但两者完全独立的克隆对象。
// 深拷贝样例
class Employee implements Cloneable
{
...
public Employee clone() throws CloneNotSupportedException
{
// call Object,clone0
Employee cloned = (Employee) super.clone();
// clone mutable fields
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
}
子类克隆
必须当心子类的克隆。例如,一旦为 Employee
类定义了 clone
方法,任何人都可以用它来克隆 Manager
对象。Employee
克隆方法能否完成工作取决于 Manager
类的域。
在这里是没有问题的,因为 bonus
域是基本类型。但是 Manager
可能会有需要深拷贝或不可克隆的域。不能保证子类的实现者一定会修正 clone
方法让它正常工作。出于这个原因,在 Object
类中 clone
方法声明为 protected
。不过,如果你希望类用户调用 clone
,就不能这样做。
参考资料:
- 《Java核心技术 卷1 基础知识》