理解接口
1.1 Java 中的接口
在 Java 程序设计语言中,接口是一个引用类型,与类相似,所以可以在程序中定义并使用一个接口
类型的变量。
在接口中只能包含有常量、方法签名和嵌套类型,它没有构造器。在接口中声明的方法,没有方法
体。因为接口没有构造器,所以它不能被实例化,它只能被类所实现或者被另外的接口所继承。
1.2 在 Java 中定义接口
在 Java 中,接口也属于引用类型。定义接口与创建一个新的类很类似,但需要使用关键字 interface,
后跟接口的名称。接口的名称同样要遵循 Java 标识符命名规则。
比如,在某一个图书信息管理系统中,可能会先定义如下的一个接口,以向各个开发小组“约定” ,
将要开发的某个类应具有该接口中规定的功能方法:
2
public interface BookDao {
// 如果有常量的话,在这里声明
//……
// 方法声明
void add(Book book); //增加一本图书的方法声明
void modify(Book book); //修改图书信息
void delete(String bookName); //删除指定书名的图书
void find(String bookName); //根据书名查询图书的方法声明
void find(String bookName, float bookPrice); //根据书名和定价查询某本图书
// 更多的方法声明
//……
}
注意,接口中的方法签名后面没有花括号(即没有方法体),以分号结尾。
1.3 在 Java 中使用接口
要使用接口,需要编写一个实现接口的类。所谓实现接口的类,指的是这个类必须为接口中声明的
每一个方法提供一个方法体,即必须实现接口中的所有方法。
例如,下面是一个对图书信息进行增加、修改、删除和查找的类,它使用关键字 implements 实现了
BookDao 接口:
/*
* 接口实现类
*/
public class BookDaoImpl implements BookDao{
@Override
public void add(Book book) {
}
@Override
public void modify(Book book) {
}
@Override
public void delete(String bookName) {
}
@Override
public void find(String bookName) {
}
@Override
public void find(String bookName, float bookPrice) {
}
//其它成员。接口实现类除了必须要实现的接口方法之外,还可以有自己的成员(字段和方法)
}
可以看到,在接口实现类中,为每一个实现的接口方法添加了@Override 注解。这说明这些方法来
自父接口,而不是本类定义的新方法。对每一个接口的实现,就是添加了方法体(花括号),即使方法
体中没有任何内容。1.4 作为 API 的接口
作为 API 使用的接口还普遍使用在商业软件生产中。典型地,一个软件公司会开发并销售包含有复
杂方法的一个软件包,而另外一家公司在他们自己的软件产品中使用这些方法。
例如,A 公司开发一个含有数字图像处理方法的软件包,然后将此软件包销售给制作终端用户图片
程序的 B 公司。 A 公司编写它们自己的类以实现一个接口,而将接口公开给它的客户 B。然后 B 公司根
据接口中定义的方法签名和返回类型,调用相应的图像处理方法。当 A 公司的 API 对它的客户 B 公司
公开时,它对 API 的实现实际上是隐藏的。事实上,A 公司可能会在以后的日子里修改 API 的实现,升
级其软件包。但是,只要新的软件包继续实现原始接口,都不会影响到它的客户 B 公司对 API 的使用。
1.5 接口和多继承
在 Java 语言中,接口有另外一个非常重要的作用。因为 Java 中不允许多继承,一个类只能继承自
一个父类,因此有时这种单继承并不能反映现实世界的某些现象。例如,将来会出现这么一种图书(事
实上已经出现了),它不但只提供有文字,还提供有视频(可以播放观看)、音频(可以朗读)、场景
再现等。
因此在 Java 中规定,一个类虽然只能继承自一个父类,但它可以实现多个接口。 这时,要想创建这
种未来的图书,除了从 Book 类继承传统的方法之外,还可以实现带有视频、音频、场景再再方法的接
口,那么它就具有了未来图书读物的方法。
当实现多个接口时,对象可以同时具有多个类型:
自身所属类的类型。
其所实现的所有接口的类型。
这意味着,如果声明一个接口类型的变量(接口是引用类型),那么它的值可以引用任何实现了该
接口的类的任何实例对象。
2 定义接口
有时候,程序员需要自己定义要使用的接口或者用来分发的接口。定义接口与定义类很相似,包括
接口的声明和接口体的实现。在接口体中,含有对接口所包含的所有的方法的方法声明。接口所含有的
方法声明后面紧跟一个分号,而不是花括号,因为一个接口不提供对它里面所声明的方法的实现。在一
个接口中声明的所有方法都隐含是 public 的,所以 public 修饰符可以被省略。
2.1 接口声明
接口声明由修饰符、关键字 interface、接口名称、用逗号分隔的父接口列表(如果有的话)和接口
体组成。声明接口的语法形式如下。
[修饰符] interface 接口名称 [extends 父接口名称列表]{
// 接口体
}
说明:
修饰符:可选,用于指定接口的访问权限,可选值为 public。如果省略,则使用默认的访问权
限,既只能在当前的软件包中使用。换句话说,声明接口时,关键字 interface 前面要么是修饰
符 public,要么什么都没有,而不能使用 protected 或 private 关键字。
interface:必选,定义接口的关键字。
接口名称:必选,用于指定接口的名称。接口名必须是合法的 Java 标识符。一般情况下,要求
接口名称的首字母大写。
extends 父接口名称列表:可选,用于指定该接口继承自哪个父接口。当使用 extends 关键字时,
父接口名称为必选参数。接口可以是多继承的,既一个接口可以有任意多个父接口。在接口的
声明中,包括有其所有父接口的一个列表,用逗号分隔。
例如,下面声明一个接口 GroupedInterface,该接口继承自三个父接口:
public interface GroupedInterface extends Interface1,Interface2,Interface3 {
// 接口体
}
在上面的声明中,public 访问控制符指出该接口可以被任何包中的任何类所使用。如果没有指定接
口是 public 的,那么接口将只能被与其定义在同一个包中的类所访问。
2.2 接口体
在接口声明后面的大括号括起来的部分,为接口的接口体。接口体由两部分组成:常量声明和方法
声明。其语法格式如下所示。
[修饰符] interface 接口名称 [extends 父接口名称列表]{
// 常量声明
[public] [static] [final] 常量名称;
// 方法声明
[public] [abstract] 返回类型 方法签名;
}
说明:
常量声明:接口中可以包含有常量声明,也可以不包含,它不是必须的,根据需要而定。如果
有常量声明的话,默认是 public static final 类型的,也就是说,接口中的所有字段都隐含地具有
public、static 和 final 属性。所以可以省略常量声明的修饰符 public、static 和 final,其作用都是
一样的。
方法声明:接口中的方法只有返回类型和方法签名,而没有方法体。接口中的方法都具有 public
和 abstract 属性,所以在声明方法时,可以省略前面的修饰符 public 和 abstract,作用都是一样
的。也就是说,即使声明方法时前面不使用修饰符,该方法也隐含地是 public 和 abstract 的。
下面定义的接口包含有对接口中常量的声明和方法的声明。
public interface GroupedInterface extends Interface1,Interface2,Interface3 {
// 常量声明
double E = 2.718282; // 自然对数的基数
//上面的变量声明等价于 public static final double E=2.718282 语句
// 方法声明
void doSomething (int i, double x);
int doSomethingElse(String s);
}
在接口的接口体中,含有对接口所包含的所有方法的声明。接口所含有的方法声明后面紧跟一个分
号,而不是花括号,因为一个接口不提供对它里面所声明的方法的实现。在一个接口中声明的所有方法
都隐含是 public 的,所以 public 修饰符可以被省略。 在接口中声明的所有常量,默认都是 public static final
类型的,如上面的常量 E。因为常量是不可修改的,所以在接口中声明的常量必须要赋初值,并且在接
口的实现类中可以使用这个常量,但不能重新赋值(修改)。例如,下面的类 GroupedInterfaceImpl 实
现的接口 GroupedInterface:
public class GroupedInterfaceImpl implements GroupedInterface {
{
//E = 3.718282; //错误。不可以修改常量的值
System.out.println(E);
}
@Override
public void doSomething (int i, double x) {
}
@Override
int doSomethingElse(String s){
}
public static void main(String[] args){
new GroupedInterfaceImpl();
}
}
3 实现接口
接口的主要作用,是声明共同的常量或方法,用来为不同的类提供不同的实现,但这些类仍然可以
保持同样的对外接口。接口可以被类实现,也可以被其他接口继承。在类中实现接口需要使用关键字
implements。
3.1 实现接口的语法
要声明一个实现接口的类,需要在类的声明中使用 implements 短语。一个类可以实现多个接口,所
以 implements 关键字后面要跟一个被类实现的接口列表,用逗号分隔符分隔。在类中实现接口的语法格
式为:
[修饰符] class 类名称 [extends 父类名称] implements 接口列表{
// 类体
// 在类中,要实现所有接口中声明的方法
}
按惯例,implements 短语跟在 extends 短语后面(如果有 extends 短语的话)。
3.2 接口实例:Relatable
下面考虑一个比较不同对象大小的接口实例 Relatable。在接口 Relatable 中,声明了一个 isLargerThan
方法,其作用是和另外一个实现了 Relatable 接口类型的对象进行比较。Relatable 接口的声明如下所示。
public interface Relatable {
public int isLargerThan(Relatable other);
}
调用 isLargerThan 方法的对象和方法参数中的 other 对象必须是同一个类的实例。当调用此方法的
对象比方法参数中的 other 对象大、相等或小时,此方法分别返回 1、0、-1 。
如果两个相似的对象想比较相互的大小,不管这些对象是什么,实例化它们的类都应该实现
Relatable 接口。
理论上,任何类都可以实现 Relatable 接口,只要从这些类实例化的对象有办法比较相对“大小”。
例如,对于字符串,可以是比较字符的个数;对于图书,可以是比较页数;对于学生,可以是年龄;等
等。对于二维几何对象,对面积进行比较是较合适的,而对于三维几何对象,对体积进行比较是较合适
的。所有诸如此类的类都可以实现 isLargerThan()方法。如果知道一个类实现了 Relatable 接口,那么就
知道可以对这个类所实例化的对象进行大小的比较。
3.3 实现接口 Relatable
下面是对其重新编写以实现接口 Relatable 后的形式。
【例】实现了 Relatable 接口的类及其接口方法调用。其实现步骤如下。
(1)编写一个 RectanglePlus 类,并实现 Relatable 接口。
public class RectanglePlus implements Relatable {
public int width = 0; //声明接口中的属性
public int height = 0;
public Point origin; //声明对象属性
// 四个构造器
public RectanglePlus() { //无参构造器
origin = new Point(0, 0); //使用默认值(0,0)初始化矩形左上角坐标
}
public RectanglePlus(Point p) { //带有一个 Point 类型参数的构造器
origin = p; //使用已知的点来初始化矩形左上角坐标
}
public RectanglePlus(int w, int h) { //带有两个参数的构造器
origin = new Point(0, 0); //使用默认值(0,0)初始化矩形左上角坐标
width = w; //使用已知的值 w 来初始化矩形的宽度
height = h; //使用已知的值 h 来初始化矩形的高度
}
public RectanglePlus(Point p, int w, int h) { //带有三个参数的构造器
origin = p; //使用已知的点来初始化矩形左上角坐标
width = w; //使用已知的值 w 来初始化矩形的宽度
height = h; //使用已知的值 h 来初始化矩形的高度
}
// 移动矩形的方法
public void move(int x, int y) {
origin.x = x; //改变矩形对象左上角的坐标值
origin.y = y;
}
// 计算矩形面积的方法
public int getArea() {
return width * height;
}
// 实现接口 Relatable 中声明的方法
public int isLargerThan(Relatable other) {
RectanglePlus otherRect = (RectanglePlus)other; //对象类型转换
if (this.getArea() < otherRect.getArea()){ //如果当前对象的面积小于参数对象的面积
return -1; //返回-1
}else if (this.getArea() > otherRect.getArea()){ //如果当前对象的面积大于参数对象的面积
return 1; //返回 1
}else{
return 0; //如果当前对象与参数对象的面积相等,返回 0
}
}
}
因为类 RectanglePlus 实现了 Relatable 接口,所以任何两个 RectanglePlus 对象都可以进行比较面积
的大小。
(2)另外编写一个含有 main()方法的 ImlExample.java 应用程序,使用 isLargerThan 方法比较大小:
public class ImlExample{
public static void main(String[] args){
RectanglePlus rect1 = new RectanglePlus(3,5); //创建实现了 Relatable 接口的 RectanglePlus 对象
RectanglePlus rect2 = new RectanglePlus(2,7); //创建实现了 Relatable 接口的 RectanglePlus 对象
int result = rect1.isLargerThan(rect2); //调用接口中的方法进行对象的比较
switch(result){ //根据比较的结果,输出相应的信息
case -1:
System.out.println(“矩形 rect1 比矩形 rect2 小。”);
break;
case 1:
System.out.println(“矩形 rect1 比矩形 rect2 大。”);
break;
default:
8
System.out.println(“矩形 rect1 与矩形 rect2 一样大小。”);
}
}
}
在上面的 main()方法中,创建两个实现了相同的 Relatable 接口的类的实例,并调用相应的接口方法
进行比较。
9.3.4 编译并运行带有接口的实例
上一节的程序共包含有 4 个类型,其中有三个类(Point 类、RectanglePlus 类、主类 ImlExample,
有一个接口 Relatable。要想让这个实例运行起来,可以有两种形式:
将 4 个类型(3 个类和 1 个接口)分别保存为单独的源文件,文件名和类名或接口名相同
(Point.java、RectanglePlus.java、Relatable.java、ImlExample.java)。4 个源文件放在同一个目
录中。然后通过命令行窗口编译主程序 ImlExample.java,编译器会自动地在当前目录下寻找相
关的类进行编译,最后运行 ImlExample 的类文件。
将 4 个类型(3 个类和 1 个接口)放在一个源文件中,并将除 ImlExample 类之外的其他类和接
口声明前的 public 修饰符去掉(在一个源文件中,只能有一个类是 public 的,并且类名与源文
件名相同,一般都将含有 main()方法的类设为 public 的)。然后编译并运行 ImlExample.java 程
序。
3.5 实现多个接口时的常量和方法冲突问题
在 Java 中,每个类实现多个接口,每个接口使用逗号分隔符分隔。这时就有可能出现常量或方法名
冲突的情况。例如,一个类实现两个接口,两个接口中都声明了相同名称的常量或相同名称的方法,那
么在实现这两个接口的类中,引用常量或实现方法时,就不明确是哪个接口中的。
要解决这类冲突问题时,如果是常量冲突,则需要在类中使用全限定名(接口名称.常量名称)明确
指定常量所属的接口。如果是方法冲突,则只需要实现一个方法就可以了。
在下面这个示例程序中,定义了两个接口,并且在这两个接口中都声明了一个同名的常量和一个同
名的方法。然后再定义一个同时实现这两个接口的类。实现步骤如下。
(1)创建 Calculate 接口,在该接口中声明一个常量和两个方法:
//位于文件 Calculate.java 中
public interface Calculate {
float PI = 3.14f; //定义一个用于表示圆周率的常量 PI
float getArea( ); //定义一个用于计算面积的方法
float getGirth( ); //定义一个用于计算周长的方法
}
(2)创建 Shape 接口,在该接口中声明一个常量和两个方法:
//位于文件 Shape.java 中
public interface Shape{
float PI = 3.14159f; //定义一个用于表示圆周率的常量 PI
float getArea( ); //定义一个用于计算面积的方法
void draw( ); //定义一个绘制图形的方法
}
(3)创建一个 Circle 类,该类同时实现 Calculate 接口和 Shape 接口:
//位于文件 Circle.java 中
public class Circle implements Calculate,Shape {
//声明一个私有变量 radius,代表圆的半径
private float radius;
//构造器,用于初始化半径 radius
public Circle(float r){
radius = r; //将圆的半径初始化为参数 r 的值
}
//实现计算圆面积的方法,该方法两个接口中都有,只需实现一个既可
public float getArea( ){
float area = Calculate.PI*radius*radius; //计算圆的面积,使用全限定名指定 Calculate 接口中的常量
return area;
}
//实现计算圆周长的方法
public float getGirth ( ){
float circuGirth = 2*Shape.PI*radius ; //计算圆的面积,使用全限定名指定 Shape 接口中的常量
return circuGirth;
}
//实现绘制图形的方法
public void draw( ){
System.out.println("在这里绘制了一个圆!");
}
}
(4)创建一个含有 main()方法的主测试程序:
//位于文件 ImlDemo2.java 中
public class ImlDemo2{
public static void main(String[ ] args){
Circle circle = new Circle(7); //创建 Circle 的对象
float area = circle.getArea( ); //调用 circle 对象的 getArea 方法,获得该对象的面板值
System.out.println("圆的面积为:" + area);
float girth = circle.getGirth( ); //调用 circle 对象的 getGirth 方法,获得该对象的周长值
System.out.println("圆的周长为:" + girth);
circle.draw( ); //调用 circle 对象的 draw 方法,绘制一个圆
}
}
(5)编译并运行 ImlDemo2.java 程序,输出结果如下所示:
圆的面积为:153.86002
圆的周长为:43.98226
在这里绘制了一个圆
在这个程序中,接口 Calculate 和接口 Shape 都声明有 getArea()方法和变量 PI,而类 Circle 实现了
这两个接口,所以为了避免变量冲突,在引用 PI 时,使用了全限定名 Calculate.PI;为了避免方法冲突,
只实现一个 getArea()方法。
4 将接口作为类型使用
当定义一个新的接口时,就是定义一个新的引用数据类型。凡是在任何能够使用其他数据类型名称
的地方,也可以使用接口名称。如同其他数据类型一样,可以定义一个接口类型的引用变量。
如果定义一个接口类型的引用变量,那么,赋给这个变量的任何对象就必须是实现这个接口的类的
一个实例。例如,在下面的示例方法中,用来在两个对象中查找最大的那个对象。对于这其中的任何一
个对象来说,不管是 object1 还是 object2,都必须是实现了 Relatable 接口的类的实例对象:
public Object findLargest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1; //将参数 object1 转换为 Relatable 接口类型对象
Relatable obj2 = (Relatable)object2; //将参数 object2 转换为 Relatable 接口类型对象
if ( (obj1).isLargerThan(obj2) > 0){ //对两个对象进行比较
return object1;
}else{
return object2;
}
}
在上面的代码中,通过将 object1 转换为一个 Relatable 接口类型,它就可以调用 isLargerThan 方法。
同样地,实现了接口 Relatable 的类的实例对象也都可以使用下面的方法进行比较:
public Object findSmallest(Object object1, Object object2) {
Relatable obj1 = (Relatable)object1; //将参数 object1 转换为 Relatable 接口类型对象
Relatable obj2 = (Relatable)object2; //将参数 object2 转换为 Relatable 接口类型对象
if ( (obj1).isLargerThan(obj2) < 0){ //对两个对象进行比较
return object1;
}else{
return object2;
}
}
public boolean isEqual(Object object1, Object object2) { //定义两个对象进行比较的方法
Relatable obj1 = (Relatable)object1; //将参数 object1 转换为 Relatable 接口类型对象
Relatable obj2 = (Relatable)object2; //将参数 object2 转换为 Relatable 接口类型对象
if ( (obj1).isLargerThan(obj2) == 0){ //判断两个对象是否相等
return true;
}else{
return false;
}
}
这些方法可以用于任何“有关联的”对象,不管这些对象它们的类是怎么继承的。当一个对象实现
Relatable 接口时,它既可以是自身类(或超类)的类型,也可以是一个 Relatable 类型。这就提供了一些
多继承的好处,这些对象可以拥有它们超类的行为,同时也可以拥有它们所实现的接口的行为。