天天看点

第六讲 面向对象之四__重载与多态

第六讲 面向对象之四__重载与多态
课题名称 第六讲 面向对象之四____重载与多态
教学提示 本讲介绍Java程序实现多态的主要手段———重载。结合继承和多态,还介绍了构造函数的继承与重载,以及接口的实现。
教学目的 1. 掌握在Java中覆盖的实现方式 2. 掌握在Java中重载的实现方式 3. 掌握抽象类的概念4. 理解接口的概念,掌握它在Java中的实现
重点 1.覆盖的概念及实现 2. 重载的实现 3. 接口的概念与实现 难点 1. 重载 2. 接口的实现
教学方法 案例教学法,讲授教学法 教学手段 幻灯片与投影示例,电子课件

教学内容

第六讲 面向对象之四____重载与多态

6.1 类成员的覆盖

在面向对象程序设计中,覆盖是实现多态的一种常见方式。通过覆盖,我们可以在一个子类中,将父类继承下来的类成员重新进行定义,满足程序设计的需要。

6.1.1 覆盖的用法

在程序的设计过程中,我们通过继承可以快速地将父类中已实现的非私有类成员应用到自己定义的子类中。但是,不是所有继承下来的类成员都是我们需要的,这时候我们就可以通过使用覆盖的方式来解决这个问题。

子类对继承自父类的类成员重新进行定义,就称为覆盖,它是一种很重要的多态形式。要进行覆盖,就是在子类中对需要覆盖的类成员以父类中相同的格式,再重新声明定义一次,这样就可以对继承下来的类成员进行功能的重新实现,从而达到程序设计的要求。

【例6.1 】

public class MethodOverride

{

public static void main(String [] args)

{

SubClass s=new SubClass();

s.fun();

}

}

class SuperClass

{

int i=1;

public void fun()

{

System.out.println("Super");

System.out.println("父类的i=" + i);

}

}

class SubClass extends SuperClass

{

int i=2;

public void fun()

{

System.out.println("Sub");

System.out.println("子类的i=" + i);

super.fun();

}

}

运行结果如图6.1。

第六讲 面向对象之四__重载与多态

图6.1 例题6.1的运行结果

从上面的例子中我们可以看到:由于在子类中,对父类中继承下来的域变量和方法都重新定义了一遍,子类对象中将父类中的原有的类成员覆盖了。所以程序执行后的结果都是调用了子类对象中新定义的子类成员而产生的。但是覆盖父类的类成员,并不等于它们就不能使用了,甚至不存在了。它们只不过是在子类对象中不能直接使用罢了。

需要注意的是在进行覆盖时,子类中所定义的格式必须和父类中的一样,否则父类的类成员就没有被覆盖。

6.1.2 使用被覆盖的成员

对于被覆盖的父类成员,我们要如何才能对它们操作呢?接下来我们介绍这方面的相关知识。

1.域变量的隐藏

子类重新定义一个与从父类那里继承来的属性变量完全相同的变量,称为域变量的隐藏。对于域变量的隐藏来说,父类定义的域变量在子类对象实例化时仍然分配一个存储空间。例如,可以对例5.1进行一点修改,在子类C中增加定义一个和父类A相同的属性:public int a1,具体如例5.7。

【例6.2 】

classA

{

public int a1;

private float a2

int getA()

{

return(a1);

}

void setA(){}

}

class B extends A

{

int b1;

String b2;

String getB()

{

return(b2);

}

}

class C extends B

{

int c;

public int a1;

int printC()

{

System.out.println(c);

}

}

这样经过修改以后的程序中,C类拥有的属性为:

int a1//继承自父类A

int a1//C类自己定义的属性

int b1//继承自父类B

String b2//继承自父类B

int c

这时,子类中定义了与父类同名的属性变量,即出现了子类变量对同名父类变量的隐藏。这里所谓隐藏是指子类拥有了两个相同名字的变量int a1,一个继承自父类,另一个由自己定义。

在程序运行中,系统是如何区分处理这两个相同的域变量?当子类执行继承自父类的操作时,处理的是继承自父类的变量,而当子类执行它自己定义的方法时,所操作的就是它自己定义的变量,而把继承自父类的变量“隐藏”起来。参看例子5.8。

