第四章 接口与多态

接口在语法上与抽象类类似,它声明了若干抽象方法常量,其主要作用是帮助实现类的多重继承功能。

多态性是面向对象设计语言的重要特性之一。

4.1 接口

接口允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主题。接口也包含了数据成员,但默认都为static和final。接口只提供一种形式并不提供具体的细节。

4.1.1 接口的作用及语法

1 接口的作用

Java的接口也是面向对象的一个重要机制。它的引进是为了实现多继承,同时免除C++中的多继承那样的复杂性。接口中的所有方法都是抽象的,这些抽象方法由实现这一接口的不同类来具体完成。

Java可以建立类与类之间的“协议”。将类根据其实现的功能分组,用接口代表,而不必顾虑它所在的类继承层次;这样可以最大限度地利用动态绑定,隐藏实现细节。

还可以实现常量的共享。

如图1所示。Java还可以在看起来不相干的对象之间定义共同的行为。

1521118827112814.jpg

图1

2 接口的语法

声明格式如下:

[接口修饰符] interface 接口名称 [extends 父接口名]{

..//方法的原型声明或者静态变量

}

接口与一般类一样,本身也有数据成员与方法,但数据成员一定要赋予初值,且不可修改(因为是常量),此时final可以省略。方法必须是“抽象方法”,public和abstract也可以省略。

例:声明一个接口Shape2D,包括π和计算面积的方法原型:

interface Shape2D{//声明Shape2D接口       final double pi =3.14;//数据成员一定要初始化       public abstract double area();//抽象方法}/*如前文所讲,在接口声明中,我们可以省略final,public,abstract等关键字。因此可以修改为以下格式:*/interface Shape2D{       double pi = 3.14;       double area();}

接口不能实例化,即不可能new一个出来。

4.1.2 实现接口

接口的使用

利用接口设计类的过程,被称为接口的实现,使用implements关键字,语法如下:

public class 类名称 implements 接口名称{  //在类体中实现接口的方法  //本来声明的更多变量和方法}

注意:

    (1)必须实现接口中的所有方法;

(2)来自接口的方法必须是public。

例:实现接口Shape2D

//接上面的接口声明class Circle implements Shape2D{       double radius;       public Circle(double r       {              radius = r;       }       public double area(){              return(pi*radius*radius;       }}class Rectangle implements Shape2D{       int width,height;       public Rectangle(int w, int h){              width = w;              height = hl       }       public double area(){              return(width*height);       }}

测试一下:

public class InterfaceTester{       public static void main(String args[]){              Rectangle rect = new Rectangle(5,6);              System.out.println("Area of rect = " + rect.area());              Circle cir = new Circle(2.0);              System.out.println("Area of cir = " + cir.area());       }}

结果为

Area of rect = 30.0

Area of cir =12.56

例:接口类型的引用变量:

public class InterfaceTester{       public static void main(String args[]){              Shape2D var1,var2;              var1 = new Rectangle(5,6);              System.out.println("Area of rect = " + var1.area());              var2 = new Circle(2.0);              System.out.println("Area of cir = " + var2.area());       }}

4.1.3 多重继承

一个类可以实现多个接口,通过这种机制可以实现对设计的多重继承。

实现多个接口的语法如下:

[类修饰符] class 类名 implements 接口1,接口2,…{……}

4.1.4 接口的扩展

已有的接口称为超接口,父接口,基本接口等,扩展出来的接口为子接口。

接口扩展的语法:

interface 子接口的名称 extends 超口1,超口2,…{……}

说明:

(1)首先声明父接口,然后声明其子接口;

(2)之后声明类实现子接口,因此必须在此类内明确定义子接口中抽象方法的处理方式;

(3)最后在主类中我们声明了类型的变量并创建对象,最后通过对象调用那些方法。

4.2 塑型(类型转换)

4.2.1~4.2.2 类型转换

1.转换方式:隐式的类型转换和显式的类型转换。

2.从方向来看:向上转型和向下转型。

3.类型转换规则:

(1)基本类型转换:将值一种类型转换成另一种类型。

(2)引用变量的转换:将引用转换向另一类型的引用,并不改变对象本身的类型。

它只能被转换为:

任意一个(直接或间接)超类的类型(向上转型);

对象所属的类(或其超类)实现的一个接口(向上转型);

被转为引用指向的对象的类型(唯一可以向下转型的情况)。

(3)当一个引用被转为其超类引用后,通过他能够访问的只有在超类中声明过的方法。

如图2所示:

1521118906856933.jpg

图2

我们看这里定义了一个person类,隐含的继承或者扩展了Object类,然后Employee雇员类继承了person类,customer顾客类也继承了person类,manager继承了雇员类,person实现了insurable接口,另外company,car都继承了insurable接口。所以把这一组可以被保险的对外接口规定在insurable这个接口里面。

下面来看,manager类型的引用可以被转换成什么呢?按照继承的层次,它可以被转型为employee雇员类,子类对象总是可以充当超类对象用的;还可以转换成person类型和object类型。由于person实现了insurable接口,所以manager课转型为insurable接口。由于没有继承关系,也不是实现接口的关系,manager又不能被塑型为customer还有company或者car。

1.隐式转换(自动转换)

基本数据类型:可以转换的类型直接,存储容量低自动向高的类型转换。

引用变量:被转成更一般的类(向上的超类)。例如:

Employee emp;

emp = new Manager();//将manager类型对象直接赋

//给Employee类的饮用对象,系统会自动将manage转换

//为employee类

2.显式类型转换

引用变量

Employee emp;Manager man;emp = new Manager();man = (Manager)emp;//将emp显式转换为它指向的对象的类型

类型转换的主要应用场合:

(1)赋值转换:赋值运算符右边的表达式或对象类型转换为左边的类型;

(2)方法调用转换:实参的类型转换为形参的类型;

(3)算术表达式转换:算术混合运算时,不同类型的操作数转换为相同的类型再进行运算;

(4)字符串转换:字符串连接运算时,如果一个操作数为字符串,另一个操作数为其他类型,则会自动将其他类型转换为字符串。

4.2.3 查找方法

如果转换前后两个类都提供了相同方法,那么系统会调用哪个类的方法呢?

1. 实例方法查找

我们通过图3来看一下实例方法的查找。实例方法是非静态方法。对于实例方法,从对象创建时的类开始,沿类层次向上查找。例如,

Manager man = new Manager();Employee emp1 = new Employee();Employee emp2 = (Employee)man;emp1.computePay();//调用employee类中的computePay()中的方法man.computePay();//调用manager类中的computePay()方法;emp2.computePay();//调用manager类中的computePay()方法。

1521118942124189.jpg 

图3

2.类方法查找

这里,我们以图4为例来讲解。对于类方法,查找在编译时进行,所以总是在变量声明时所属的类中进行查找。

例如:

Manager man = new Manager();Employee emp1 = new Employee();Employee emp2 = (Employee)man;Manager.expenseAllowance();//in managerman. .expenseAllowance();//in managerEmployee.expenseAllowance();//in employeeemp1.expenseAllowance();//in employeeemp2.expenseAllowance();//in employee因为总是在变量声明时//所属的类中进行查找。emp2声明的是employee类

 

1521118967106198.jpg

图4

4.3 多态的概念

超类对象和从相同的超类派生出来的多个子类的对象,可被当作同一种类型的对象对待;

实现统一接口的不同类型对象,也可以被当作同一种类型的对象对待;

可向这些不同类型的对象发送同样的消息,由于多态性,这些不同类的对象响应同一消息时的行为可能有所差别。

多态的目的:

