ch04.docx
《ch04.docx》由会员分享,可在线阅读,更多相关《ch04.docx(29页珍藏版)》请在冰豆网上搜索。
ch04
第4章继承和多态
继承是Java语言的一个重要特性。
本章主要介绍关于继承的若干应用。
通过继承,子类拥有父类的所有非私有成员。
子类对继承自父类的类成员重新进行定义,就称为覆盖。
方法的重载是一个类中对自身已有的同名方法的重新定义。
每个重载方法的参数的类型和(或)数量必须是不同的。
用abstract修饰符修饰的类称为抽象类,抽象类不能实例化为对象。
如果一个类被final修饰符所修饰,说明这个类不可能有子类,这样的类就称为最终类。
最终类不能被别的类继承,它的方法也不能被覆盖。
接口用interface来声明。
接口中的域变量都是常量,方法都是没有方法体的抽象方法,其方法的真正实现在“继承”这个接口的各个类中完成。
一个类只能有一个父类,但是类可以同时实现若干个接口,从而实现了多重继承。
本章要点
●继承和多态的概念、特点以及实现方法
●域的继承与隐藏
●方法的继承与覆盖
●接口的实现
本章难点
●继承和多态的实现方法
●接口的实现
学习目标
●理解继承、多态、接口的概念
●掌握继承、多态、接口在Java中的实现。
4.1继承和多态的概念
4.1.1继承的概念
继承实际上是存在于面向对象程序中的两个类之间的一种关系。
当一个类A能够获取另一个类B中所有非私有的数据和操作的定义作为自己的部分或全部成分时,就称这两个类之间具有继承关系。
被继承的类B称为父类或超类,继承了父类或超类的数据和操作的类A称为子类或衍生类,一个父类可以同时拥有多个子类。
使用继承具有以下的好处:
降低了代码编写中的冗余度,更好地实现了代码复用的功能,从而提高了程序编写的效率,使得程序维护变得简单、方便。
现以汽车为例,汽车分为很多种,比如公共汽车、警车、私家车等。
如果要实现对汽车的管理,就需要为每一种汽车创建一个类,而每一个类中都会具有所有汽车共有的属性和方法,比如每个汽车类都会有颜色、车灯等属性,都会有加速、刹车等方法,这样势必带来代码的重复,维护起来也比较麻烦。
如果我们将所有汽车共有的属性和方法都抽象出来,构建一个汽车类,让各种汽车都继承这个类,这样在汽车这个类中所拥有的属性就会衍生在它的子类中,各种汽车都会具有汽车类所拥有的属性和方法,我们就不用为每种汽车定义像颜色、车灯这样的属性,以及加速、刹车等相同的方法了。
而且维护起来也比较容易了。
这种结构如图4-1所示。
图4-1汽车类的继承
继承分为单重继承和多重继承两种类型,所谓单重继承,是指任何一个类都只有一个单一的父类;而多重继承是指一个类可以有一个以上的父类。
采用单重继承的程序结构比较简单,是单纯的树状结构,掌握、控制起来相对容易;而支持多重继承的程序,其结构则是复杂的网状,设计、实现都比较复杂。
Java语言仅支持类的单重继承。
4.1.2多态的概念
多态是面向对象程序设计的又一个特殊特性。
所谓多态,是指一个程序中同名的不同方法共存的情况。
面向对象的程序中多态的情况有多种,可以通过子类对父类成员的覆盖实现多态,也可以利用在同一个类中同名方法的重载来实现多态。
多态是指一个方法声明的多个不同表现形式。
在同一个类中或者不同的类中,同一个方法的多个不同实现。
比如同样是刹车这样一个功能,在不同的汽车中所要做的操作可能是不一样的,这样我们在程序中就要为刹车这个方法名创建几个不同的实现,也就是说,刹车这个方法名可能会有几个不同的方法体,这就是面向对象程序设计中多态的概念。
4.2类的继承
4.2.1继承的实现
Java中的继承是通过extends关键字来实现的,在定义类时使用extends关键字指明新定义类的父类,就在两个类之间建立了继承关系。
其语法是:
[类修饰符]class子类名extends父类名
从上面的语法格式,我们可以看出比一般类的声明多了“extends”关键字部分,通过该关键字来指明子类要继承的父类。
如果,父类和子类不在同一个包中,则需要使用“import”语句来引入父类所在的包。
【例4_1】继承的简单示例。
classFather{
intf_a=0;
voidprint(){
System.out.println("f_a="+f_a);
}
}
classSonextendsFather{
ints_a=10;
}
classExam4_1{
publicstaticvoidmain(String[]args){
Sons=newSon();
s.print();
System.out.println("f_a="+s.f_a);
System.out.println("s_a="+s.s_a);
}
}
在例子中可以看到,在子类的声明中使用“extends”关键字指明一个被继承的父类就可以实现类之间的继承关系。
Son类中虽然并没有定义f_a变量和print()方法,但是仍然可以访问,这是因为它从父类中继承过来的原因。
子类Son还定义了自己的变量s_a,这说明了子类可以拥有比父类更多的变量和方法,也就是说子类拥有更强大的功能。
程序的执行结果如图4-2所示。
图4-2例4_1程序运行结果
4.2.2类成员的继承
新定义的子类可以从父类那里自动继承所有非private的属性和方法作为自己的成员。
同时根据需要再加入一些自己的属性或方法就产生了一个新的子类。
可见父类的所有非私有成员实际是各子类都拥有集合的一部分,这样做的好处是减少程序维护的工作量。
从父类继承来的成员,就成为了子类所有成员的一部分,子类可以使用它。
【例4_2】继承的示例。
classFather{
privateintf_a=0;
privatevoidprint(){
System.out.println("f_a="+f_a);
}
}
classSonextendsFather{
ints_a=10;
}
classExam4_2{
publicstaticvoidmain(String[]args){
Sons=newSon();
//s.print();
//System.out.println("f_a="+s.f_a);
System.out.println("s_a="+s.s_a);
}
}
仍然如4.1的例子,但此时我们把父类中的变量f_a和方法print()修改为了private的作用域,此时在子类中就不能访问这两个成员了,因为子类不能继承私有的父类成员,程序中注释掉的两条语句是错误的读者可自行检验。
程序的运行结果如图4—3所示。
图4-3例4_2程序运行结果
【例4_3】继承的示例。
classFather{
privateintf_a=0;
voidprint(){
System.out.println("f_a="+f_a);
}
}
classSonextendsFather{
ints_a=10;
}
classExam4_3{
publicstaticvoidmain(String[]args){
Sons=newSon();
s.print();
//System.out.println("f_a="+s.f_a);
System.out.println("s_a="+s.s_a);
}
}
继续对4.1的例子作修改,把把父类中的变量f_a修改为了private的作用域,但方法print()仍然保持默认作用域,此时可以看到在子类中直接访问变量f_a会出现错误,但通过print()方法我们却可以访问到私有作用域的f_a变量,这又是因为什么呢?
这是因为print()方法被子类继承,print()方法和变量f_a都是父类的成员,它们之间进行访问是不存在问题的,利用公共的方法去访问私有的成员,这正是面向对象封装特点的好处,它使得在类外不能直接访问私有的属性,但可以利用方法作为接口对变量进行读取操作,增强了程序的健壮性。
程序的运行结果如图4_4所示。
图4-4例4_3程序运行结果
4.2.3替代原理
有了继承特点,面向对象编程中就存在了一个替代原理,用一句话讲替代原理的内容是:
子类对象总可以看作一个父类对象。
也就是说,在所有使用父类对象的地方,都可以使用一个子类对象来代替父类对象,也有人把这种原理称为“isa”关系。
比如在例4.1中,f_a变量和print()方法是父类的成员,应该用父类的对象去访问,但我们却使用了子类的对象去访问这两个成员,这可以认为是一种简单的替代,我们来看下面这个例子。
【例4_4】替代原理示例。
classFather{
privateintf_a=0;
voidprint(Fatherf){
System.out.println("f_a="+f.f_a);
}
}
classSonextendsFather{
}
classExam4_4{
publicstaticvoidmain(String[]args){
Fatherf=newFather();
Sons=newSon();
f.print(s);
}
}
我们给print()方法加上了参数,这个参数是一个父类的引用,但我们调用时,却给方法传递了一个子类的引用s,程序却依然可以正常运行,这就是替代原理。
程序的运行结果如图4-5所示。
图4-5例4_4程序运行结果
4.3类成员的覆盖
4.3.1覆盖的用法
在程序的设计过程中,我们通过继承可以快速地将父类中已实现的非私有类成员应用到自己定义的子类中。
但是,不是所有继承下来的类成员都是我们需要的,这时候我们就可以通过使用覆盖的方式来解决这个问题。
子类对继承自父类的类成员重新进行定义,就称为覆盖。
要进行覆盖,就是在子类中对需要覆盖的类成员以父类中相同的格式,再重新声明定义一次,这样就可以对继承下来的类成员进行功能的重新实现,从而达到程序设计的要求。
【例4_5】覆盖的示例。
classFather{
intf_a=0;
voidprint(){
System.out.println("f_a="+f_a);
}
}
classSonextendsFather{
intf_a=10;
voidprint(){
System.out.println("f_a="+f_a);
}
}
classExam4_5{
publicstaticvoidmain(String[]args){
Sons=newSon();
s.print();
System.out.println("f_a="+s.f_a);
}
}
我们在子类中重新定义了print()方法和变量f_a,此时子类的print()方法覆盖了父类的print()方法,子类中的变量f_a隐藏了父类中的变量f_a,在子类中再去调用print()方法或者访问变量f_a都是访问的子类中的成员,因此此时只输出的变量f_a都是子类中的变量f_a=10,而没有输出父类的变量f_a=0。
程序运行结果如图4-6所示。
图4-6例4_5程序运行结果
4.3.2super参考
如果要使用父类中被覆盖的方法或被隐藏的变量,此时可以使用super参考。
相对this来说,super表示的是当前类的直接父类对象,是当前对象的直接父类对象的引用。
所谓直接父类是相对于当前类的其他“祖先”类而言的。
例如,假设类A派生出子类B,B类又派生出自己的子类C,则B是C的直接父类,而A是C的祖先类。
super代表的就是直接父类。
这就使得我们可以比较简便、直观地在子类中引用直接父类中的相应属性或方法。
【例4_6】super参考的示例。
classFather{
intf_a=0;
voidprint(){
System.out.println("f_a="+f_a);
}
}
classSonextendsFather{
intf_a=10;
voidprint(){
super.print();
System.out.println("f_a="+super.f_a);
}
}
classExam4_6{
publicstaticvoidmain(String[]args){
Sons=newSon();
s.print();
System.out.println("f_a="+s.f_a);
}
}
程序中我们对子类的print()方法作以修改,在方法内部我们首先使用了super关键字引用了父类的print()方法,输出了父类的变量f_a,然后我们又通过super关键字直接引用了父类的变量f_a,最后才输出了子类中的变量f_a。
程序运行的结果如图4-7所示。
图4-7例4_6程序运行结果
4.4方法重载
4.4.1方法的重载
在Java中,同一个类中多个同名方法之间构成重载关系,在完成同一功能时,可能遇到不同的具体情况,所以需要定义含不同的具体内容的方法,这些方法的具体实现代码可能不一样,但他们的名称相同,这些方法间构成重载。
例如,一个类需要具有打印的功能,而打印是一个很广泛的概念,对应的具体情况和操作有多种,如实数打印、整数打印、字符打印、分行打印等。
为了使打印功能完整,在这个类中就可以定义若干个名字都叫print()的方法,每个方法用来完成一种不同于其他方法的具体打印操作,处理一种具体的打印情况,这些同名print()方法的关系就是重载关系。
publicvoidprint(inti)
publicvoidprint(floatf)
publicvoidprint()
当一个重载方法被调用时,Java用参数的类型、数量、参数的顺序来表明实际调用的重载方法的版本。
因此,每个重载方法的参数的类型、数量或者参数的顺序至少有一个是不同的,但不能通过方法的返回值类型来定义重载的方法。
【例4_7】方法重载的示例。
classExam4_7{
inti=5;
Strings="hello";
voidprint(){
System.out.println("i="+i+"s="+s);
}
voidprint(inti){
System.out.println("i="+i);
}
voidprint(Strings){
System.out.println("s="+s);
}
voidprint(inti,Strings){
System.out.println("i="+i+"s="+s);
}
voidprint(Strings,inti){
System.out.println("s="+s+"i="+i);
}
/*intprint(inti){
returni;
}*/
publicstaticvoidmain(String[]args){
Exam4_7e=newExam4_7();
e.print();
e.print(10);
e.print("java");
e.print(10,"java");
e.print("java",10);
}
}
程序中一共定义了5个print()方法,这5个print()方法要么参数的个数不一样,要么参数的类型不一样,要么参数的顺序不一样,这5个方法间构成方法的重载。
但是被注释掉的intprint(inti)方法由于只是返回值类型与voidprint(inti)方法不一样,不具备构成重载的条件,不能够构成重载,因此编译时会出错,提示方法已经被定义,读者可自行测试。
程序的运行结果如图4—8所示。
图4-8例4_7程序运行结果
4.4.2构造函数的重载
构造函数间也可以构成重载。
不同对象的创建很多时候就是通过构造函数的重载来实现的。
并且在构造函数重载中我们还可以使用this关键字来指明重载的构造函数。
【例4_8】构造函数的重载示例。
classPerson{
Stringname;
intage;
Person(){
name="noname";
age=1;
}
Person(Stringname){
this();
this.name=name;
}
Person(intage){
this();
this.age=age;
}
Person(Stringname,intage){
this.name=name;
this.age=age;
}
voidprint(){
System.out.println("我的名字是"+name+",年龄是"+age);
}
}
classExam4_8{
publicstaticvoidmain(String[]args){
Personp1=newPerson();
p1.print();
Personp2=newPerson("小明");
p2.print();
Personp3=newPerson(10);
p3.print();
Personp4=newPerson("小芳",11);
p4.print();
}
}
程序中创建了四个构造函数,四个构造函数构成重载,通过不同的构造函数可以创建出不同的对象,并且使用了this关键字来调用了已经存在的构造函数,程序的运行结果如图4-9所示。
图4-9例4_8程序运行结果
注意在使用this关键字来重载构造函数时,this必须是构造函数的第一个语句,且一个构造函数中只能出现一次。
类似的可以在构造函数中使用super关键字来调用父类的构造函数。
4.4.3多态
面向对象编程的第三大特征称为多态,是指一个方法声明的多个不同表现形式。
一个方法可以用不同的方式来解释,多态通常被认为是一种方法在不同的类中可以有不同的实现,甚至在同一类中仍可能有不同的定义及实现。
比如前面讲过的子类对父类方法的覆盖以及同一个类中方法的重载,这都是多态的表现形式。
多态是指一个方法声明的多种实现状态。
无论是方法重载还是方法覆盖,都要求其方法的声明要一致,不一样的是在具体实现方法时,方法体的内容不一样,方法在调用时,必须通过传入的参数的不同或者具体对象的不同,来确定究竟是调用方法的那一种实现形式。
方法收到消息时,对象要予以响应,不同的对象收到同一消息可以产生完全不同的结果,一个名字有多个不同的实现,以实现不同的功能,一名多用,方便名称空间的简化和记忆,方便代码的抽象编程,这正是多态存在的意义。
4.5抽象类和最终类
4.4.1抽象类
类是对对象的抽象,有时我们需要对类进行抽象,比如有些类具有共同的特性和功能,我们可以把这些共同的东西抽象出来组织成一个类,让其他类继承这个类,这样就可以简化代码的设计了。
有些时候这些具有相同功能的类可能根本不相关,功能的具体实现也有很大差别,做普通类的继承不能达到我们的要求,此时就需要一种跟高级别的抽象,在Java中使用抽象类来实现这种抽象。
举个例子而言,假设“鸟”是一个类,它代表了所有鸟的共同属性及其动作,任何一只具体的鸟儿都同时是由“鸟”经过特殊化形成的某个子类的对象,比如它可以派生出“鸽子”、“燕子”、“麻雀”、“天鹅”等具体的鸟类。
但是现实中并不会存在一只实实在在的鸟,它既不是鸽子,也不是燕子或麻雀,更不是天鹅,这只鸟仅仅是一只抽象的“鸟”,这就是抽象类的概念。
有了“鸟”这个抽象类,在描述和处理某一种具体的鸟时,就只需要简单地描述出它与其他鸟类所不同的特殊之处,而不必再重复它与其它鸟类相同的特点。
比如可以这样描述“燕子”这种鸟——“燕子是一种长着剪刀似的尾巴,喜在屋檐下筑窝的鸟”。
这种组织方式使得所有的概念层次分明,描述方便简单,符合人们的思维习惯。
Java中定义抽象类是出于相同的考虑。
由于抽象类是它的所有子类的公共属性的集合,所以使用抽象类的一大优点就是可以充分利用这些公共属性来提高开发和维护程序的效率。
在Java中,凡是用abstract修饰符修饰的类称为抽象类。
它和一般的类不同之处在于:
①如果一个类中含有未实现的抽象方法,那么这个类就必须通过关键字abstract进行标记声明为抽象类。
②抽象类中可以包含抽象方法,但不是一定要包含抽象方法。
它也可以包含非抽象方法和域变量,就像一般类一样。
③抽象类是没有具体对象的概念类,也就是说抽象类不能实例化为对象。
④抽象类的子类必须为父类中的所有抽象方法提供实现,否则它们也是抽象类。
定义一个抽象类的格式如下:
abstractclassClassName
{
.......//类的主体部分
}
抽象方法是指使用abstract关键字修饰,没有方法体的方法,其格式为:
[修饰符]abstract返回值类型方法名(参数列表);
注意抽象方法是没有方法体的,甚至连方法体的括号也没有。
【例4_9】抽象类示例。
abstractclassbird{
abstractvoidfly();
}
classswallowextendsbird{
voidfly(){
System.out.println("燕子在飞翔!
");
}
}
classeagleextendsbird{
voidfly(){
System.out.println("老鹰在滑翔!
");
}
}
classExam4_9{
publicstaticvoidmain(String[]args){
newswallow().fly();
neweagle().fly();
}
}
程序的运行结果如图4-10所示。
图4-10例4_9程序运行结果
4.4.2最终类
如果一个类被final修饰符所修饰和限定,说明这个类不可能有子类,这样的类就称为最终类。
最终类不能被别的类继承,它的方法也不能被覆盖。
被定义为final的类通常是一些有固定作用、用来完成某种标准功能的类。
例如最常用的System类就是final类。
将一个类定义成final类,使得这个类不能再派生子类,这样其中的方法也就不能被覆盖,避免了这个类被外界修改,增强了程序的健壮性、稳定性。
注意abstract和final修饰符不能同时修饰一个类,因为abstract类自身没有具体对象,需要派生出子类后再创建子类的对象;而final类不可能有子类,这两个修饰符恰好是矛盾的,所以abstract和final修饰符不能同时修饰一个类。
4.6接口
4.6.1接口的定义
Java中的接口是对类的进一步抽象,是一种比抽象类更高一层次的抽象。
有时一个类中被抽象的只剩下了抽象方法和一些常量,此时我们可以把这个类声明为一个接口。
可以说接口是一个完全抽象类。
接口中只能定义常量和抽象方法,并且他们默