【例6.3 】

public class ExtendsExam

{

public static void main(String [] args)

{

subclass e=new subclass();

System.out.println(" Sup_getX()方法结果:"+e. Sup_getX());

System.out.println(" Sub_getX()方法结果:"+e.Sub_getX());

}

}

class superclass

{

public int x=10;

int Sup_getX()

{

return(x);

}

}

class subclass extends superclass

{

private int x=20;

int Sub_getX()

{

return(x);

}

}

运行结果如图6.2。

第六讲 面向对象之四__重载与多态

图6.2 例题6.3的运行结果

从例子中我们可以看到,subclass类的e对象调用父类的Sup_getX()方法时,处理的域变量为父类中定义的x,所以第一行的输出结果为10;而在调用自身定义的Sub_getX()方法时,处理的域变量是子类中定义的x,所以第二行的输出结果为20。

2.方法的覆盖

正像子类可以定义与父类同名的域,实现对父类域变量的隐藏一样;子类也可以重新定义与父类同名的方法,实现时父类方法的覆盖(Overload)。方法的覆盖与域的隐藏的不同之处在于:子类隐藏父类的域只是使之不可见,父类的同名域在子类对象中仍然占有自己的独立内存空间;而子类方法对父类同名方法的覆盖将清除父类方法占用的内存空间,从而使父类方法在子类对象中不复存在。

方法的覆盖中需要注意的问题是:子类在重新定义父类已有的方法时,应保持与父类完全相同的方法头声明,即应与父类有完全相同的方法名、返回值和参数列表。否则就不是方法的覆盖,而是子类定义自己的与父类无关的方法,父类的方法未被覆盖,所以仍然存在。

在覆盖多态中,由于同名的不同方法是存在于不同的类中的,所以需在调用方法时指明调用的是哪个类的方法,就可以很容易地把它们区分开来。下面我们看例子5.9。

【例6.4 】

class superClass{

void superPrint(){

System.out.println("This is superClass!");

}

}

class subClass extends superClass{

void superPrint(){

System.out.println("This is subClass!");

}

}

public class myInherit{

public static void main(String args[]){

subClass subObject = new subClass();

subObject.superPrint();//子类对象调用子类的方法

superClass superObject = new superClass();

superObject.superPrint();//父类对象调用父类的方法

}

}

运行结果如图6.3。

第六讲 面向对象之四__重载与多态
图6.3 例题6.4的运行结果

3.super参考

相对this来说,super表示的是当前类的直接父类对象,是当前对象的直接父类对象的引用。所谓直接父类是相对于当前类的其他“祖先”类而言的。例如,假设类A派生出子类B,B类又派生出自己的子类C,则B是C的直接父类,而A是C的祖先类。 super代表的就是直接父类。这就使得我们可以比较简便、直观地在子类中引用直接父类中的相应属性或方法。我们对例子5.4稍作修改,就会有不同的结果。具体看如下例子5.10:

【例6.5 】

public class ExtendsExam

{

public static void main(String [] args)

{

subclass e=new subclass();

e.Sub_printX();

}

}

class superclass

{

public int x=10;

int Sup_getX()

{

return(x);

}

void Sup_printX()

{

System.out.println(“Sup_printX()方法结果:”+ Sup_getX());

}

}

class subclass extends superclass

{

private int x=20;

int Sub_getX()

{

return(super.x);//修改部分

}

void Sub_printX()

{

System.out.println(" Sub_getX()方法结果:"+Sup_getX());

super. Sup_printX();

}

}

运行的结果如图5.8所示。

第六讲 面向对象之四__重载与多态

图6.4 例题6.5的运行结果

从例子中我们可以看到,subclass类的e对象调用父类的Sup_getX()方法时,处理的域变量为父类中定义的x,所以第一行的输出结果为10;而在调用自身定义的Sub_getX()方法时,我们通过使用super指定处理的域变量x为父类中声明定义的域变量x,所以第二行的输出结果为10。

需要注意的是:this和super是属于类的有特指的域,只能用来代表当前对象和当前对象的父对象,而不能像其他类的属性一样随意引用。下面语句中的用法都是错误的。

