?第四章已经有论及,抽象性、封装性、继承性和多态性称为面向对象的四大特性。通过前面的学习,对前两个特性有了一定程度的理解,本章将继续探讨后两个特性,即继承性和多态性。
第 5章 继承与多态
类的继承;
类成员的隐藏与重载;
多态性;
Object类和 Class类;
抽象类与接口;
对象克隆;
对象转型和类的设计原则本章要点
Java中的继承分类的继承和接口的继承两种,类的继承只支持单继承;而接口的继承可以是多重继承。
5.1 类的继承
[Modifiers] class SubClassName extends SuperClassName
{ //ClassBody
}
子类的定义 子类也是类。
它的定义形式与一般类的定义形式极其相似,其格式为:
其中,Modifiers是修饰符,可以使用的修饰符与前一章中所介绍的一样; SubClassName是子类的名称; extends是用于实现继承的关键字,当类的定义中有 extends关键字时,表示当前定义的类继承于别的类,是别的类的子类;
SuperClassName是父类名; ClassBody是子类的类体。
5.1 类的继承 ——子类的定义
class Point{ //点类
protected int x,y; //点坐标
public Point(){} //无参构造方法
public Point(int xx,int yy){setPoint(xx,yy);}
public void setPoint(int m,int n){x=m;y=n;} //设置标位置
public int getX(){return x;}
public int getY(){return y;}
}
class Circle extends Point{ //定义圆类
private double radius; //radius为圆的半径
public Circle(int x,int y,double r){this.x=x;this.y=y;setRadius(r);}
public void setRadius(double r){radius=r;} //设置圆的半径
public double getRadius(){return radius;} //获取圆的半径
public double getArea(){return 3.1415*radius*radius;} //获取圆的面积
5.1 类的继承 ——子类的定义示例继承于点类的圆类
public String toString()
{ return"Position("+x+","+y+")Radius="+radius;}
}
public class CircleTest{
public static void main(String[] args){
Circle c=new Circle(50,50,10);
System.out.println(c.toString());
c.setPoint(100,100);
c.setRadius(20);
System.out.println(c.toString());
}}
5.1 类的继承 ——子类的定义示例例中,定义了三个类 Point,Circle和 CircleTest。 Circle继承于 Point类,
Point类是父类,Circle是子类,它们的 UML图及相互关系如图 5.1所示。
而 CircleTest类用于对 Circle作一简单测试。
5.1 类的继承 ——子类的定义示例图 5.1
在子类的定义示例 有这样一条语句:
5.1 类的继承 ——子类的构造方法
public Point(){} //无参构造方法
Java中,在执行子类的构造方法时,必须先执行父类的构造方法,它的第一条语句必须是调用父类的构造方法的语句。
调用父类的构造方法是通过语句 super()来完成的。如果要调用父类的有参构造方法,则在 super()的圆括号中加入所需的参数。
当在子类的构造方法中通过 super()来调用父类的构造方法时,
如果不带参数,则这样的显式调用可以省略,它调用父类的构造方法是由系统自动进行的。但是,此时,如果父类中没有显式定义无参的构造方法,则编译出错。
通过类的继承,子类继承了父类的成员,同时在子类中还可以添加一些新成员。但是,当在子类中添加的成员与父类中的成员同名时,系统如何处理这样的冲突呢?本节主要探讨这个问题。
5.2 类成员的隐藏与重载
通过继承,子类继承了父类中除构造方法以外的所有成员,这些成员称为子类的继承成员。继承成员不仅包括在超类中定义的公有( public)、受保护( protected)及私有成员( private),还包括超类的继承成员。但是在子类中,只能直接引用父类中用
public或 protected修饰的成员,父类中用 private修饰的成员在子类中不可直接引用,因为它们受访问属性的控制。
使用子类的程序能访问子类的公有继承成员,但不能访问子类的受保护和私有的继承成员。子类的内部能访问它自己定义的所有成员,在程序中,只能访问子类自己定义的公有成员,而不能访问子类自己定义的受保护和私有成员。
5.2 类成员的隐藏与重载 ——类成员的继承
当在子类中添加的新成员变量与父类中的成员变量同名时,
父类中对应的成员变量将被隐藏起来。在子类中,如果直接引用了这些同名的变量,则引用的是子类中定义的同名变量。
当在子类中要引用父类中的同名变量时,可以通过 super关键字来进行。 super关键字用于指向当前类的父类。请读者结合前面所学的知识,自行区别 this,super和 super()三者的不同。
5.2 类成员的隐藏与重载 ——成员变量的隐藏
class SuperClass{
protected int x,y;
SuperClass(){}
SuperClass(int x,int y){this.x=x;this.y=y;}
String superToString(){return "In SuperClass:x="+x+" y="+y;}
}
class SubClass extends SuperClass{
protected int x,y;
SubClass(){}
SubClass(int x,int y){super(x-1,y-1);this.x=x;this.y=y;}
String subToString(){return "In SubClass:x="+x+" y="+y;}
String superToStringInSub(){
return "In SuperClass Called by super in
SubClass:super.x="+super.x+" super.y="+super.y;
}
}
5.2 类成员的隐藏与重载 ——成员变量的隐藏示例成员变量的隐藏及 super的用法
In SuperClass:x=5 y=5
In SubClass:x=10 y=10
In SuperClass Called by super in SubClass:super.x=9 super.y=9
5.2 类成员的隐藏与重载 ——成员变量的隐藏示例
public class Example5_2{
public static void main(String[] args){
SuperClass sup=new SuperClass(5,5);
SubClass sub=new SubClass(10,10);
System.out.println(sup.superToString());
System.out.println(sub.subToString());
System.out.println(sub.superToStringInSub());
}
}
编译运行得到如下结果:
子类可以继承父类中的成员方法,同时也可以在子类中定义一些新的成员方法,这些新添加的成员方法有如下三种情况:
在子类中所添加的成员方法是全新的成员方法,它与父类中的成员方法不存在同名冲突问题。这是子类对父类功能所作的一种扩展。
在子类中,定义了与父类同名的成员方法,但其参数不一样,这种情况称为成员方法的重载。这也是一种重载,只不过它的重载不是在同一个类中来完成的,而是在子类和父类中共同完成。这也是子类对父类功能所作的一种扩展。
在子类中定义了与父类中同名同参的成员方法,这种情况被称为成员方法的覆盖。这种情况就像成员变量的隐藏一样。如果在子类中或通过子类的对象直接引用同名的方法,则引用的是子类中所定义的成员方法;若要引用父类中的同名方法,也可以通过关键字
super来完成。这是子类对父类所作的一种修改。
5.2 类成员的隐藏与重载 ——成员方法的重载与覆盖构造方法不能被覆盖,它们的名称总是与当前类相同。构造方法是新创建的,而不是继承而来的,子类不能直接通过方法名调用父类的一个构造方法,而是通过关键字 super调用父类的一个构造方法。
由于构造方法没有可供调用的方法名,要调用超类中的常规方法,常采用如下格式:
5.2 类成员的隐藏与重载 ——构造方法的覆盖
super(arg1,arg2,…,)
super语句必须是构造函数定义中的第一条语句。如下面的程序是一个名为 SubPoint的类,它是从 java.awt包中的 Point类中派生而来的。
SubPoint类的定义了一个用于初始化 x,y和名称的构造函数。
import java.awt.Point;
class SubPoint extends Point{
String name;
SubPoint(int x,int y,String name){
super(x,y);
this.name=name;
}
public static void main(String[] arguments){
SubPoint p=new SubPoint(8,9,”subPoint”);
System.out.printIn(“x is,+ p.x);
System.out.printIn(“y is,+ p.y);
System.out.printIn(“name is,+ p.name);
}
}
5.2 类成员的隐藏与重载 ——构造方法的覆盖示例成员变量的隐藏具有覆盖构造方法的坐标点类 Point 及 super的用法
多态性表示同一种事物的多种形态;不同的对象对相同的消息可有不同的解释,这就形成多态性。在面向对象的方法中,对一个类似的操作,使用相同消息的能力与求解问题中人的思维模式是一致的,如对打印整数、浮点数、字符、字符串和数据记录等使用不同的术语是不自然的。为此,可以在此基础上抽象出一个关于数据打印的类来描述数据的打印,而对不同数据打印的具体操作根据不同的数据类型而有所区别,从而编制不同的具体打印操作方法。当需要打印数据时,不论是打印整数还是打印字符串,只需发出打印数据的消息,而其具体操作可根据打印的数据类型的不同而调用不同的打印操作方法。这样做,可使设计在更高的层次上进行,简化了处理问题的复杂性,使设计出来的程序具有良好的可
5.3 多态性
多态性具有更广泛的定义:如果操作方法及它们的操作对象多于一个,则称为具有多态性。 Gardelli和 Wegner将各种多态性严格的分为四类:
5.3 多态性多态性包含多态性强制多态性重载多态性参数多态性通用的专用的
Java的标准系统包提供了很多类,其中,在 java.lang包中所提供的的 Object类和 Class类经常用到,在本节对它们作一简单说明。
5.4 Object类和 Class类
Object类是 Java的所有类的根,如果一个类在定义时不继承于任何类,则它也有一个默认的父类,那就是
Object类。
Object具有很多用途,它为程序设计带来了极大的方便,
它至少在以下三个方面发挥了极大的作用:
Object类型的变量可以用来引用任何类型的对象,这在多态性的实现中经常用到,可将它作为一个通用的父类,
这在建立一些通用结构时极其重要。
将成员方法的参数设置成 Object类类型的变量时,可接收任何类型的对象传来的参数,这对于很多处理是相当方便的。
Object提供了很多公有的( public)或保护的
( protected)成员方法,这些成员方法是所有类都可能会用到的成员方法,在子类中能直接引用。
5.4 Object类和 Class类 ——Object类
Class类是一个公有的( public)最终类( final),它直接继承于 Object
类。 Class类常用于描述正在运行的类或接口等。
Class类没有公有的构造方法,Class的实例可通过类的对象调用
getClass()方法来得到。此时,Class类的实例化其实是由 Java虚拟机( JVM)来自动完成的。下面的方法用于打印一个对象的类名信息:
5.4 Object类和 Class类 ——Class类
void printClassName(Object obj) {
System.out.println("The class of " + obj + " is " + obj.getClass().getName());
}
在本例中,obj.getClass()方法的返回值是 Class类型,由 JVM自动创建
Class类型的一个对象,再通过该对象调用 Class类中的成员方法
getName()获得 obj的类名 。
抽象类不同于一般的类,它不能实例化对象;接口是一种特殊的抽象类,用于提供规范的公共接口。抽象类和接口既有相似的地方,也有不同,下面对它们分别进行探讨。
5.5 抽象类与接口
5.5 抽象类与接口 ——抽象类人们往往用建立抽象类的方法为一组类提供统一的界面。抽象类的概念来源于现实生活之中。如现实生活中有打扑克牌、打桥牌和打麻将牌等不同的打牌娱乐活动,打扑克牌也分很多种,如有升级、双扣、锄大地、斗地主等。总之,不同国家、不同地区的这种打牌游戏很多,人们对它们进行了归纳,从而形成一个抽象的类 ----打牌类。这就使我们能在一个更高、
更抽象的级别上考虑问题,从而简化了问题的复杂性。其实,这里介绍的抽象类就是第 4章中关于抽象概念的最直接的一种代码实现。
抽象类不同于一般的类,它不能实例化对象,因为其中的动作是抽象的,
如打牌,只说打牌,没说打什么牌,这是无法进行的。,打牌,是一抽象的概念。
Java抽象类的实现是通过关键字 abstract来说明的。其格式为:
[Modifies] abstract class ClassName{…… }
5.5 抽象类与接口 ——抽象类
Modifies是修饰符,abstract是声明抽象类的关键字,class是定义类的关键字,ClassName是类名,大括号内的省略号表示类体部分。其中的成员方法可以是一般的成员方法,还可以是抽象的成员方法。抽象的成员方法也是通过关键字 abstract来说明的。它在形式上仅有方法的头部分,而没有方法体,甚至用于描述方法体的一对大括号也没有,常将这样的形式称为方法的原型声明。其格式如下:
[Modifies] abstract returnType methodName(parameterLists);
Modifies与上面的意义相同,abstract是声明抽象方法的关键字,
returnType是方法的返回值类型,圆括号中的 parameterLists是参数列表。
抽象方法显然不是一个完整的方法,它也不完成任何具体的功能,只是用于提供一个接口,它只有在子类中进行覆盖后才可使用,因此,抽象方法只能出现在抽象类中 。
5.5 抽象类与接口 ——抽象类关于抽象类还需作如下几点说明:
定义抽象类的目的是用于描述一个抽象的概念,它不能实例化对象。因此,常通过抽象类派生出子类,由子类来实现具体的功能,抽象类仅提供一个统一的接口。派生出的子类可以是抽象类,也可以是非抽象的类,即一般的类。
如果由抽象类派生出一个非抽象的子类,则在子类中必须覆盖掉父类(抽象类)中所有的抽象方法,否则,只能将子类定义为抽象类。
抽象类虽然不能实例化对象,但可以定义抽象类类型的变量,
由该变量可以引用此抽象类的某个非抽象子类的对象,这在多态性的实现中经常用到。
任何抽象方法都只能出现在抽象类中。
static,private和 final修饰符不能用来修饰抽象类和抽象方法。
5.5 抽象类与接口 ——接口接口实质上是一种特殊的抽象类,其内部只能包含静态常量和抽象方法。但在 Java中,对接口的定义形式和处理方法与抽象类却截然不同。下面对有关接口的知识进行探讨。
5.5 抽象类与接口 ——接口声明接口很象类,声明方式几乎与类相同,可以被组织成层次结构。接口的声明格式为:
[public] interface InterfaceName{//静态常量及抽象方法的声明 }
方括号表示可省略部分。访问属性控制符 public与用于修饰类的 public意义一致,如果省略 public则就是第四章所介绍的默认访问控制属性。
Interface是用于定义接口的关键字。 InterfaceName是接口的名字。
常量和抽象方法的声明放在一对大括号中。
在 Java中,编译器将常量的定义默认为 public static final类型的静态常量,
不论是否使用了这些修饰符,它都是这样处理的。所以在定义常量时,
可以只给出其数据类型说明和常量名,同时,定义时要为每个常量都赋值。因为成员方法都是抽象的,在定义成员方法时也可以省略关键字 abstract,它默认也是抽象的。
5.5 抽象类与接口 ——接口声明接口声明注意事项:
接口内的方法定义是公有和抽象的,如果没有包括这些限定符,它们将被自动转换为公有和抽象。
不能在接口内将方法声明为私有( private)或保护的
(protected)。
接口内定义的变量必须声明为公有、静态和 final,或者不使用限定符。
声明接口时没有加上限定符 public,接口不会自动将接口内的方法转换为公用和抽象的,也不会将其常量转换为公有的。
非公有接口的方法和常量也是非公有的,它们只能被同一个包的类或其他接口使用。
5.5 抽象类与接口 ——接口的继承接口和类一样,也可以继承。不过,类仅支持单继承,而接口既支持单继承,也支持多重继承。通过继承,一个接口可以继承父接口中的所有成员。接口的继承也是通过关键字 extends来说明的。接口的单继承示例:public interface SubMathInterface extends MathInterface{
double minuteToRadian();//分转换为弧度
double RadianToMinute();//弧度转换为分
double secondToRadian();//秒转换为弧度
double radianToSecond();//弧度转换为秒
}
通过继承,在子接口 SubMathInterface中不仅有此处定义的四个方法,而且也继承了父接口 MathInterface中的所有常量和方法。
5.5 抽象类与接口 ——接口的实现定义抽象类也好,定义接口也好,都是为了使用。要使抽象类发挥功能,必须通过抽象类派生出一个非抽象子类,在子类中覆盖掉父类中的所有抽象方法来实现。但是,如何使接口发挥其功能呢?显然通过派生子接口是无法完成的,
因为派生出的子接口还是接口,同样不能实例化对象。在
Java中,要让接口发挥其功能,需定义一个普通的类,在这个类中覆盖掉接口中的所有方法,以便将其完善,这称为某个类对接口的实现,实现接口是通过关键字
implements来说明的。类实现接口后,其子类将继承这些新的方法(可以覆盖或重载它们),就象超类定义了这些方法一样。
5.6 对象克隆对于基本数据类型,赋值操作就是将赋值符号右侧表达式的值赋给左侧的变量。然而,当参与赋值操作的变量为引用类型(类类型)时,结果就不一样了。在第四章介绍对象的清除时就已了解到,一个引用型变量的内容并不是所引用的那个对象的具体内容,而是与该对象有关的一些存储信息。通过引用,可以方便地对所引用的对象进行各种操作。
因此,引用型变量的赋值操作实际上是引用的相互赋值。如有一个描述学生的类 Stu,其中有两个实例变量 stuNo和 name,分别用来描述学号和姓名,有如下的语句:
Stu s1,s2;
s1=new Stu(B0537082,“WangXia”);
s2=s1;
其中,s1和 s2都是引用型变量,s1的内容为 Stu类对象的引用,而不是对象本身。赋值操作 s2=s1只是将 s1所指示的对象引用赋值给 s2,使得
s2和 s1引用同一个对象 。
类的继承研究的是类之间的关系,而对象转型研究的则是具有继承关系的类的对象引用(即类类型的变量)之间的关系。对象的转型其实就是一种数据类型的转换,如前面已说明了的基本数据类型之间在赋值或运算过程中有可能进行的类型转换也是一种对象的转型,但本处讨论的转型主要是类的对象引用之间的转型,简单地讲就是类类型的变量之间的相互转换问题,主要是相互赋值问题。本节主要探讨对象转型,然后接着对类的设计原则作简单介绍。
5.7对象转型和类的设计原则
开闭原则:一个类在扩展性方面应该是开放的,而在更改方面应该是封闭的。在设计一个类时应尽量考虑它的可扩展性,一个扩展性好的类是比较受欢迎的。同时,一个类应该具有很好的封闭性,这主要靠数据的封装来实现。
替换原则:子类应当可以替换父类的使用,并出现在父类能够出现的任何地方,这主要是靠子类继承父类的所有属性和行为来完成的。
依赖原则:在进行业务设计时,与特定业务有关的依赖关系应该尽量依赖接口和抽象类,而不是依赖于具体类。具体类只负责相关业务的实现,修改具体类不影响与特定业务有关的依赖关系。为此,
我们在进行业务设计时,应尽量在接口或抽象类中定义业务方法的原型,并通过具体的实现类(子类)来实现该业务方法;业务方法内容的修改将不会影响到运行时业务方法的调用。
单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。
在构造对象时,将对象的不同职责分离至两个或多个类中,确保引起该类变化的原因只有一个。其带来的好处:提高内聚、降低耦合。
5.7对象转型和类的设计原则 ——类的设计原则继承是构成子类型关系的一种重要途径,类 A继承类 B,则类型 A是类型
B的子类型。 JAVA语言允许祖先类类型的引用变量引用后代类类型的对象实例。如,B obj=new A();// 超类的引用变量 obj引用子类对象实例类似的,可将后代类型的引用变量隐式地转换为父类类型的引用变量,
如,A obj1=new A();B obj2=new obj1;
两个对象引用之间并不是可以任意相互赋值的,它们之间赋值遵循如下规则:
同个类的不同对象引用之间可以任意相互赋值;
子类的对象引用可以直接赋值给父类的对象引用;
父类的对象引用也可以赋值给子类的对象引用,但是有条件的,其条件为:只有当父类的对象引用已引用了子类的对象时,通过强制类型转换,才能将父类对象的引用赋值给子类对象的引用;如果一个父类对象的引用未指向一个子类对象,那当通过强制类型转换将它赋值给一个子类对象的引用时,运行时出错。
没有继承关系的不同类的对象引用之间不能相互赋值。
5.7对象转型和类的设计原则 ——对象转型