深入解析Java的多态性及应用研究

来源:互联网 发布:网络销售周总结范文 编辑:程序博客网 时间:2024/06/16 12:38


“polymorphism(多态)”一词来自希腊语,意为“多种形式”。多态在面向对象语言中是个很普遍的概念,同时也是对象开发软件的一个特殊特性,指的是一个程序中同名的不同方法共存的情况。Java语言支持两种类型的多态性:运行时的多态性和编译时的多态性。运行时的特性(动态多态性)是指Java中的动态多态性实现手段---覆盖(替换)基类中的同名成员函数(函数原型一致),其调用规则是依据对象在实例化时而非定义时的类型相应地调用对应类中的同名成员函数。编译时的特性(静态多态性)是指Java中的静态多态性实现手段-----重载函数,其调用规则是依据对象在定义时的类型相应地调用对应类中的重载函数。Java多态性的主要表现形式有:继承多态、抽象多态和接口多态。

1 继承实现的多态

在Java中,当一个类获取另一个类中所有非私有的数据和操作的定义作为自己的部分或全部成分时,就称这两个类之间具有「继承」关系。「继承」可分为「介面继承」和「实作继承」两类,「介面继承」就是只继承父类别的函数名称,然后子类别一定会实作取代之。所以当我们以父类别的指标「多型」于各子类别时,由于子类别一定会实作父类别的多型函数,所以每个子类别的实作都不一样,此时我们(使用父类别指标的人)并不知道此多型函数到底怎么完成,因之称为「黑箱设计」。而「实作继承」就是继承父类别的函数名称,子类别在实作时,也会用到父类别的函数实作。所以我们(使用父类别指标的人)知道此多型函数怎么完成工作,因为大概也跟父类别的函数实作差不多,因之称为「白箱设计」。

Java的类别继承为实作继承,子类别会用到父类别的实作(更正确地说应该是父类别有定义实作,所以子类别可能会使用到,即使不使用到也会遵循父类别实作的演算法),所以父类别与子类别有一定程度的相关性;Java的interface接口则是介面继承,因为接口只能定义函数名称,无法定义函数实作,所以子类别必须用「implements」关键字来实现继承,且每个继承同一介面的子类别当然彼此不知道对方如何实作,因此为一个黑箱设计。

1.1方法的覆盖

根据实作继承及动态多态性的特点,派生类(子类)将继承基类(父类)所有的方法、属性和事件。同时,我们可根据需要来重新定义基类的方法、属性和事件,甚至增加或者修改部分内容,以提供不同形式的实现。

代码示例一:

  1. //定义父类superc   
  2. import java.io.*;   
  3. class superc   
  4. {public void sc()   
  5.     { System.out.println("This is superc!");   
  6.  }}   
  7. //定义子类subc1   
  8. class subc1 extends superc   
  9. {public void sc()   
  10.  { System.out.println("This is subc1!!");   
  11.  }}   
  12. //定义子类subc2   
  13. class subc2 extends superc   
  14. public void sc()   
  15.  { System.out.println("This is subc2!!");   
  16.  }}   
  17. class Test   
  18. {public static void main(String[] arg)   
  19. {superc a;   
  20. subc1 b=new subc1();   
  21. subc2 c=new subc2();   
  22. a=b;   
  23. a.sc();   
  24. a=c;   
  25. a.sc();   
  26. }}   

程序运行结果为:

如上例所示,在父类superc中我们定义了方法SC(),其每一个子类都将继承这个方法。但是,这个方法在每个子类中的具体实现是各不相同的。

