1. 为什么使用接口
Java中的接口是一组对需求的描述。例如,以下是Comparable<T>接口的定义:
public interface Comparable<T> { int compareTo(T o); }
Comparable<T>接口中定义了一个compareTo方法,这个方法就是它所描述的需求。若我们想调用Arrays.sort方法对一个People对象数组进行比较,那么People对象必须是”可比较的”,即People类需要实现Comparable<T>接口。接口的实现类需要实现接口中定义的方法。也就是说,接口描述了一组需求,而实现一个接口的类就需要实现这个接口所描述的需求。比如我们想要People类对象是可比较的,我们可以这样:
public class People implements Comparable<People> { ... public int compareTo(People p) { //定义具体的比较标准 } ... }
实际上,Arrays.sort方法之所以要求它所比较的对象需实现了Comparable接口,是因为它在比较对象时调用的对象的compareTo方法(因为它不知道评价一个对象大小的标准,这个标准是由我们来定的)。那么,我们为什么不直接在People类中定义一个compareTo实例方法来定义People对象的比较标准,而是要去实现一个Comparable<T>接口呢?让我们举例来说明以下,假如我们调用了以下代码来对People对象数组peoples进行比较:
Arrays.sort(peoples);
在sort方法内部,实际上调用了类似下面这样的代码来比较People对象:
if (peoples[i].compareTo(peoples[j]) > 0) { //若为true说明peoples[i] 大于peoples[j] }
也就是说Arrays.sort方法内部调用了People对象的compareTo方法,那么编译器如何知道People类中确实定义了一个方法呢?若People类没有实现Comparable接口,编译器就只有检查这个类是否实现了这个方法,这样做无疑会增大开销,若接口中的方法不只一个,开销就更大了。而且需要比较的对象可能不只一个,假如后面又有Date对象、Job对象需要我们比较呢?如果每次调用相应对象的compareTo方法都要去检查以下它究竟实现了这个方法没有,将无形中增加很多本不必要的开销。
反过来,我们看看引入接口的好处。People类头部的"implements Comparable<People>“就像是在告诉编译器”我是可比较的,可以直接调用我的compareTo方法而不用检查我有没有这个方法“。这样一来,所有可比较的对象只要实现Comparable<T>接口,编译器就能够知道它一定定义了compareTo方法。一个接口实际上是描述了一种规范,它只说明了这个接口需要实现什么需求,而没有强制规定这个需求如何实现。就像之前我们提到的Comparable<T>接口,需要比较大小的对象不只People对象一个,既然大家都需要比较大小,那索性就来个规定,所有想比较大小的对象的类都实现Compareble<T>接口,统一规定一下可比较的对象究竟该满足什么需求(这里的需求就是compareTo方法)。
2. 接口的特性
既然已经知道了为什么要使用接口,接下来让我们多了解下接口到底有什么特性。
首先,接口本身的访问修饰符可以为public或protected,接口中的所有方法都只能是public(所以可以不加这个修饰符)。其次接口中可以定义常量,但不能包含实例变量(提供实例变量这个活应该交给接口的实现类)。使用接口的步骤也很简单,正如我们上面提到的,只需要用关键子implements加上接口名表示这个类要实现这个接口,然后在类的定义中定义接口所要求实现的方法即可。
接口只是一种规范,所以我们不能用new实例化一个接口。但是我们可以声明接口类型的变量,接口变量只能引用实现了该接口的类对象,比如以下代码:
Comparable<People> p = new People(...);
虽然接口不是类,但我们也可以使用instanceof来判断一个对象是否实现了某个接口:
if (p instanceof Comparable) { ... }
我们也可以”继承“一个接口:
public interface InA extends InB { ... }
接口还有一个很有用的特性就是一个类可以实现多个接口。其实以上提到的接口特性只要”接口只是一种规范”这个本质来看就很好理解:比如接口中不能实例化、不能含有实例变量,就是因为接口不同于类那样是一个“蓝图”,接口更像一个“标签”。
3.接口与抽象类的比较
接口与抽象类的一个本质区别是,抽象类只是一种含有抽象方法的类。也就是说,抽象类是对常规类的进一步”抽象“。常规类是可以直接实例化的,因为它所定义的方法都有了明确的实现,也就是说相应类对象的行为应经能够完全确定下来。而抽象类包含抽象方法,也就是说对象的一部分行为还没有完全确定下来。可以把抽象方法也理解成一种需求描述,因为抽象类存在着”待解决的需求“,所以它不能实例化,只能先派生出子类,解决了这个需求后(也就是实现了抽象方法),从而把实例的整个“蓝图”确定下来,才能够实例化。比如说抽象类People中定义一个write抽象方法,因为写这个动作不同人可以选择不同的方式完成(比如有的人是左撇子,拿笔姿势各不相同)。因为抽象类是一种类,所以一个类只能继承一个抽象类,而不能像接口那样可以“多继承”。
另一方面,抽象类由于本质上是一个类,所以可以包含实例变量,也可包含方法的实现。接口中只能含有public方法,而接口中的常量也都是public的(因为作为一种规范就要”公开透明“);而抽象类可以含有private变量及方法。抽象类中只有抽象方法需要派生类(假设派生来非抽象类)实现,而实现接口的类必须实现接口中规定的所有方法。这样一来,若我们向某个接口中新增一个方法,那么所有实现了这个接口的类都需要进行相应的改动;而对于抽象类,我们向其中添加非抽象方法,不需要它的派生类做任何改动。
4. 接口与回调(callback)
回调(callback)是一种常见的设计模式,利用这种模式,我们可以指出在某个事件发生时应该采取什么动作。比如,Java类库中有个Timer类,可以使通过它在过了指定的时间后发出通知,就像一个闹钟一样。初始化Time类时,需要指定一个时间和到达这个时间后要执行的动作。在这里,告诉Timer要执行的动作就是通过传递给它一个对象,然后到达指定时间后,Timer会调用这个对象的方法。Timer要求我们传递给它的对象需要实现ActionListener接口,这个接口的定义如下:
public interface ActionListener { public void actionPerformed(ActionEvent event); }
我们传递给Time构造器的对象必然定义了actionPerformed方法,我们可以在这个方法中定义我们在定时间隔到达后需要执行的动作。请看以下代码:
1 public class TimerTest { 2 public static void main(String[] args) { 3 ActionListener listener = new TimePrinter(); 4 Timer t = new Timer(10000, listener); 5 t.start(); 6 System.exit(0); 7 } 8 } 9 10 class TimePrinter implements ActionListener { 11 public void actionPerformed(ActionEvent event) { 12 Date now = new Date(); 13 System.out.println("The time is " + now); 14 } 15 }
在以上代码中,会每隔10秒调用一次TimerPrinter中的actionPerformed方法,获取并输出一次当前时间。actionPerformed方法就是回调方法,在回调模式中,通常都是一个类(TimePrinter)实现一个接口(ActionListener),然后另一个类(Timer)的对象通过持有实现该接口的类对象引用(listener)来调用相应的回调方法。