进击的新人HF

导航

 

多态概念及使用:

core:

有一个用来判断是否应该设计为继承关系的简单规则,这就是“is-a”规则,它表明子类的每个对象也是超类的对象。例如,每个经理都是雇员,因此,将Manager类设计为Employee类的子类是显而易见的,反之不然,并不是每个雇员都是经理。

    “is-a”规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。例如,可以将一个子类的对象赋给超类变量。

1 Employee e; 
2 e = new Employee();    // Employee object expected 
3 e = new Manager();    // OK, Manager can be used as well 

    在Java程序设计语言中,对象变量是多态的。一个Employee变量既可以引用一个Employee类对象,也可以引用一个Employee类的任何一个子类的对象(例如,Manager,Executive等等)。

    从例5-1中,已经看到了置换法则的优点:

1 Manager boss = new Manager(...); 
2 Employee[] staff = new Employee[3]; 
3 staff[0] = boss; 

在这个例子中,变量staff[0]与boss引用同一个对象。但编译器将staff[0]看成Employee对象。

    这意味着,可以这样调用

  1. boss.setBonus(5000);    // OK

但不能这样调用

  1. staff[0].setBonus(5000);    // Error 

这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。

    然而,不能将一个超类的引用赋给子类变量。例如,下面的赋值是非法的

  1. Manager m = staff[i];    // ERROR 

原因很清楚:不是所有的雇员都是经理。如果赋值成功,m有可能引用了一个不是经理的Employee对象,当在后面调用m.setBonus(...)时就有可能发生运行时错误。

    警告:在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。例如,下面是一个经理数组

  1. Manager[] managers = new Manager[10]; 

将它转换成Employee[]数组是完全合法的:

  1. Employee[] staff = managers;    // OK 

这样做肯定不会有问题,请思考一下其中的缘由。毕竟,如果managers[i]是一个Manager,也一定是一个Employee。然而,实际上,将会发生一些令人惊讶的事情。要切记managers和staff引用的是同一个数组。现在卡一下这条语句:

  1. staff[0] = new Employee("Harry Hacker", ...); 

编译器竟然接纳了这个赋值操作。但在这里,staff[0]与managers[0]引用的是同一个对象,似乎我们把一个普通雇员擅自归入经理行列中了。这是一种很忌讳发生的情形,当调用managers[0].setBonus(1000)的时候,将会导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容。

    为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。例如,使用new managers[10]创建的数组时一个经理数组。

    如果试图存储一个Employee类型的引用就会引发ArrayStoreException异常。

动态绑定:

 弄清调用对象方法的执行过程十分重要。下面是调用过程的详细描述:

    1)编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法。

    至此,编译器已获得所有可能被调用的候选方法。

    2)接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,对于调用x.f("Hello")来说,编译器将会挑选f(String),而不是f(int)。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到域参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。

    至此,编译器已获得需要调用的方法名字和参数类型。

    注释:签名曾经说过,方法的名字和参数列表称为方法的签名。例如,f(int)和f(String)是两个具有相同名字,不同签名的方法。如果在子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

    不过,返回类型不是签名的一部分,因此,在覆盖方法时,一定要保证返回类型的兼容性。在Java SE 5.0以前的版本中,要求返回类型必须是一样的。现在允许子类将覆盖方法的返回类型定义为原返回类型的子类型。例如,假设Employee类有

  1. public Employee getBuddy() {....} 

在后面的子类Manager中,可以按照如下所示的方式覆盖这个方法

  1. public Manager getBuddy() {....} 

我们说,这两个getBuddy方法具有可协变的返回类型。

    3)如果是private方法、static方法、final方法(有关final修饰符的含义将在下一节讲述)或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们列举的示例中,编译器采用动态绑定的方式生成一条调用f(String)的指令。

     4)当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。

    每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,以便寻找与调用f(String)想匹配的方法。这个方法既有可能是D.f(String),也有可能是X.f(String),这里的X是D的超类。这里需要提醒一点,如果调用super.f(String),编译器将对隐式参数超类的方法表进行搜索。

    现在,查看一下例5-1中调用e.getSalary()的详细过程。e声明为Employee类型。Employee类只有一个名叫getSalary的方法,这个方法没有参数。因此,在这里不必担心重载解析的问题。

    由于getSalary不是private方法、static方法或final方法,所以采用动态绑定。虚拟机为Employee和Manager两个类生成方法表。在Employee的方法表中,列出了这个类定义的所有方法:

