您的位置 首页 java

thank in java第七章 多形性(多态)

第七章 多形性( 多态

面向对象程序设计语言三大基本特征:封装、继承、多态。

“多形性“(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,即实现了”是什么“与”怎样做“两个模块的分离。利用多形性的概念,代码的组织以及可读性君能获得改善。此外,还能创建”易于扩展“的程序。

7.1 上溯造型

在前面章节,已经了解到可将一个对象作为它自己的类型使用,或者作为它的基础类的一个对象使用。取得一个对象 句柄 ,并将其作为基础类句柄使用的行为叫作“上溯造型”——因为继承树的画法是基础类位于最上方。但这样做会遇到如下问题,示例:

package package07;
class Note {
private int value;
private Note(int val){
 value = val;
}
public static final Note
middleC = new Note(0),
cSharp = new Note(1),
cFlat = new Note(2);
}
package package07;
class Instrument {
public void play(Note n){
 System.out.println("Instrument.play()");
}
}
package package07;
class Wind extends Instrument{
public void play(Note n){
 System.out.println("Wind.play()");
}
}
package package07;
public class Music {
public static void tune(Instrument i){
 i.play(Note.middleC);
}
public static void main(String[] args) {
 Wind fluteWind = new Wind();
 tune(fluteWind);
}
}
输出:Wind.play() 

7.2 深入理解

接受继承基础类的句柄,编译器如何才能知道基础类句柄指向的是什么呢?编译器无从得知,为了深入了解这个问题,引申出“绑定”这个主题。

7.2.1 方法调用的绑定

将一个方法调用同一个方法主题连接到一起就称为“绑定(Bingding)“。若在程序执行前执行绑定,就称为”早期绑定“。但是这个术语或者从未听见,因为它在任何程序化语言里都是不可能的。

解决的办法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机智,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

Java 中绑定的所有方法都是采取后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。

关于final声明:由之前我们了解到,final声明后,能够防止其他人覆盖 此方法。但也许更重要的一点,它可有效地“关闭”动态绑定,或者告诉编译器不需要动态绑定。从而编译器就可以为final方法调用生成更高的代码。

7.2.2 产生正确的行为

知道java里绑定的所有方法都是通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正确地工作。或者换用另一种方法,我们可以“将一条消息发送给一个对象,让对象自行判断要做什么事情。”

比如: Shape (形状)是一个基础类,另外还有一些衍生类:Circle(圆形),Square(方形),Triangle(三角形)等等。其中衍生类继承基础类。上溯造型可用如下表示出来:

shape s = new Circle();

在这里,我们创建了Circle对象,并将结果句柄立即赋给一个Shape。按照继承关系这是Circle属于Shape的一种,因此编译器认可上述语句。

package package07;
class Shape {
void draw(){}
void erase(){}
}
package package07;
class Cricle extends Shape{
void draw(){
 System.out.println("Circle.draw()");
}
void erase(){
 System.out.println("Circle.erase()");
}
}
package package07;
class Square extends Shape{
void draw(){
 System.out.println("Square.draw()");
}
void erase(){
 System.out.println("Square.erase()");
}
}
package package07;
class Triangle extends Shape{
void draw(){
 System.out.println("Triangle.draw()");
}
void erase(){
 System.out.println("Triangle.erase()");
}
}
package package07;
import javafx.scene.shape.Circle;
public class Shapes {
public static Shape randShape(){
 switch ((int)(Math.random()*3)) {
 default:
 case 0:
 return new Cricle();
 case 1:
 return new Square();
 case 2:
 return new Triangle();
 }
}
public static void main(String[] args) {
 Shape[] s = new Shape[9];
 for(int i = 0;i<s.length;i++)
 s[i] = randShape();
 for(int i = 0;i<s.length;i++)
 s[i].draw();
}
}
输出:Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
Circle.draw()
Square.draw()
Square.draw()
Square.draw()
Square.draw() 

针对从Shape衍生出来的所有东西,shape建立了一个通用接口——也就是说,所有形状都可以描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的集合形状都提供了独一无二的行为。

7.3 覆盖与过载

回过头来看本章的第一个例子。在下面这个程序中,,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过载处理,使其不报告出错。但这种行为可能并不是我们希望的。下面是这个例子:

package package07;
class NoteX {
public static final int
MIDDLE_C = 0,C_SHAPE = 1,C_FLAT = 2;
}
package package07;
class InstrumentX {
public void play(int NoteX){
 System.out.println("InstrumentX.play()");
}
}
package package07;
class WindX extends InstrumentX{
public void play(NoteX n){
 System.out.println("WindX.play(NoteX n)");
}
}
package package07;
public class WindError {
public static void tune(InstrumentX i){
 i.play(NoteX.MIDDLE_C);
}
public static void main(String[] args) {
 WindX flute = new WindX();
 tune(flute);
}
}
输出:InstrumentX.play() 

“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原有的含义完全被后来的含义取代了。

7.4 抽象 类和方法

在我们所有乐器(instrument)例子中,基础类Instrument内的方法都是肯定是“伪”方法。若去调用这些方法,就会出现错误。这是由于Instrument的意图是为从它衍生出去的所有类都创建一个通用接口。之所以创建这样一个接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有衍生类里”通用“的一些东西。为阐述这个观念,另一种方法是把Instrument称为”抽象基础类“(简称抽象类)。因为Instrument的作用仅仅是用来表达接口,而不是表达一些具体的实施细节,所以创建一个Instrument对象是没有任何意义的,而且我们通常都应禁止用户那样做。为达到这一目的,可令instrument内所有方法都显示出错消息。但这样做会延迟信息到达期,并要求用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕捉到问题。

针对这个问题,java专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声明,没有主体。语法如下:

abstract void();

包含一个抽象方法的类,被称为“抽象类”。如果一个类包含了一个或多个抽象方法,类必须指定成abstract(抽象)。否则,编译器会报错。当然即使不包括任何abstract方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。

instrument类可很轻松地转换成一个抽象类。只有其中一部分方法会变成抽象方法,因为使一个类抽象以后,并不会强迫我们将它所有方法都同时变成抽象。比如:

package package07;
class Wind4 extends Instrument4{
public void play(){
 System.out.println("Wind4.play()");
}
public String what(){
 return "Wind4";
}
public void adjust(){}
}
package package07;
class Percussion4 extends Instrument4{
public void play(){
 System.out.println("Percussion4.play()");
}
public String what(){
 return "Percussion4";
}
public void adjust(){}
}
package package07;
class Stringed4 extends Instrument4{
public void play(){
 System.out.println("Stringed4,play()");
}
public String what(){
 return "Stringed";
}
public void adjust(){}
}
package package07;
class Brass4 extends Wind4{
public void play(){
 System.out.println("Brass4.play()");
}
public void adjust(){
 System.out.println("Brass4.adjust()");
}
}
package package07;
public class Music4 {
static void tune(Instrument4 i){
 i.play();
}
static void tuneAll(Instrument4[] e){
 for (int i = 0; i < e.length; i++) {
 tune(e[i]);
 }
}
public static void main(String[] args) {
 Instrument4[] orchestraInstrument4s = new Instrument4[5];
 int i = 0;
 orchestraInstrument4s[i++] = new Wind4();
 orchestraInstrument4s[i++] = new Percussion4();
 orchestraInstrument4s[i++] = new Stringed4();
 orchestraInstrument4s[i++] = new Brass4();
 orchestraInstrument4s[i++] = new Woodwind4();
 tuneAll(orchestraInstrument4s);
}
}
输出:Wind4.play()
Percussion4.play()
Stringed4,play()
Brass4.play()
Woodwind4.play() 

创建抽象类和方法有时非常有用,因为它们使得一类的抽象变成明显的事实,可明确告诉用户和编译器自己打算如何。

7.5 接口

“interface”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主题。接口也包含了基本数据类型的数据成员,但他们都默认为static和final。接口只是提供一种形式,并不提供实施的细节。接口这样描述自己:“对于实现我们的所有类,看起来都应该像是我现在的样子”。因此,采用了一个特定节后的所有代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以,几口常用于建立类于类之间的一个“协议“。有些面向对象的程序设计语言采用一个名为“protocol”(协议)的关键字,它做的便是与接口相同的事情。

接口关键字是interface, 与类像是可以与public等属性关键字联合使用。

为了生成与一个特定的接口(或一组接口)相符的类,要使用implements(实现)关键字。 例子如下:

package package07;
import java.util.*;
interface Instrument5 {
int i = 5;
void play();
String what();
void adjust();
}
package package07;
class Wind5 implements Instrument5{
public void play(){
 System.out.println("Wind.play()");
}
public String what(){ 
 return "Wind5";
}
public void adjust() {
}
}
package package07;
class Percussion5 implements Instrument5{
public void play() {
 System.out.println("Percussion5.play()");
}
public String what() {
 return "Percussion5";
}
public void adjust() {
}
}
package package07;
class Stringed5 implements Instrument5{
public void play() {
 System.out.println("Stringed.play()");
}
public String what() {
 return "Stringed";
}
public void adjust() {
}
}
package package07;
class Brass5 extends Wind5{
public void play(){
 System.out.println("Brass5.play()");
}
public void adjust(){
 System.out.println("Brass5.adjust");
}
}
package package07;
class Woodwind5 extends Wind5{
public void play(){
 System.out.println("Woodwind5.play()");
}
public String what(){
 return "Woodwind";
}
}
package package07;
public class Music5 {
static void tune(Instrument5 i){
 i.play();
}
static void tuneAll(Instrument5[] e){
 for (int i = 0; i < e.length; i++) {
 tune(e[i]);
 }
}
public static void main(String[] args) {
 Instrument5[] orchestra = new Instrument5[5];
 int i = 0;
 orchestra[i++] = new Wind5();
 orchestra[i++] = new Percussion5();
 orchestra[i++] = new Stringed5();
 orchestra[i++] = new Brass5();
 orchestra[i++] = new Woodwind5();
 tuneAll(orchestra);
}
}
输出:Wind.play()
Percussion5.play()
Stringed.play()
Brass5.play()
Woodwind5.play() 

7.5.1 Java的“多重继承”

接口只是比抽象类“更纯”的一种形式。接口根本没有具体的实施细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。因为我们经常都需要表达这样一个意思:“x从属于a,也从属于b,也从属于c”。在C++中,将各个类合并到一起的行动,称作”多重继承“,而且操作较为不方便,因为每个类都可能有一套自己的实施细节。在Java中,我们可以采用同样的行动,但只有其中一个类拥有具体的实施细节。所以在合并多个接口的时候,C++的问题不会再Java中重演。

在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个费接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于implement 关键字后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会称为一个独立的类型,可对其进行上溯造型。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成一个新类:

7.5.2 通过集成扩展接口

利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口,在这两种情况下最终得到的都是一个新街口,如下所示:

package package07;
interface Monster {
void menace();
}
package package07;
interface DangerousMonster extends Monster{
void destroy();
}
package package07;
interface Lethal {
void kill();
}
package package07;
class DragonZilla implements DangerousMonster{
public void menace(){}
public void destroy() {}
}
package package07;
interface Vampire extends DangerousMonster,Lethal{
void drinkBlood();
}
package package07;
class HorrorShow {
static void u(Monster b){b.menace();}
static void v(DangerousMonster d){
 d.menace();
 d.destroy();
}
public static void main(String[] args) {
 DragonZilla if2 = new DragonZilla();
 u(if2);
 v(if2);
}
} 

DangeroursMonster 是对Monterey的一个简单扩展,最终生成一个新的接口。这是在DragonZilla里实现的。

Vampire的语法仅在继承接口是才可使用。通常,我们只能对单独一个类应用extends(扩展)关键字。但由于接口可能由多个其他接口构成,所以在构建一个新接口时,extends可能应用多个基础接口。正如大家看到的那样,接口的名字就只是简单的实用逗号分隔。

7.5.3 常数分组

由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enmu非常相似的效果。如下例所示:

package package07;
public interface Months {
int
JANUARY = 1,FEBRUARY=2,MARCH=3,APPIL = 4,
MAY = 5,JUNE =6,JULY = 7,AUGUST = 8,
SEPTEMBER = 9,OCTOBER = 10,MOVEMBER=11,DECEMBER=12;
} 

7.6 内部类

在Java1.1中,可将一个类定义置入另一个类定义中。这就就叫作“内部类”。内部类对我们非常有用,因为利用它可对那些逻辑上相互联系的类进行分组,并可控制一个在另一个类里的“可见性”。然而,我们必须认识到内部类与以前讲述的“合成”方法存在着根本区别。

通常,对内部类的需要并不是特别明显得出,至少不会立即感觉到自己需要使用内部类。创建内部类:将类定义置入一个用于封装它的类内部。

package package07;
public class Parcell {
class Contents{
 private int i = 11;
 public int value(){
 return i;
 }
}
class Destination{
 private String label;
 Destination(String whereTo){
 label = whereTo;
 }
 String readLabel(){
 return label;
 }
}
public void ship(String dest){
 Contents c = new Contents();
 Destination d = new Destination(dest);
}
public static void main(String[] args) {
 Parcell p = new Parcell();
 p.ship("Tanzania");
 }
} 

若在ship()内部使用,,内部类的使用看起来和其他任何雷度没有什么区别。在这里唯一明显的区别就是它的名字嵌套在Parcell里面。但是这并不是唯一的区别。

7.6.1 内部类和上溯造型

迄今为止,内部类看起来仍然没什么特别的地方。毕竟,用它实现隐藏显得有些大题小做。Java已经有一个非常优秀的隐藏机制——只允许类成为“友好的”(只在一个包内可见),而不是把它创建成一个内部类。然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后完全进入不可见或不可用状态——对任何人都将如此。所以我们可以非常方便隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型,就如下面所示:

package package07;
abstract class Contents {
abstract public int value();
}
package package07;
interface Destination {
String readLabel();
}
package package07;
public class Parcel3 {
private class PContents extends Contents{
 private int i = 11;
 public int value(){
 return i;
 }
}
protected class PDestination implements Destination{
 private String Label;
 private PDestination(String whereTo){
 Label = whereTo;
 }
 public String readLabel(){
 return Label;
 }
}
public Contents cont(){
 return new PContents();
}
}
package package07;
class Test {
public static void main(String[] args){
 Parcel3 p = new Parcel3();
 Contents c = p.cont();
 Destination d = p.dest("Tanzania");
}
} 

7.6.2方法和作用域中的内部类

至此,我们已基本理解了内部类的典型用途。那些涉及内部类的代码,通常表达的都是“单纯”的内部类,非常简单,且极易理解。然而,内部类的设计非常全面,不可避免地会遇到他们的其他大量用法——假若我们在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做:

(1)正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。

(2)要解决一个复杂的问题,,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。

7.6.3 链接到外部类

我们见到的内部类好像仅仅是一种名字隐藏以及代码组织方案。尽管这些功能非常有用,但似乎并不特别引人注目。然而,我们还忽略了另一个重要的事实。创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个连接。所以他们能访问那个封装对象的成员——无需取得任何资格。除此之外,内部类拥有对封装类所有的访问权限。示例:

package package07;
interface Selestor {
boolean end();
Object current();
void next();
}
package package07;
public class Sequence {
private Object[] o;
private int next = 0;
public Sequence(int size){
 o = new Object[size];
}
public void add(Object x){
 if(next < o.length){
 o[next] = x;
 next++;
 }
}
private class SSelector implements Selestor{
 int i = 0;
 public boolean end(){
 return i = o.length;
 }
 public Object current(){
 return o[i];
 }
 public void next(){
 if(i < o.length)i++;
 }
}
public Selector getSelector(){
 return new SSelector();
}
public static void main(String[] args){
 Sequence s = new Sequence(10);
 for (int i = 0; i < 10; i++)
 s.add(Integer.toString(i));
 Selestor s1 = s.getSelector();
 while (!s1.end()) {
 System.out.println((String)s1.current());
 s1.next();
 }
}
} 

一个内部类可以访问封装类的成员,这是如何实现的呢?内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类。随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管所有的这些细节。但我们现在也可以理解内部类的一个对象只能与封装类的一个对象联合创建。在这个创建过程中,要求封装对象的句柄进行初始化。若不能访问那个句柄,编译器就会报错。

总结

a. 面向对象的三大特性 :封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。这是我们最后一个概念,也是最重要的知识点。

b. 多态的定义 :指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

c. 实现多态的技术称为 :动态绑定(dynamic binding),是指在 执行期间 判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

d. 多态的作用 :消除类型之间的耦合关系。

多态存在的三个必要条件一、要有继承;二、要有重写;三、父类引用指向子类对象。

多态的好处

1.可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。如图8.3 所示。图中超类Shape规定了两个实现多态的接口方法,computeArea()以及computeVolume()。子类,如Circle和Sphere为了实现多态,完善或者覆盖这两个接口方法。4.灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。5.简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。

文章来源:智云一二三科技

文章标题:thank in java第七章 多形性(多态)

文章地址:https://www.zhihuclub.com/197299.shtml

关于作者: 智云科技

热门文章

网站地图