public static void main(String [] args) {

subclass e=new subclass();

System.out.println(" Sup_getX()方法结果:"+e.super.Sup_getX());//错误

System.out.println(" Sub_getX()方法结果:"+e.this.Sub_getX());//错误

}

除了用来指代当前对象或父类对象的引用外,this和super还有一个重要的用法,就是调用当前对象或父类对象的构造函数。

6.2 方法重载

方法的重载是实现多态技术的重要手段。与方法的覆盖不同,它不是子类对父类同名方法的重新定义,而是一个类中对自身已有的同名方法的重新定义。

6.2.1 方法的重载

在Java 中,同一个类中的两个或两个以上的方法可以有同一个名字,只要它们的参数声明不同即可。在这种情况下,该方法就被称为重载(overloaded ),这个过程称为方法重载(method overloading )。方法重载是Java 实现多态性的一种方式。如果你以前从来没有使用过一种允许方法重载的语言,这个概念最初可能有点奇怪。这些方法同名的原因,是因为它们的最终功能和目的都相同,但是由于在完成同一功能时,可能遇到不同的具体情况,所以需要定义含不同的具体内容的方法,来代表多种具体实现形式。例如,一个类需要具有打印的功能,而打印是一个很广泛的概念,对应的具体情况和操作有多种,如实数打印、整数打印、字符打印、分行打印等。为了使打印功能完整,在这个类中就可以定义若干个名字都叫myprint的方法,每个方法用来完成一种不同于其他方法的具体打印操作,处理一种具体的打印情况。方法重载可如下所示使用:

public void myprint (int i)

public void myprint (float f)

public void myprint ()

当一个重载方法被调用时,Java 用参数的类型和(或)数量来表明实际调用的重载方法的版本。因此,每个重载方法的参数的类型和(或)数量必须是不同的。虽然每个重载方法可以有不同的返回类型,但返回类型并不足以区分所使用的是哪个方法。当Java 调用一个重载方法时,参数与调用参数匹配的方法被执行。

当需要调用这些方法中的一种方法时,根据提供的参数的类型选择合适的一种方法。有两个规则适用于重载方法:

  • ① 调用语句的参数表必须有足够的不同,以至于允许区分出正确的方法被调用。正常的拓展晋升(如,单精度类型float到双精度类型double)可能被应用,但是这样会导致在某些条件下的混淆。
  • ② 方法的返回类型可以各不相同,但它不足以使返回类型变成唯一的差异。重载方法的参数表必须不同。

【例6.6 】

public class OverloadExam

{

public static void print(String str)

{

System.out.println("String="+str);

}

public static void print(int i)

{

System.out.println("int="+i);

}

public static void main(String [] args)

{

print("123");

print(123);

}

}

运行结果如图5.9。

第六讲 面向对象之四__重载与多态

图6.5 例题6.6的运行结果

当找不到所需要的类型时,按照“提升顺序表”寻找最合适的方法。

【例6.7 】

public class OverloadExam{

public static void print(char c){

System.out.println("char="+c);

}

public static void print(short i){

System.out.println("short="+i);

}

public static void main(String [] args){

byte b=1;

print(b);

}

}

运行结果如图5.10。

第六讲 面向对象之四__重载与多态
图6.6 例题6.7的运行结果

6.2.2 构造函数的重载

构造函数又称构造器。一个类可以有多个构造器,构造器可以重载,如果没有提供构造器,系统为你提供一个空的、没有参数的默认构造器,一旦用户自己定义了构造器,系统就不再提供默认构造器。

【例6.8 】

class Xyz

{

// member variables

public Xyz()

{

// 无参数的构造函数

……

}

public Xyz(int x)

{

//整形参数的构造函数

……

}

}

注意由于采用了重载方法,因此可以通过为几个构造函数提供不同的参数表的办法来重载构造函数。当发出new Xyz(argument_list)调用的时候,传递到new语句中的参数表决定采用哪个构造函数。

如果有一个类带有几个构造函数,那么可以通过使用关键字this作为一个方法调用,将一个构造函数全部功能复制到另一个构造函数中,实现部分功能,避免重复编写代码。具体看例子5.14:

【例6.9 】