1 Employee: 
2     getName() -> Employee.getName() 
3     getSalary() -> Employee.getSalary() 
4     getHireDay() -> Employee.getHireDay() 
5     raiseSalary(double) -> Employee.raiseSalary(double) 

    实际上,上面列出的方法并不完整,稍后会看到Employee类有一个超类Object,Employee类从这个超类中还继承了许多方法,在此,我们略去了这些方法。

    Manager方法表稍微有些不同。其中有三个方法是继承而来的,一个方法是重新定义的,还有一个方法是新增加的。

1 Manager: 
2     getName() -> Employee.getName() 
3     getSalary() -> Manager.getSalary() 
4     getHireDay() -> Employee.getHireDay() 
5     raiseSalary(double) -> Employee.raiseSalary(double) 
6     setBonus() -> Manager.setBonus() 

    在运行的时候,调用e.getSalary()的解析过程为:

    1)首先,虚拟机提取e的实际类型的方法表。既可能是Employee、Manager的方法表,也可能是Employee类的其他子类的方法表。

    2)接下来,虚拟机搜索定义getSalary签名的类。此时,虚拟机已经知道应该调用哪个方法。

    3)最后,虚拟机调用方法。

    动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个Executive类的对象,就会自动地调用Executive.getSalary()方法。

    警告:在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是,如果超类方法是public,子类方法一定要声明为public。经常会发生这类错误:在声明子类方法的时候,遗漏了public修饰符。此时,编译器将会把它解释为试图降低访问权限。

博客详解:

java提高篇(四)-----理解java的三大特性之多态

视频笔记:

对象的多态性。

 1 class 动物
 2 {}
 3 
 4 class 猫 extends 动物
 5 {}
 6 
 7 class 狗 extends 动物
 8 {}
 9 
10 猫 x = new 猫();
11 
12 动物 x = new 猫();//一个对象,两种形态。

猫这类事物即具备者猫的形态,又具备着动物的形态。这就是对象的多态性。

简单说:就是一个对象对应着不同类型.

多态在代码中的体现:

父类或者接口的引用指向其子类的对象。

多态的好处:

提高了代码的扩展性,前期定义的代码可以使用后期的内容。

多态的弊端:

前期定义的内容不能使用(调用)后期子类的特有内容。

多态的前提:

1,必须有关系,继承,实现。

2,要有覆盖

多态时,成员的特点:

1,成员变量。

编译时:参考引用型变量所属的类中的是否有调用的成员变量,有,编译通过,没有,编译失败。

运行时:参考引用型变量所属的类中的是否有调用的成员变量,并运行该所属类中的成员变量。

简单说:编译和运行都参考等号的左边。哦了。

 1 57.class DuoTaiDemo 
 2 {
 3 public static void main(String[] args) 
 4 {
 5 
 6 //    Cat c = new Cat();
 7 //    c.eat();
 8 //    c.catchMouse();
 9 
10 Animal a = new Cat(); //自动类型提升,猫对象提升了动物类型。但是特有功能无法s访问。
11 //作用就是限制对特有功能的访问。
12 //专业讲:向上转型。将子类型隐藏。就不用使用子类的特有方法。
13 
14 
15 //    a.eat();
16 
17 //如果还想用具体动物猫的特有功能。 
18 //你可以将该对象进行向下转型。
19 //    Cat c = (Cat)a;//向下转型的目的是为了使用子类中的特有方法。
20 //    c.eat();
21 //    c.catchMouse();
22 
23 //    注意:对于转型,自始自终都是子类对象在做着类型的变化。
24 //    Animal a1 = new Dog();
25 //    Cat c1 = (Cat)a1;//ClassCastException
26 
27 
28 /*
29 Cat c = new Cat();
30 
31 //    Dog d = new Dog();
32 
33 //    c.eat();
34 method(c);
35 //    method(d);
36 //    method(new Pig());
37 */
38 
39 method(new Dog());
40 
41 }

2,成员函数(非静态)。

编译时:参考引用型变量所属的类中的是否有调用的函数。有,编译通过,没有,编译失败。

运行时:参考的是对象所属的类中是否有调用的函数。

简单说:编译看左边,运行看右边。

因为成员函数存在覆盖特性。

3,静态函数。

编译时:参考引用型变量所属的类中的是否有调用的静态方法。

运行时:参考引用型变量所属的类中的是否有调用的静态方法。

简单说,编译和运行都看左边。

其实对于静态方法,是不需要对象的。直接用类名调用即可。

posted on 2018-02-04 23:31  进击的新人HF  阅读(199)  评论(0)    收藏  举报