那么,Java编译器如何实现对同名方法函数的调用呢?面向对象程序开发中,我们将一个方法调用同一个方法主体连接到一起称为“绑定”(Binding)。Java中绑定的所有方法都采用后期绑定技术,即动态绑定:它意味着绑定在运行期间进行,以对象的类型为基础。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息,并可通过这些区别信息实现多态。由于动态绑定技术的支持,Java的程序在执行时灵活性就大大提高了。Java 的这种机制遵循如下原则:其一,当超类(父类)对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类(父类)中定义过的,也就是说被子类覆盖的方法。其二,每一个实例对象都自带一个虚拟函数表(virtualtable),这个表中存储的是指向虚函数的指针,实例对象通过这个表来调用虚函数,以实现多态。实际上,使用虚拟函数表的方法,表项在编译时便已固定,把函数映射为在虚拟函数表中的偏移,到了运行时,只知道“偏移、执行”,至于究竟是哪个函数,无从知晓。类似于查表的过程,在编译的时候一定是存在的,但不存在于运行时。对程序而言,从源码到运行是一个完整的过程,一些功能被某些语言放到了编译时,而在另一些语言中被放到了运行时,折衷的原则取决于语言设计。虚拟函数表的实现中,每个类的表中,不仅仅要保持自己的定义的方法,还要保持自己超类的方法。我们知道,在面向对象的语言中,子类对象常常要当作超类对象使用,而在运行时,要找某个方法,只知“偏移”,所以,子类的虚拟函数表必须完全兼容超类的虚拟函数表,才能保证整个系统的正常运行,而保证的方法就是保存超类的所有表项。这样带来的问题是,当子类增多,虚拟函数表就无可避免的会增多,即便子类只有一个属于自己的方法,但它仍要带有超类所有的方法,这是一个巨大的负担。所以,那些建议“不要定义太庞杂的继承系统”的说法,是有一定物理基础的。

1.2函数的重载

重载是同一类中定义同名方法的情况。这些方法同名的原因,是它们的最终功能和目的都相同,但是由于在完成同一功能时,可能遇到不同的具体情况,所以需要定义含不同的具体内容的方法,来代表多种具体实现形式。

Java支持用户定义的函数重载。一个类中可以有相同名字的方法,这些方法可以有不同的意义。但是,这些重载的方法中,必须满足参数数目不同,相同位置上的参数类型不同等等。这些不同可以帮助编译器区分不同版本的方法;根据静态多态性调用规则,编译器依据对象在定义时的类型相应地调用对应类中的重载函数。

构造函数的多态性就是典型函数重载情况。

代码示例二:

  1.  import java.io.*;   
  2. class Gz   
  3. {//第一种构造函数   
  4. publicGz(){System.out.println("这个构造函数的参数是:空");   
  5.     }   
  6. publicGz(int s)   
  7. {//第二种构造函数   
  8. System.out.println("这个构造函数的参数是:整数");   
  9. }   
  10. publicGz(char m)   
  11. {//第三种构造函数   
  12. System.out.println("这个构造函数的参数是:字符型");   
  13. }   
  14. public static void main(String args[])   
  15. {//三个Gz类的对象实例   
  16. Gz aa=new Gz();   
  17. Gz bb=new Gz(2);   
  18. Gz cc=new Gz('a');   
  19. }}   

运行结果:

上面的例子中,我们定义了若干个构造函数Gz(),当用户创建该类对象的语句时,编译器会自动根据给出的实际参数的数目、类型和顺序来确定调用哪个构造函数来完成对新对象的初始化工作。

同样地,子类也可以根据实际情况对其父类的构造函数进行覆盖,有异曲同工的效果。但应注意:子类如果有多个构造函数的时候,父类要么没有构造成函数,让编译器自动产生,那么在执行子类构造函数之前先执行编译器自动产生的父类缺省的构造函数;要么至少要有一个显式的缺省构造函数可以让子类的构造函数调用。

2 抽象类实现的多态

在很多Java程序应用中,类层次的顶层类并不具备下层类的一些功能和方法。我们可以在超类中将这些方法声明为没有实现的抽象方法,如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract即「抽象类」。使用abstract类型修饰符的抽象方法,属于一种不完整的方法,只含有一个声明,没有方法主体,基类不能实现它,必须由派生类过载实现,为其它子孙类用抽象机制实现多态性提供了统一的界面。对所有与基础类声明的签名相符的衍生类方法,都可以通过上面介绍过的动态绑定机制进行调用,该类未实现的方法由派生类提供,已实现的成员仍可被重写,并且派生类仍可以是抽象类或实现附加接口等功能。

