继承层次以及多态的理解调用

继承层次以及多态的理解调用

继承层次

继承并不只限于一个层次。例如,可以由Manager类派生出Executive类。由一个公司超类派生出的所有类的集合称之为继承层次。在继承层次中,从某个特定的类到其祖先的类的路径成为该类的继承链。
通常,一个祖先类可以拥有多个子孙类。例如,可以由Employee类派生出的子类Programmer和Secretary,它们与Manager类没有任何的关系。

多态

有一个简单规则可以用来判断是否应该将数据设计成继承关系,就是“is-a”规则。他指出子类的每个对象也是超类的对象。
例如每个经理也是员工,因此将Manager类设置为Employee类的子类是有道理的;反之则不然,并不是每个员工都是经理。
“is-a”规则的另一种表达是替换原则。他指出程序中出现超类对象的的任何对象都可以使用子类对象替换。
例如可以将子类的对象赋给超类对象。

Employee e;
e=new Employee (. . .);//Employee类建立
e=new Mananger(. . .);//Manager类也可以被调用

在Java程序设计语言中,对象变量是多态的。一个Employee类型的变量既可以引用一个Employee类的对象,也可以引用Employee类任何一个子类的对象。
在上一篇文章的程序清单1中,我们就已经利用了这个替换原则。

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

在这个例子中变量staff[0]和boss引用的是同一个对象。但编译器只将staff[0]看成是一个Employee类对象。
这意味着我们可以这样调用
boss.setBonus(5000);
但是不能这样调用
staff[0].setBonus(5000);
这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。
不过,不能将超类引用赋值给予子类变量。例如下面的赋值是非法的:

Manager m = staff[i]; 

原因很清楚,应为并不是所有的员工都是经理。当运行调用到setBonus时就会发生运行错误。

提示

  • 在Java中,子类引用的数组可以转换成超类引用的数组,而不需要使用强制类型转化。例如下面是一个经理数组
    Manager[] manager=new Manager[10];
    将它转化为Employee[]数组是完全合法的:
    Employee[ ] staff =managers;
    这样操作肯定不会有问题,毕竟一个manager也一定是一个employee。不过,实际上会发生一些让人惊讶的事。要切记manager和staff引用的是同一个数组。现在看这条语句
    staff[0] = new Employee("Carl Cracker", 75000, 1994, 12, 15);
    编译器竟然接纳了这个赋值操作。但在这里,staff[0]和manager[0]是相同的引用,似乎将一个员工归于经理之中了。
    为了确保不在发生这类破坏,所有数组都要牢记建立的元素类型,并负责监督仅将类型兼容的引用储存到数组中。

理解方法如何调用

准确的理解如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x为类c的一个对象,下面是调用过程的详细描述:
1.编译器查看对象的声明类型和方法名。需要注意的是:有可能存在多个名字为f但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举c类中所有名为f的方法和其超类中所有名为f而且可以访问的方法。
到此编译器知道了所有可能被调用的方法。
2.接下来,编译器要确定方法调用中提供的参数类型。如果在所有名为f的方法中存在一个与所提供参数类型完全匹配的方法,就选择这个方法。这个过程叫做重载解析。例如,对于调用f.(“Hello!”),编译器会首先挑选f.(String),而不是f.(int)。由于允许类型转换,所以情况可能会变得很复杂。如果编译器没有找到参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,编译器就会报告一个错误。
至此,编译器已经知道需要调用的方法的名字和参数类型。
3.如果是private方法,static方法,final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法。这叫做静态绑定。与此对应的是,如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行是动态绑定。在我们的示例中,编译器会利用动态绑定生成一个调用f(String)的指令。
4.程序运行并且采用动态绑定调用方法时,虚拟机必须调用与x所引用对象的实际类型对应的那个方法。假设x的实际类型是D,他的子类是C类的子类。如果D类定义了方法f(String),就会调用这个方法;否则,将D类的超类中寻找f(String),以此类推。
每次调用方法都要完成这一系类的搜索,时间开销相当大。因此,虚拟机预先为每个类计算了一个方法表,其中列出来了所有的方法签名和要调用的实际方法。这样一来,在真正调用方法的时候,虚拟机仅仅查找这个表就行了。在前面的例子中,虚拟机搜索D类的方法表,寻找与调用f(String)相匹配的方法。这个方法极有可能是D.f(String),也有可能是X.f(String),这里的X是D的超类。这里提醒一下,如果调用的是super.f(param)那么编译器对隐式参数超类的方法进行查找。
现在来详细分析程序清单1中调用e.getSalary()的过程。e声明为Employee类型。Employee类只有一个名叫getSalary的方法,这个方法没有参数。因此,在这里不必担心重载解析问题。
由于getSalary()不是private方法,static方法,final方法所以采用动态绑定。虚拟机为Employee类和Manager类生成方法表。在Employee的方法表中列出了这个Employee类本身定义的所有方法

EmployeegetName()->Employee.getName()
	getSalary()->Employee.getSalary()
	getHireDay()->Employee.getHireDay()
	raiseSalary(double)->Employee.raiseSalary(double)

实际上,上面列出的方法并不完整,稍后会看到Employee类有一个超类Object,Employee类从这个超类中还继承了大量方法,为此我们略去了Object方法。
Manager方法表稍微有些不同。其中有三个方法是继承而来,一个方法是重新定义的,一个是新增加的。

ManagergetName()->Employee.getName()
	getSalary()->Manager.getSalary()
	getHireDay()->Employee.getHireDay()
	raiseSalary(double)->Employee.raiseSalary(double)
	setBonus(double)->Manager.setBonus(double)

在运行时,调用e.getSalary()的解析过程为:
1、首先,虚拟机获取e的实际类型方法表。
这可能是Employee类和Manager类的方法表,也可能是Employee类的其他子类的方法表。
2.接下来,虚拟机查找定义了getSalary()签名的类。此时虚拟机已经知道该定义哪个方法。
3.最后,虚拟机调用这个方法。
动态绑定有一个非常重要的特征:无须对现有的代码进行修改就可以对程序进行扩展。假设增加一个新类Executive,并且变量e有可能对这个类的对象进行引用,我们就只有自动调用Executive.getSalary()方法,而不用重新进行编译。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值