public class Employee {

private String name;

private int salary;

public Employee(String n, int s) {

name = n;

salary = s;

}

public Employee(String n){

this(n, 0);

}

public Employee(){

this( " Unknown " );

}

}

在第二个构造函数中,有一个字符串参数,调用this(n,0)将控制权传递到构造函数的另一个版本,即采用了一个String参数和一个int参数的构造函数中。在第三个构造函数中,它没有参数,调用this(“Unknownn”)将控制权传递到构造函数的另一个版本,即采用了一个String参数的构造函数中。注意对于this的任何调用,如果出现,在任何构造函数中必须是第一个语句。

6.3 抽象类和最终类

6.3.1 抽象类

假设“鸟”是一个类,它可以派生出若干个子类如“鸽子”、“燕子”、“麻雀”、“天鹅”等,那么是否存在一只实实在在的鸟,它既不是鸽子,也不是燕子或麻雀,更不是天鹅,它不是任何一种具体种类的鸟,而仅仅是一只抽象的“鸟”呢?答案很明显,没有。 “鸟”仅仅作为一个抽象的概念存在着,它代表了所有鸟的共同属性,任何一只具体的鸟儿都同时是由“鸟”经过特殊化形成的某个子类的对象。这样的类就是 Java中的abstract类。

既然抽象类没有具体的对象,定义它又有什么作用呢?仍然以“鸟”的概念为例:假设需要向别人描述 “天鹅”是什么,通常都会这样说:“天鹅是一种脖子长长,姿态优美的候鸟”;若是描述“燕子”,可能会说:“燕子是一种长着剪刀似的尾巴,喜在屋檐下筑窝的鸟”;可见定义是建筑在假设对方已经知道了什么是“鸟”的前提之上,只有在被进一步问及“鸟”是什么时,才会具体解释说:“鸟是一种长着翅膀和羽毛的卵生动物”,而不会在一开始就把“天鹅”描述成“是一种脖子长长,姿态优美,长着翅膀和羽毛的卵生动物”。这实际是一种经过优化了的概念组织方式:把所有鸟的共同特点抽象出来,概括形成“鸟”的概念;其后在描述和处理某一种具体的鸟时,就只需要简单地描述出它与其他鸟类所不同的特殊之处,而不必再重复它与其它鸟类相同的特点。这种组织方式使得所有的概念层次分明,非常符合人们的思维习惯。

Java中定义抽象类是出于相同的考虑。由于抽象类是它的所有子类的公共属性的集合,所以使用抽象类的一大优点就是可以充分利用这些公共属性来提高开发和维护程序的效率。

在Java中,凡是用abstract修饰符修饰的类称为抽象类。它和一般的类不同之处在于:

① 如果一个类中含有未实现的抽象方法,那么这个类就必须通过关键字abstract进行标记声明为抽象类。

② 抽象类中可以包含抽象方法,但不是一定要包含抽象方法。它也可以包含非抽象方法和域变量,就像一般类一样。

③ 抽象类是没有具体对象的概念类,也就是说抽象类不能实例化为对象。

④ 抽象类必须被继承。子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类。

定义一个抽象类的格式如下:

abstract class ClassName

{

.......//类的主体部分

}

下面我们通过例子,学习抽象方法的使用。

【例6.10 】

abstract class fatherClass

{

abstract void abstractMethod();

void printMethod()

{

System.out.println("fatherClass function! ");

}

}

class childClass extends fatherClass

{

void abstractMethod()

{

System.out.println("childClass function! ");

}

}

public class mainClass

{

public static void main(String args[])

{

childClass obj=new childClass();

obj. printMethod();

obj.abstractMethod();

}

}

运行结果如图6.10。

第六讲 面向对象之四__重载与多态

图6.7 例题6.10 的运行结果

在上面的程序中,首先定义了一个抽象类fatherClass,在这个抽象类中,声明一个抽象方法abstractMethod()和一个非抽象方法printMethod(),接着定义了fatherClass的子类childClass,在 childClass中重写了abstractMethod()方法,随后,在主类mainClass中生成类childClass的一个实例,并将该实例引用返回到fatherClass类变量obj中。

6.3.2 最终类