代码示例三:

  1. import java.util.*;   
  2. abstract class Animal {   
  3. //int i; // storage allocated for each   
  4. public abstract void Breath();   
  5. public String what() { return "Animal";}   
  6. public abstract void adjust();}   
  7. class Human extends Animal {   
  8. public void Breath() { System.out.println("Human is breathing.");}   
  9. public String what() { return "Human"; }   
  10. public void adjust() {}}   
  11. class Dog extends Animal {   
  12. public void Breath() { System.out.println("Dog is breathing.");}   
  13. public String what() { return "Dog"; }   
  14. public void adjust() {}}   
  15. class Bird extends Animal {   
  16. public void Breath() { System.out.println("Bird is breathing.");}   
  17. public String what() { return "Bird"; }   
  18. public void adjust() {}}   
  19. class Woman extends Human {   
  20. public void Breath() { System.out.println("Woman is breathing.");}   
  21. public void adjust() { System.out.println("I am woman.");}}   
  22. class Man extends Human {   
  23. public void play() { System.out.println("Man is breathing.");}   
  24.     public String what() { return "Man"; }}   
  25. public class cxhs {// Doesn't care about type, so new types   
  26. // added to the system still work right:   
  27. static void tune(Animal i) {// ...   
  28. i.Breath();}   
  29. static void tuneAll(Animal[] e) {   
  30. for(int i = 0; i < e.length; i++)   
  31. tune(e[i]); }   
  32. public static void main(String[] args) {   
  33. Animal[] orchestra = new Animal[5];   
  34. int i = 0;   
  35. // Upcasting during addition to the array:   
  36. orchestra[i++] = new Human();   
  37. orchestra[i++] = new Dog();   
  38. orchestra[i++] = new Bird();   
  39. orchestra[i++] = new Woman();   
  40. orchestra[i++] = new Man();   
  41. tuneAll(orchestra); }}   

运行结果:

其逻辑结构如下图:

由于抽象类是其所有子类的公共属性和方法的集合,它可以在类的某些属性和方法中提供不变的因素和功能,同时大大提高了类的其他过程的灵活性。从上面的例子可以看出,除基础类以外,实际并没有进行什么改变。创建抽象类和方法有时对我们非常有用,因为它们使一个类的抽象变成明显的事实,可明确告诉用户和编译器自己打算如何用它。

3 接口实现的多态

以上所谈到的多态行为均用到了类的继承关系所建立起来的子类型关系。Java接口同样支持用户定义的类型,可以实现类型的「界面继承」;并且Java的接口机制启动了建立在类型层次结构上的多态行为,能够实现接口的组合和扩充,一定程度上对Java类型及其功能进行了优化。Java中一个类只能有一个父类,但是单个类可以实现一个或多个接口,多个类可实现相同的“接口”。

“interface”(接口)关键字使接口的抽象概念更深入了一层,我们可将其想象为一个“纯”抽象类。接口常常被用来为具有相似功能的一组类,对外提供一致的服务接口,这一组类可以是相关的,也可以是不相关的,而抽象类则是主为一组相关的类提供一致的服务接口。所以说接口往往比抽象类具有更大的灵活性,它允许创建者规定一个类的基本形式、方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static和final。接口只提供一种形式,并不提供实施的细节,这也为其本身及子类提供了较广泛的空间。

如例三中把Animal定义为一个接口:

  1. interface Animal1 {   
  2. // Compile-time constant:   
  3. int i = 5// static & final   
  4. // Cannot have method definitions:   
  5. void Breath(); // Automatically public   
  6. String what();   
  7. void adjust();   
  8. }   

其中的三个方法函数均没定义方法体;换言之,接口可以看成只定义了API的协议规范,相当于C++中的只含有纯虚抽象类。其子类Human、Dog、Bird继承时,必须使用关键字implements或extends从接口实现或继承。由于接口中只有方法原形,实现接口时无成员变量名字冲突问题,也没有对父类方法的重定义问题,也不存在重复继承问题,比一般类的多态度继承简单。接口继承形成的层次独立于类层次,因此允许不同层次中的类实现同一接口,这些实现接口的类支持公共的行为,但实现这些行为的方法可以不同,无须共享任何实现方式,呈现多样化。同时,这样的多态行为使Java的接口的功能的重大意义显得很明显。通过接口,每个类都可以自由决定其实现的细节;利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。这样,实现某一接口的多个类或接口可以以不同的方式实现相同的接口,而每个类或接口仍支持同一组方法,当实例化这些类或实现接口后,就弥补了Java中“一个子类,只能有一个父类”的不足,实现了多态性的“一个接口,多种方法”。

4 结束语

“多态性”意味着“不同的形式”,是建立对象模型的有力工具。为充分使用多态性乃至面向对象的技术,我们必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。本文对Java中的多态性及其实现原理进行了深入地解析,目的在于希望学习和使用Java语言的程序设计人员,能更好地掌握开发Java程序。

原创粉丝点击