接口概述
上个总结中我们可以知道抽象类是从多个类中抽象出来的模板。例如,抽象类Employee是从Salary类、Hourly类、PartTime类、Contractor类等具体类中抽象出来的。这些具体类中共性的行为,放到做为父类的抽象类Employee中实现;而具体类中不同的行为,在抽象类中用抽象方法的形式定义,在具体的子类来实现。
如果将这种抽象进行得更彻底,则可以提炼出一种更为特殊的"抽象类"——接口(interface)。在接口中,所有方法都是抽象方法,所有方法都不提供具体的实现。
因为接口中全部都是抽象方法,所以也不能实例化。类不能继承接口,只能"实现"接口所定义的方法。如果一个类实现了接口,那么它将继承接口中的抽象方法。除非实现接口的类是抽象类,否则接口中所有的方法都必须在实现类中进行定义。
接口与类在以下方面相似:
- 接口可以有任意数量的方法;
- 接口保存为以".java"为后缀名的文件,接口名需要与文件名匹配;
- 接口编译后是后缀名为".class"的字节码文件;
- 在包中的接口,其相应的字节码文件必须放置在和包名称相匹配的一个目录结构中。
然而,接口与类之间也有几个不同之处,包括:
- 接口不能被实例化;
- 接口不包含任何构造器;
- 接口中的所有方法都是抽象的;
- 接口中不能包含实例成员变量。唯一可以出现在接口中的成员变量必须是同时声明为static和final;
- 接口不能被一个类继承,只能被一个类来实现;
- 一个接口能够继承多个接口。
注:jdk1.8之后新特性:
- 在接口中可以使用default声明非抽象方法,去实现方法,但它的访问修饰符依然是public,default此时就是一个声明关键字,而非访问修饰符
- 可以用static声明静态方法,但是要有方法体的实现"{}"
接口有许多用途和好处。例如,接口能够用于暴露一个类的某些行为,而不是这个类的所有行为。接口还可以用于强制作用于其它对象的行为,以确保某些方法被对象实现。
接口的声明
在Java语言中,关键字interface用于声明接口。声明接口的格式如下代码所示:
| public interface 接口名 { //任意数量的final, static成员变量 //任意数量的抽象方法声明 } |
接口具有下列性质:
- 接口默认是抽象的。在声明接口时,不需要使用关键字abstract;
- 接口中的每一个方法默认也是抽象的,所以也不需要关键字abstract;
- 接口中的方法默认访问级别是public。 在编写接口的时候通常用public关键字,但是如果我们不显式地将接口中的方法声明为public,它仍然将是public的。
接口在Java语言中被大量的使用。在Java的标准API中就包含了很多接口。例如,如下的Runnable接口是在java.lang包中定义的:
| package java.lang; public interface Runnable{ public void run(); } |
这里,Runnable接口包含一个名为run()的方法。即使abstract关键字并没有在方法前使用,但这个方法默认是抽象的,所以Runnable接口也是抽象的。
接口的实现
接口不能被实例化,因为接口中的方法都是抽象的,所以需要一个类来实现接口。当接口被一个类实现的时候,我们可以简单地认为类与接口之间签订了一个合同,类必须要实现接口所定义的行为,也就是是要履行合同。如果类不能执行接口中所定义的所有行为,那么,类必须声明自己是一个抽象类。
更具体来说,类实现接口时有两个选择:
- 实现接口中定义的所有方法;
- 声明自己为抽象类。
类使用关键字implements来实现接口。关键字implements放在类声明的extends部分之后。其语法格式如下:
| public class 类名 extends 父类名 implements 接口名 |
例如,如下的HelloWorld类声明它实现了Runnable接口:
| public class HelloWorld implements Runnable |
因为HelloWorld类并没有声明为抽象的,所以它必须实现Runnable接口中定义的run()方法。
一个类可以实现多个接口,接口之间用逗号进行分隔。例如,下面的HelloWorld类同时实现了Runnable接口和java.awt.event.MouseListener接口:
| public class HelloWorld implements Runnable,java.awt.event.MouseListener |
因为HelloWorld仍然不是一个抽象类,所以它不仅要实现Runnable接口中的run()方法,还要实现java.awt.event.MouseListener接口中所定义的几个方法。
接口的应用
从上面通过程序代码不难看出,接口有两个同等重要但用法不同的功能:
- 接口可以用于暴露类的行为,允许类的用户通过接口的实现类来使用接口中的方法;
- 接口可以用于强制一个类具有某些行为。
接口的这两种用法为Java应用程序创建了丰富的设计形式。使用接口来暴露类的行为,被广泛地应用于分布式计算,例如RMI、CORBA和Web Services等技术。强制一个类具有某些行为,允许我们创建通用性和灵活性更强的类,从而让类可以与共享共同接口的完全不同的对象之间进行通讯。
在接口中定义变量
接口可以包含成员变量,但是成员变量只能用static和final关键字共同来修饰。由于接口不能被实例化,所以成员变量必须被声明为static,而声明为final是为了程序的可靠性。
代码清单所示中的PhoneHandler接口演示了一个具有三个成员变量的接口。
| /*代码清单 PhoneHandler.java PhoneHandler接口,用于演示如何定义接口的成员变量 */ public interface PhoneHandler{ public static final int LOCAL = 0; public static final int LONG_DISTANCE = 1; public static final int COLLECT = 2; public void answer(); } |
类的静态成员通过类名来访问。同样,接口的静态成员变量也是用接口名来访问。
接口继承
一个接口能够继承另外的一个接口,这类似于一个类能够继承另外一个类。关键字extends被用于继承接口,子接口继承了父接口中的方法。
代码清单10.18定义了一个SportsListener(运动比赛的听众)接口。
| //代码清单10.18 SportsListener.java public interface SportsListener{ public void setHomeTeam(String name); public void setVisitingTeam(String name); } |
代码清单10.19定义了一个HockeyListener(曲棍球比赛的听众)接口,该接口继承SportsListener接口。
| //代码清单10.19 HockeyListener.java public interface HockeyListener extends SportsListener{ public void homeGoalScored(); public void visitingGoalScored(); public void endOfPeriod(int period); public void overtimePeriod(int ot); } |
从上面的代码中可以看出,HockeyListener接口有四个方法,它从SportsListener接口中继承了两个,因此,实现HockeyListener接口的类需要实现所有的六个方法。
继承多个接口
我们知道,Java语言不允许多继承,一个类只能继承一个父类。然而,接口不是类,它可以继承多个父接口。在接口多继承中,关键字extends只使用一次,多个父接口之间用逗号分隔。
例如,如果HockeyListener接口继承SportsListener和EventListener两个父接口,它必须被声明为如下形式:
| public interface HockeyListener extends SportsListener, EventListener |
于是,任何实现HockeyListener接口的类,都必须实现HockeyListener、SportsListener和 EventListener三个接口中的所有方法。
标记接口
既然接口可以被用来继承,那么在什么情况下使用呢?最常见的情况是发生在当父接口不包含任何方法的时候。例如,java.awt.event包中的MouseListener接口继承了java.util.EventListener接口,而该父接口的定义如下:
| package java.util; public interface EventListener{} |
一个没有任何方法的接口,我们将其称为"标记接口"。为什么定义一个没有方法的接口呢?这里有两个关于标记接口的基本设计宗旨:
- 创建一个通用的父类。例如,在Java API中有几十个接口继承了EventListener接口,我们就可以用标记接口来为一组接口创建通用的父接口。例如,当一个接口继承了EventListener时,JVM就会知道这个特定的接口将用于事件处理。
- 给一个类添加数据类型:一个实现标记接口的类,其不需要定义任何方法(因为接口中没有任何方法),但是这个类通过多态成为了接口类型。
java.io.Serializable接口就是一个没有被设计成父接口的标记接口示例,这意味着只要一个类实现这个接口,这个类的对象就成为了可序列化的对象。Serializable接口定义如下:
| package java.io; public interface Serializable{ public static final long serialVersionUID; } |
请注意Serializable接口只有一个属性,并没有方法。一个类实现该接口就实现了可序列化,而不需要额外的更改。下面的Employee类就实现了Serializable接口:
| public class Employee implements java.io.Serializable{ public String name, address; public double weeklyPay; public void computePay(int hoursWorked){ weeklyPay = hoursWorked * 6.50; System.out.println("weekly pay for " + name + " is $" + weeklyPay); }
public void mailCheck(){ System.out.println("Mailing check to " + name + " at " + address); } } |
为什么实现一个没有任何方法的接口?现在实现序列化的Employee类有什么不同?答案在于多态。如果一个类实现了接口,那么这个类的对象就会被视为接口类型。例如,一个Employee对象能够被视为一个Serializable对象。考虑下面使用Employee类的语句,会有什么输出呢?
| Employee e = new Employee(); if(e instanceof Employee){ System.out.println("e是一个Employee对象"); } if(e instanceof java.io.Serializable) { System.out.println("e是一个Serializable对象"); } |
上面的语句输出为:
| e是一个Employee对象 e是一个Serializable对象 |
如果Employee类没有实现Serializable接口,输出的第二行就不会显示。因此,实现一个标记接口,类虽然不需要特别的工作,但是添加了一个数据类型到该类的对象上。
接口与多态
如果一个类实现一个接口,那么这个类的对象就呈现出接口数据类型的特点。对象采用接口形式的这种能力就是多态的一个例子。在本章中我们已经在多处应用了这种特性,现在我们正式介绍多态和接口的细节。
下面我们来看一个演示接口如何影响多态的示例。我们将使用具有一个父类和实现一个接口的Dog类。代码清单1所示的代码定义了哺乳动物类Mammal,该类将作为Dog类的父类。
| /*代码清单1 Mammal.java Mammal类,代表哺乳动物 */
public class Mammal { public void breathe() { System.out.println("哺乳动物在呼吸"); } } |
代码清2单定义的接口Play表示了能够追赶和捕捉的对象,它被Dog类来实现:
| /*代码清单2 Play.java Play接口中定义了追赶和捕捉的行为 */
public interface Play { public void playFetch(); public void playCatch(); } |
代码清单3定义的Dog类继承了Mammal类并实现了Play接口:
| /*代码清单3 Dog.java Dog类,代表狗,继承哺乳动物类,并实现了Play接口 */
public class Dog extends Mammal implements Play { public void playFetch() { System.out.println("狗正在追赶"); } public void playCatch() { System.out.println("狗正在捕捉"); } public void sleep() { System.out.println("狗正在睡觉"); } } |
接下来,我们指出Dog对象能够采取的各种形式。通过多态,Dog类能够采取下列形式:
- Dog:Dog对象采用的形式当然是Dog;
- Mammal:一个Dog对象是一个哺乳动物对象,因为Dog类继承了Mammal类;
- Play:一个Dog对象是一个Play对象,因为Dog类实现了Play接口;
- Object:一个Dog对象是一个Object对象,因为Dog继承了Mammal类,而Mammal类又继承了Object。
因此,下面的四种条语句都是有效的:
| Dog fido = new Dog(); Mammal rover = new Dog(); Play spot = new Dog(); Object pooch = new Dog(); |
这四个Dog对象在内存中看上去都是一样的。然而,它们却根据其引用不同,使用不同的表现形式。例如,Dog的引用fido,不需要任何类型转换就能够调用Dog类、Mammal类、Object类和Play接口中的方法:
| fido.sleep(); fido.playFetch(); fido.breathe(); fido.toString(); |
我们再来看看Mammal类型的引用rover。在没有类型转换的情况下,rover可以调用哪些方法呢?除了Object对象的方法,仅有breathe()方法能够被调用:
| rover.breathe(); |
同样地,在没有类型转换的情况下,Play类型的引用spot能够调用什么方法(除了Object对象的方法)呢?由于spot是Play接口类型的引用,有两个方法可以被调用:
| spot.playCatch(); spot.playFetch(); |
而Object类型的引用pooch仅能调用Object对象中的方法。例如:
| pooch.toString(); |
如果pooch要调用Mammal、Play或Dog类中的任何一个方法,就需要进行强制类型转换。例如:
| ((Dog) pooch).sleep(); |
代码清单4所示的FourDogs程序演示Dog对象通过多态来呈现这四种不同的表现形式:
| /*代码清单4 FourDogs.java FourDogs类,演示接口多态 */
public class FourDogs { public static void main(String[] args) { System.out.println("实例化四条狗"); Dog fido = new Dog(); Mammal rover = new Dog(); Play spot = new Dog(); Object pooch = new Dog(); System.out.println("调用Dog的方法"); fido.sleep(); fido.playFetch(); fido.breathe(); System.out.println("fido is " + fido.toString()); System.out.println("\n调用Mammal的方法"); rover.breathe(); System.out.println("\n调用Play的方法"); spot.playCatch(); spot.playFetch(); System.out.println("\n调用Object的方法"); System.out.println("pooch is " + pooch.toString()); ((Dog) pooch).sleep(); } } |
正确编译并执行"FourDogs"类,输出结果如图所示。
接口与抽象类的区别
接口和抽象类很相像,它们都具有如下特征:
- 二者都不能被实例化,它们都位于继承树的顶端,用于被其它类实现或者继承。
- 二者都可以包含抽象方法,实现接口或者继承抽象类的子类都必须实现这些抽象方法。
但是,接口和抽象类的差别非常大。这种差别主要体现在二者的设计目的上。
接口通常做为一个系统与外界交互的窗口,它体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务。对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。
而抽象类通常是做为系统中多个子类的共同父类,体现的是一种模板式设计。它可以被当成系统实现过程中的中间产品,实现了部分功能,其他的功能交给不同的子类来完成。
除此之外,接口和抽象类在用法上也存在如下区别:
- 接口中只能包含抽象方法;而抽象类中可以包含普通方法。
- 接口中不能定义静态方法;而抽象类可以定义静态方法。
- 接口中只能定义静态常量属性,不能定义普通属性;而抽象类既可以定义普通属性,也可以定义静态常量属性。
- 接口不包含构造器;而抽象类中可以包含构造器。抽象类中的构造器并不是用于创建对象,而是让子类调用这些构造器来完成属于抽象类的初始化工作。
- 接口中不包含初始化块;而抽象类可以包含初始化块。
- 一个类最多有一个直接父类,包括抽象类;但是一个类可以直接实现多个接口,通过实现多个接口来弥补Java单继承的不足。
使用接口原则
1.使用接口解决多承
2.使用接口为外部类添加功能
3.以面向对象的角度考虑将一个类与身自来的特征和行为和依赖于外部的可选的特征和行为分离,类尽可能的丰富,让我们在设计程序的时候,不再关注具体的类,而是分离关注类上拥有哪些接口所定义的能力,从而做到程序设计时,类类的直接关系,变换为类-接口-类,这种变换关系,也被称之 “解耦”。
接口的优点
1.将设计和实现分离,对外(调用者)隐藏了实现(而通常调用者也不需要关心实现)
2.面向接口编程是OOP的核心
总结
- 接口是一个要被类实现的抽象方法集合。类用关键字implements实现接口。
- 实现接口的类必须实现接口中定义的每个方法。否则,类必须被声明为"abstract"。
- 一个接口可以被用于暴露类的某些方法,也可以强制类包含某些的方法;
- 一个接口可以包含成员变量。接口中的成员变量默认访问修饰符为public、static和final。
- 一个接口可以继承一个或多个接口;
- 没有方法的接口被称为标记接口。