    (1)使代码变得更简单且容易理解;

(2)使程序具有很好的可扩展性。

接下来结合一个例子来进行介绍。如图5所示。我们来看这个继承关系图,最上面的这个和超类shape类里面,声明了一个绘图方法draw()还有一个擦出方法erase()。子类circle、square、triangle中都覆盖了这两个方法。以后绘图可以如下进行:

Shape s =new Circle();s.draw();//实际调用的是circle对象的draw()

1521119042848358.jpg

图5

绑定就是将一个方法调用表达式与方法体的代码结合起来。它分为:

早绑定:程序运行之前执行绑定(编译时)

晚绑定:基于对象的类别,在程序运行时执行。又被称为动态绑定或者运行绑定。若一种语言实现了后期绑定,同时必须提供一些机制,课在运行期间判断对象的类型,并分别调用适当的方法。

4.4 多态的应用举例

例子:二次分发

有不同种类的交通工具(vehicle),如公共汽车(bus)及小汽车(car),由此可声明一个抽象类Vehicle和两个子类Bus,Car。

声明一个抽象类Driver和两个子类FemaleDriver及MaleDriver;

在Driver中声明抽象方法drives,在两个子类中对这个方法进行覆盖;

drives接受一个Vehicle类的参数,当不同类型的交通工具被传送到此方法时,可以输出具体的交通工具;

所有的类放在drive包中。

具体代码详见课本。

二次分发即对输出消息的请求被分发两次,首先根据驾驶员的类型发送给一个类,之后根据交通工具的类型发送给另一个类。

4.5 构造方法与多态

构造方法不具有多态性。

构造子类对象时构造方法的调用顺序:

首先是调用超类的构造方法;这个步骤会不断重复下去,首先被执行的是最远超类的构造方法;

执行当前子类对象的构造方法体的其它语句。

注意:子类不能直接存取父类中声明的私有数据成员。所以在该类方法中要调用它超类的toString时用super.toString。

如果构造方法中调用多态方法会发生什么呢?

会造成一些难以发现的程序错误。从概念上讲,构造方法的职责是让对象实际进入存在状态。在任何构造方法内部,整个对象可能只是得到部分初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用却会调用位于派生类里的一个方法。如果在构造方法内部做这件事情,那么对于调用方法,它要操纵的成员可能尚未得到正确的初始化,就会造成一些难以发现的程序错误。

实现构造方法的注意事项:

(1)用尽可能少的动作把对象的状态设置好。构造方法就是用来初始化的,别干别的事情。

(2)如果可以避免不要调用任何方法。

(3)在构造方法内唯一能够安全调用的是在基类中具有final属性的哪些方法(private方法也可以,因为它们自动具有final属性)。这些方法不能被覆盖,所以不会出现问题。