-
Java程序设计(2021春)——第四章接口与多态笔记与思考
4.1 接口
接口可以看作纯的抽象类,只提供设计而不提供实现。
- 接口中可以规定方法的原型:方法名、参数列表以及返回类型,但不规定方法主体(即没有给出方法的实现)。
-
也可以包含基本数据类型的数据成员,但它们都默认为
static
和final
的。
接口的作用
-
继承多个设计(可以实现类的设计的多继承)。
-
建立类和类之间的协议。
将类根据其实现的功能分组用接口代表,而不必顾虑它所在的类继承层次;这样可以最大限度地利用动态绑定,隐藏实现细节。
接口允许在看起来不相关的对象之间定义共同的行为,如下图。
接口的语法
声明格式为
[接口修饰符]interface 接口名称 [extends 父接口名]{
...////方法的原型或静态变量
}
-
interface
表明正在声明的是一个接口。 - 接口可以继承父接口(后续会介绍)。
- 在接口体中可以声明方法原型和静态常量。
-
由于接口体中数据成员是静态的,因此一定要有初值,且此值将不能改变,可以省略
final
关键字(默认是常量)。 -
接口中的方法必须是抽象方法,不能有方法体,可以省略
public
和abstract
关键字(默认是public
,因为是对外服务接口;默认是abstract
,因为方法是抽象的)。
例:接口声明
声明一个接口Shape2D
,包括π
和计算面积的方法原型
interface Shape2D{//声明Shape2D接口
final double pi = 3.14;//数据成员一定要初始化
public abstract double area();//抽象方法
}
在接口的声明中,允许省略一些关键字,也可声明如下:
interface Shape2D{
double pi = 3.14;
double area();
}
如上,final
public
abstract
关键字都可以省略。
接口中只有方法体的原型,没有方法体的实现,所以和抽象类一样,接口不能产生实例(不能new
一个接口对象)。
实现接口
实现接口就是利用接口设计类的过程,成为接口的实现,使用implements
关键字,语法如下:
public class 类名称 implements 接口名称{
//在类体中实现接口的方法
//本类声明的更多变量和方法
}
由上,实现接口形式上类似继承超类(使用extends
关键字)
注意:
- 必须实现接口中的所有方法。
-
来自接口的方法必须声明为
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 = h;
}
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
由上可知,虽然都调用了area()
方法,但是分别计算了正确的面积。
例:接口类型的引用变量
声明接口类型的变量,并用它来访问对象:
public class VariableTester {
public static void main(String[] args) {
Shape2D var1, var2;//声明两个接口类型的引用变量var1,var1
var1 = new Rectangle(5, 6);//将Rectangle对象的引用赋值给Shape2D接口类型的引用,发生了隐含的类型转换
System.out.println("Area of var1 = " + var1.area());
var2 = new Circle(2.0);//将Circle对象的引用赋值给Shape2D接口类型的引用,发生了隐含的类型转换
System.out.println("Area of var2 = " + var2.area());
}
}
输出
Area of var1 = 30.0
Area of var2 = 12.56
由上可知,依然准确执行了各自的area()
方法。
实现多个接口的语法
一个类可以实现多个接口,通过这种机制可以实现对设计的多重继承(Java中仅支持单继承,此种方法是拐个弯)。
实现多个接口的语法如下
[类修饰符] class 类名称 implements 接口1,接口2,...{
//对每个接口中的抽象方法予以实现
}
例:通过实现接口达到(对设计的)多重继承
声明Circle
类实现接口Shape2D
和Color
-
Shape2D
具有常量pi
与area
方法,用来计算面积。 -
Color
则具有setColor
方法,可用来赋值颜色。 -
通过实现这两个接口,
Circle
类得以同时拥有这两个接口的成员,达到了对设计进行多重继承的目的。
interface Shape2D{//声明Shape2D接口
final double pi = 3.14;//数据成员一定要初始化
public abstract double area();//抽象方法
}
interface Color{
void setColor(String str);//抽象方法
}
class Circle implements Shape2D, Color {
double radius;
String color;
public Circle(double r) {// 构造方法
radius = r;
}
public double area() {
return (pi * radius * radius);
}
public void setColor(String str) {// 定义setColor()的处理方式
color = str;
System.out.println("color = " + color);
}
}
测试类
public class MultiInterfaceTester {
public void main() {
Circle cir;
cir = new Circle(2.0);
cir.setColor("blue");
System.out.println("Area = " + cir.area());
}
}
输出结果
color = blue
Area = 12.56
接口的扩展
接口与接口之间也可以有继承关系,即扩展(extends
)关系,可以从一个已有的接口扩展出更多的接口。已有的接口成为超接口,扩展出来的接口称为子接口。
-
实现一个接口的类必须实现其超接口。
-
接口扩展的语法
interface 子接口的名称 extends 超接口的名称1,超接口的名称2,...{ // }
如下例:
例:接口的扩展
//声明Shape接口
interface Shape{
double pi = 3.14;
void setColor(String str);
}
//声明Shape2D接口拓展了Shape接口
interface Shape2D extends Shape{//继承了Shape接口,自动继承了常量pi和setColor方法
double area();
}
class Circle implements Shape2D {
double radius;
String color;
public Circle(double r) {// 构造方法
radius = r;
}
public double area() {
return (pi * radius * radius);
}
public void setColor(String str) {// 定义setColor()的处理方式
color = str;
System.out.println("color = " + color);
}
}
public calss ExtendsInterfaceTester{
public static void main(String[] args){
Circle cir;
cir = new Circle(2.0);
cir.setColor("blue");
System.out.println("Area = " + cir.area());
}
}
运行结果
color = blue
Area = 12.56
说明
-
首先声明了父接口
Shape
,然后声明其子接口Shape2D
。 -
之后声明类
Circle
实现Shape2D
子接口,因而在类内必须明确定义setColor()
与area()
方法的处理方式。 -
最后在主类中声明了
Circle
类型的变量cir
并创建新的对象,最后通过cir
对象调用setColor
与area()
方法。
4.2 类型转换
类型转换
-
又称为塑型(
type-casting
)。 - 转换方式可以分为隐式的类型转换和显式的类型转换。
- 转换方向可以分为向上转型和向下转型。
类型转换规则
- 基本类型之间的转换:将值从一种类型转换成另一种类型。
-
引用类型的类型转换:
- 将引用转换为另一类型的引用,并不改变对象本身的类型。
-
引用类型只能被转为
- 任何一个(直接或间接)超类的类型(向上转型)。
- 对象所属的类(或其超类)实现的一个借口(向上转型)。
- 被转为引用指向的对象的类型(唯一可以向下转型的情况)。
- 当一个引用被转为其超类引用后,通过它能够访问的只有在超类中声明过的方法,即受限了,转为接口引用同理。
以下通过举例说明类型转换:
Person
继承或者扩展了Object
类;Emploee
类和Customer
类继承了Person
类;manager
类继承了Emploee
类。
Person
实现了Insurable
(可保险)接口。
-
Manager
对象-
可以被塑型为
Emploee
Person
Object
或Insurable
。 -
不能被型为
Customer
、Company
、Car
类,因为没有继承关系,也不是实现接口的关系。
-
可以被塑型为
隐式类型转换
基本数据类型
- 可以转换的类型之间,存储容量低的自动向存储容量高的类型转换。
引用变量
-
被转成更一般的类(将子类型的引用转换为超类型的引用),例如:
Emploee emp; emp = new Manager(); //将Manager类型的对象直接赋给Emploee类的引用变量,系统会自动将Manager对象塑型为Emploee类
-
被塑型为对象所属类实现的接口类型,例如:
Car jetta = new Car(); Insurable item = jetta;
显式类型转化
基本数据类型
(int)871.34354;//结果为871,是高类型向低类型转换,须显式转换,方法为直接截掉小数部分,是有数据丢失的。
(char)65;//结果为'A'
(long)453;//结果为453L
引用变量
Emploee emp;
Manager man;
emp = new Manager();//如上例,发生了自动向超类的隐含转换,但是emp实际指向的对象就是子类对象
man = (Manager)emp;//将emp显式转换为它所指向的对象的类型。在这种情况下可以将emp引用强制转换为子类型。这个转换不会自动发生,须显示转换。这种强制转换不会发生错误,可以正常运行,就是因为emp真正指向的对象就是子类对象。如果不能确定这一点,一定不要向下转型。
类型转换的主要应用场合
- 赋值转换:将赋值运算符右边的表达式或对象类型转换为左边的类型。
- 方法调用转换:将实参的类型转换为形参的类型。
- 算术表达式转换:算数混合运算时,不同类型的操作数转换为相同的类型在进行运算。
- 字符串转化(字符串拼接):字符串连接运算时,如果一个操作数为字符串,另一个操作数为其他类型,则会自动将其他类型转换为字符串。
类型转换的应用举例
manager
类继承了emploee
类,emploee
类继承了person
类,在person
类中声明了getName
类方法;在Emploee
类中声明了getEmploeeNumber
类方法。当我们将Manager
类型的引用转换为Emploee
类型的引用的时候,这个时候只能访问emploee
类以及它的超类中的方法,如Person
中的getname
方法和Emploee
中的getEmploeenumber()
方法。而Manager
类中的getSalary()
方法就不能通过Manager
类的超类如emploee
的引用去访问了
4.2.3 方法的查找
上一节中学习了可以将子类类型的引用向上转换为超类类型的引用。当发生了引用类型的转换时,如果该引用转换前所属的类型和转换后所属的类型中都声明了同样原型的方法,那么当发生了类型转换后,再通过这个引用去调用或者访问这个方法,将要访问哪个方法体,就是关于方法查找的问题。
方法查找
实例方法的查找
从对象创建时的类开始,沿类层次向上查找
Manager man = new Manager();
Emploee emp1 = new Emploee();
Emploee emp2 = (Emploee)man;
以下探索调用Computepay
方法
emp1.Computepay();//引用是Emploee类型的,实际指向的对象也是emploee类型的,自然调用的是Emploee类型中的Computepay方法
man.Computepay();//man是Manager类型的,实际指向的对象也是Manager类型的对象,调用的是Manager类中的Computepay方法
emp2.Computepay();//该引用是Emploee类型的,但是指向的对象是Manager类型的,按照如上规则从对象创建时的类开始,沿类层次向上查找,也就是说从Manager类开始查找是否有Computepay()方法,所以该语句调用的仍然是Manager类中的Computepay()方法。
类方法的查找
类方法是static
的、静态的、属于整个类的。
Manager man = new Manager();
Emploee emp1 = new Emploee();
Emploee emp2 = (Emploee)man;
以下对其进行测试
man.expenseAllowance();//in Manager
emp1.expenseAllowance();//in Emploee
emp2.expenseAllowance();//in Emploee!!
注意,类方法属于整个类,不属于某个对象,因此在调用emp2.expenseAllowance()
的时候,就不会根据引用所指向的对象是谁来查找这个方法了,因为类方法不属于任何一个对象。因此,唯一的查找方法就是根据引用变量自己的类型。
4.3 多态的概念
多态指的是不同类型的对象可以响应相同的消息,而各自对这个消息的相应行为可以是不同的
多态的概念
- 超类对象和从相同的超类派生出来的多个子类的对象,可以被当作同一种类型的对象对待(因为子类的对象总是可以充当超类对象使用)。
- 实现统一接口不同类型的对象,可以被当作同一种类型的对象对待(被当作接口类型的对象对待)。
- 可向这些不同的类型对象发送同样的消息,由于多态性,这些不同类的对象响应同一消息时的行为可以有所差别。
例如:
-
所有
Object
类的对象都响应同toString()
方法。 -
所有
BankAccount
类的对象都相应deposit()
方法。 - 但是,上述对方法的响应可以不同,因为每个类有自己对超类继承来的方法的一个覆盖,即各自实现了方法体。
多态的目的
- 使代码变得简单且容易理解。
- 使程序具有很好的可扩展性。
例:图形类
-
在超类
Shape
中声明一个绘图方法draw()
、一个擦除方法erase()
。 -
在每个子类中覆盖(重写)了
draw()
和erase()
方法。 -
以后绘图可以如下进行:
Shape s = new Circle(); s.draw();//实际调用的Circle对象的draw()
绑定的概念
绑定是将一个方法调用表达式与方法体的代码结合起来。
根据绑定时期的不同,可分为:
- 早绑定:程序运行之前执行绑定(编译过程中)。
- 晚绑定:也叫做“动态绑定”或“运行期绑定”,是基于对象的类别,在程序运行时执行绑定。
例:动态绑定
仍以绘图为例,所有类都放在binding
包中
超类Shape
建立了一个通用接口(因为draw
和erase
都是空方法体)
class Shape {
void draw();
void erase();
}
子类覆盖了draw()
方法,为每种特殊的几何形状都提供独一无二的行为:
calss Circle extends Shape{
void draw(){
System.out.println("Circle.draw()");
}
void erase(){
System.out.println("Circle.erase()");
}
}
calss Square extends Shape{
void draw(){
System.out.println("Square.draw()");
}
void erase(){
System.out.println("Square.erase()");
}
}
calss Triangle extends Shape{
void draw(){
System.out.println("Triangle.draw()");
}
void erase(){
System.out.println("Triangle.erase()");
}
}
对动态绑定进行如下测试:
public class BindingTester{
public satic void main(String[] args){
Shape[] s = new Shape[9];
int n;
for(int i = 0;i < s.length();i++){
n = (int)(Math.random() * 3);
switch(n){
case 0:s[i] = new Circle();
break;
case 1:s[i] = new Square();
break;
case 2:s[i] = new Triangle();
}
}
for(int i = 0;i < s.length();i++){
s[i].draw;
}
}
}
运行结果(由于random
随机数的特点,所以以下仅为某一次实验的结果):
Square.draw()
Triangle.draw();
Cicrcle.drwa();
Triangle.draw();
Triangle.draw();
Cicrcle.drwa();
Square.draw()
Cicrcle.drwa();
Triangle.draw();
说明
-
在主方法的循环体中,每次随机生成一个
Circle
、Square()
或者Triangle()
对象。 -
编译时无法知道
s
数组元素指向的实际对象类型,运行时才能确定类型,所以是动态绑定。
小结:多态性的基础,一个是动态绑定技术,一个是向上转型技术。
4.4 多态的应用举例
例:二次分发
-
有不同种类的交通工具(
vehicle
),如公共汽车(bus
)及小汽车(car
),由此可以声明一个抽象类Vehicle
及两个子类Bus
及Car
。 -
声明一个抽象类
Driver
和两个子类FemaleDriver
及MaleDriver
。 -
在
Driver
类中声明了抽象方法drives
,在两个子类中对这个方法进行覆盖。 -
drives
方法接受一个Vehicle
类的参数,当不同类型的交通工具被传送到此方法时,可以输出具体的交通工具。 -
所有类放在
drive
包中。
测试代码:
package drive;
public class DriverTest {
static void main(String args[]) {
Driver a = new FemaleDriver();//虽然a是Driver类型,但是实际指向的对象是FemaleDriver类型
Driver b = new MaleDriver();
Vehicle x = new car();
Vehicle y = new bus();
a.drives(x);
b.drives(y);
}
}
希望得到的输出:
A Female driver drives a car
A male driver drives a bus
Vehicle
及其子类声明如下
package drive;
//抽象类
public abstract class Vehicle {
private String type;
public Vehicle() {
};
//抽象方法
public abstract void drivedByFemaleDriver();
//抽象方法
public abstract void drivedByMaleDriver();
}
package drive;
public class Car extends Vehicle {
public Car() {
};
public void drivedByFemaleDriver() {
System.out.println("A Female driver drives a car");
}
public void drivedByMaleDriver() {
System.out.println("A Male driver drives a car");
}
}
package drive;
public class Bus {
public Bus() {
};
public void drivedByFemaleDriver() {
System.out.println("A female driver drives a bus");
}
public void drivedByMaleDriver() {
System.out.println("A male driver drives a bus");
}
}
Driver
及其子类声明如下
package drive;
public abstract class Driver {
public Driver() {
};
public abstract void drives(Vehicle v);
}
package drive;
public class FemaleDriver extends Driver {
public FemaleDriver() {
};
public void drives(Vehicle v) {
v.drivedByFemaleDriver();
}
}
package drive;
public class MaleDriver extends Driver {
public MaleDriver() {
};
public void drives(Vehicle v) {
v.drivedByMaleDriver();
}
}
说明:
-
这种技术成为二次分发(“
double dispatching
”),即对输出消息的请求被分发两次。 - 首先根据驾驶员的类型被发送给一个类。
- 之后根据交通工具的类型被发送给另一个类。
4.5 构造方法与多态性
构造方法与其他方法有区别,是不具有多态性的特点的。但是仍需了解在构造方法中调用了多态的方法会怎么样。
构造子类对象时构造方法的调用顺序
- 首先调用超类的构造方法(如果有超类的话),这个步骤会不断重复下去,首先被执行的是最远超类的构造方法。
- 执行当前子类的构造方法体其他语句。
例:构造方法的调用顺序
构建一个点类Point
,一个球类Ball
,一个运动的球类MovingBall
继承自Ball
public class Point {
private double xCoordinate;
private double yCoordinate;
public Point() {
};//没有参数的构造方法
public Point(double x, double y) {
xCoordinate = x;
yCoordinate = y;
}//有参数的构造方法
public String toString() {
return "(" + Double.toString(xCoordinate) + "," + Double.toString(yCoordinate) + ")";
}
}
public class Ball {
private Point center;//中心点
private double radius;//半径
private String color;//颜色
public Ball() {
};//无参数的构造方法
public Ball(double xValue, double yValue, double r) {//三个参数的构造方法
center = new Point(xValue, yValue);//调用Point中的构造方法
radius = r;
}
public Ball(double xValue, double yValue, double r, String c) {//四个参数的构造方法,可以直接服用调用三个参数的构造方法
this(xValue, yValue, r);
color = c;
}
public String toString() {
return "A ball with center " + center.toString() + ",radius " + Double.toString(radius) + ",colour" + color;
}
}
public class MovingBall extends Ball {
private double speed;
public MovingBall() {
};
public MovingBall(double xValue, double yValue, double r, String c, double s) {
super(xValue, yValue, r, c);//注意要先调用超类的方法
speed = s;
}
public String toString() {
return super.toString() + ",speed" + Double.toString(speed);
}
}
子类不能直接存取父类中声明的私有数据成员,super.toString()
调用父类Ball
的toString
方法输出类Ball
中声明的属性值。
public class Tester {
public static void main(String args[]) {
MovingBall mb = new MovingBall(10, 20, 40, "green", 25);
System.out.println(mb);
}
}
输出
A ball with center (10.0,20.0),radius 40.0,colourgreen,speed25.0
构造方法的调用顺序为MovingBall(double xValue,double yValue,double r,String c,double s)
->Ball(double xValue,double yValue,double r,String c)
->Ball(double xValue,double yValue,double r)
->Point(double x,double y)
例:构造方法中调用多态方法
在Glyph
中声明一个抽象方法,并在构造方法内部调用之
abstract class Glyph {
abstract void draw();
Glyph() {
System.out.println("Glyph() before draw()");
draw();// 不必担心找不到方法体,因为抽象类不能生成对象,所以一定是某一个非抽象子类的draw()方法
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
int radius = 1;
//第一句应当是调用超类构造方法,但是没有显式调用超类构造方法,就会默认调用超类无参数的构造方法
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(),radius = " + radius);
}
void draw() {
System.out.println("RoundGlyph.draw(),radius = " + radius);
}
}
public class PolyConstructors {//测试
public static void main(String args[]) {
new RoundGlyph(5);
}
}
输出:
Glyph() before draw()
RoundGlyph.draw(),radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius = 5
分析:第一个输出的radius
等于是因为,此时对象还没构造好,RoundGlyph
中的radius = 1
还没初始化运行,因此就是没有初始化的默认值;而在Java
中没有数值型数据没有初始化的默认值是。
说明:
-
在
Glyph
中,draw()
方法是抽象方法,在子类RoundGlyph
中对此方法进行了覆盖,Glyph
的构造方法调用了这个方法。 -
从运行的结果可以看到:当
Glyph
的构造方法调用draw()
时,radius
的值甚至不是默认的初始值,而是。
实现构造方法的注意事项:
- 用尽可能少的动作把对象的状态设置好,即,构造方法就是用来初始化的,除了初始化以外最好不要做别的事。
- 如果可以避免,不要调用任何方法。
-
在构造方法内唯一能够安全调用的是在超类中具有
final
属性的哪些方法(也适用于private
方法,它们具有final
属性)。这些方法不能被覆盖,所以不会出现前述的潜在问题。