如果一个类被final修饰符所修饰和限定,说明这个类不可能有子类,这样的类就称为最终类。最终类不能被别的类继承,它的方法也不能被覆盖。被定义为final的类通常是一些有固定作用、用来完成某种标准功能的类。如Java系统定义好的用来实现网络功能的InetAddress、Socket等类都是final类。在Java程序中,当通过类名引用一个类或其对象时,实际真正引用的既可能是这个类或其对象本身,也可能是这个类的某个子类及子类的对象,即具有一定的不确定性。将一个类定义为final则可以将它的内容、属性和功能固定下来,与它的类名形成稳定的映射关系,从而保证引用这个类时所实现的功能的正确无误。

注意abstract和final修饰符不能同时修饰一个类,因为abstract类自身没有具体对象,需要派生出子类后再创建子类的对象;而final类不可能有子类,这样abstractfinal类就无法使用,也就没有意义了。

6.4 接口

Java为了避免C++中随着多重继承所衍生的问题,因而限定类的继承只支持单重继承。但是实际中的较为复杂问题的解决有需要用到多重继承,因此,Java通过接口来实现类间多重继承的功能。

6.4.1 接口的定义

Java中的接口就是定义了若干个抽象方法和常量,形成的一个属性集合,该属性集合通常对应了某一组功能,其主要作用是可以实现类似于类的多重继承的功能。接口中的域变量都是常量,方法都是没有方法体的抽象方法,所以,接口定义的仅仅是实现某一特定功能的一组对外的规范,而并没有真正的实现这个功能。这个功能的真正实现是在“继承”这个接口的各个类中完成的,要由这些类来具体定义接口中各抽象方法的方法体。因而在Java中,通常把对接口功能的“继承”称为“实现”。

Java中声明接口的语法如下:

[public] interface接口名[extends父接口名列表]

{//接口体

//常量域声明

[public] [static] [final] 域类型域名=常量值;

……

//抽象方法声明

[public] [abstract] [native] 返回值方法名(参数列表)[throw 异常列表];

……

}

从上面的语法规定可以看出,定义接口与定义类非常相似。实际上完全可以把接口理解成为由常量和抽象方法组成的特殊类。一个类只能有一个父类,但是类可以同时实现若干个接口。这种情况下如果把接口理解成特殊的类,那么这个类利用接口实际上就获得了多个父类,即实现了多重继承。

就像class是声明类的关键字一样,interface是接口声明的关键字,它引导着所定义的接口的名字,这个名字应该符合Java对标识符的规定。与类定义相仿,声明接口时也需要给出访问控制符,不同的是接口的访问控制符只有public一个。用public修饰的接口是公共接口,可以被所有的类和接口使用,而没有public修饰符的接口则只能被同一个包中的其他类和接口利用。与类相仿,接口也具有继承性。定义一个接口时可以通过extends关键字声明该新接口是某个已经存在的父接口的派生接口,它将继承父接口的所有属性和方法。与类的继承不同的是一个接口可以有一个以上父接口,它们之间用逗号分隔,形成父接口列表。新接口将继承所有父接口中的属性和方法。

接口体的声明是定义接口的重要部分。接口体由两个部分组成:一部分是对接口中域变量的声明,另一部分是对接口中方法的声明。接口中的所有域变量都必须是public static final,这是系统默认的规定,所以接口属性也可以没有任何修饰符,其效果完全相同。接口中的所有方法都必须是默认的public abstract,无论是否有修饰符显式地限定它。在接口中只能给出这些抽象方法的方法名、返回值和参数列表,而不能定义方法体。定义接口可归纳为如下几点:

① 在Java中接口是一种专门的类型。用interface关键字定义接口。

② 接口中只能定义抽象方法,不能有方法体,一定是public修饰的。

③ 接口中可以定义变量,但实际上是static final修饰的常量。

④ 接口中不能定义静态方法。

【例6.11 】

public interface Sup_InterfaceExam

{

public static final int x;

int y;

public void z();

public abstract int getz();

}

public interface Sub_InterfaceExam extends Sup_InterfaceExam

{

public static final int a;

int b;

public void c();

public abstract int getc();

}

class MyClass implements Sub_InterfaceExam , Sup_InterfaceExam

{

public void z()

{

}

public int getz()

{

return 1;

}

public void c()

{

}

public int getc()

{

return 5;

}

}

因为接口是简单的未执行的系列以及一些抽象的方法,你可能会思考究竟接口于抽象类有什么区别。了解它们的区别是相当重要的,它们之间的区别如下:

  • ① 接口不能包含任何可以执行的方法,而抽象类可以。
  • ② 类可以实现多个接口,但只有一个父类。
  • ③ 接口不是类分级结构的一部分,而没有联系的类可以执行相同的接口。

6.4.2 接口的实现

接口的声明仅仅给出了抽象方法,相当于程序开发早期的一组协议,而具体地实现接口所规定的功能,则需某个类为接口中的抽象方法书写语句并定义实在的方法体,称为实现这个接口。如果一个类要实现一个接口,那么这个类就提供了实现定义在接口中的所有抽象方法的方法体。

一个类要实现接口时,请注意以下问题:

① 在类的声明部分,用implements关键字声明该类将要实现哪些接口。

② 如果实现某接口的类不是abstract抽象类,则在类的定义部分必须实现指定接口的所有抽象方法,即为所有抽象方法定义方法体,而且方法头部分应该与接口中的定义完全一致,即有完全相同的返回值和参数列表。

③ 如果实现某接口的的类是abstract的抽象类,则它可以不实现该接口所有的方法。但是对于这个抽象类任何一个非抽象的子类而言,它们父类所实现的接口中的所有抽象方法都必须有实在的方法体。这些方法体可以来自抽象的父类,也可以来自子类自身,但是不允许存在未被实现的接口方法。这主要体现了非抽象类中不能存在抽象方法的原则。

④ —个类在实现某接口的抽象方法时,必须使用完全相同方法头。如果所实现的方法与抽象方法有相同的方法名和不同的参数列表,则只是在重载一个新的方法,而不是实现已有的抽象方法。

⑤ 接口的抽象方法的访问限制符都已制定为public,所以类在实现方法时,必须显式地使用public修饰符,否则将被系统警告为缩小了接口中定义的方法的访问控制范围。

【例6.12 】

interface A{

int a=1;

}

interface B{

int b=2;

public abstract void pp();

}

interface MyInterface extends A,B{}//接口的继承

abstract class AbstractInterfaceExam implements A,B {} //抽象类实现接口

public class InterfaceExam implements A,B//一般类实现接口

{

static InterfaceExam obj = new InterfaceExam();

public static void main(String [] args){

System.out.println("继承接口A中的a=" + obj.a);

obj.pp();

}

public void pp()//实现抽象方法pp()

{

System.out.println("继承接口AB中的ab=" + obj.b);

}kkkkkkkkkkkkkkkkkkkkkkkkkkkkkk

}

运行结果如图6.8。

第六讲 面向对象之四__重载与多态
图6.8 例题6.12的运行结果

小结

方法的重载是一个类中对自身已有的同名方法的重新定义。每个重载方法的参数的类型和(或)数量必须是不同的。

构造器(构造函数)也可以重载。如果没有定义构造器,系统会提供默认构造器,一旦用户自己定义了构造器,系统就不再提供默认构造器。在重载的构造函数内部,可以使用关键字this作为一个方法调用,从一个构造函数中调用另一个构造函数。

用abstract修饰符修饰的类称为抽象类,抽象类不能实例化为对象。抽象类必须被继承,子类为它们父类中的所有抽象方法提供实现,否则它们也是抽象类。

如果一个类被final修饰符所修饰,说明这个类不可能有子类,这样的类就称为最终类。最终类不能被别的类继承,它的方法也不能被覆盖。

因为abstract类自身没有具体对象,需要派生出子类后再创建子类的对象;而final类不可能有子类,因此abstract和final修饰符不能同时修饰一个类。

接口用interface来声明。接口中的域变量都是常量,方法都是没有方法体的抽象方法,其方法的真正实现在“继承”这个接口的各个类中完成。一个类只能有一个父类,但是类可以同时实现若干个接口,从而实现了多重继承。一个类要实现接口时,在类的声明部分,用implements关键字声明该类将要实现哪些接口。