Java编程思想学习笔记八: 多态

一、再论向上转型

    在第七章中我们说过,对象引用既可以作为它自己本身的类型使用,也可以作为它的基类型使用,这种把某个类型引用作为它的基类型使用的做法被称为向上转型。

package com.chenxyt.java.practice;
class Instrument{
	public Instrument() {
		//---
	}
	public void print(){
		System.out.println("Instrument-----:");
	}
}
class Wind extends Instrument{
	public void print(){
		System.out.println("Wind-----:");
	}
}
public class Music{
	public static void play(Instrument i){
		i.print();
	}
	public static void main(String[] args) {
		Wind wind = new Wind();
		play(wind);
	}
}

  

    Music.play方法接受一个Instrument的引用,同时也接受任何Instrument类的子类,即main方法中play方法传递wind引用的时候,不需要做任何类型转换。这样做是允许的,因为Wind自Instrument类继承而来,所以Instrument类的接口必定存在于Wind类中。

    我们可能觉得Music.play方法有点奇怪,既然要传递Wind引用,为什么不再写一个方法来传递Wind引用呢?当然,再写是可以的,只不过会使程序显得过为复杂而已,比如Instrument类有多个子类,那么就要写多个方法,显然没有这种单一的方法显得高效。因此只要我们记好这种特性那么就会使程序变得更加简洁。

二、转机

    考虑一个新的问题,当我们有多个子类的时候,编译器是怎样知道我们传递给基类引用的参数是哪个子类呢?比如我们把刚才的程序做个修改,再增加一个子类:

package com.chenxyt.java.practice;
class Instrument{
	public Instrument() {
		//---
	}
	public void print(){
		System.out.println("Instrument-----:");
	}
}
class Wind extends Instrument{
	public void print(){
		System.out.println("Wind-----:");
	}
}
class Rain extends Instrument{
	public void print(){
		System.out.println("Rain-----");
	}
}
public class Music{
	public static void play(Instrument i){
		i.print();
	}
	public static void main(String[] args) {
		Rain rain = new Rain();
		play(rain);
	}
}

运行结果如下:

    
    在play方法中,既然做到了向上转型,那么编译器怎样知道类型从哪里转来的呢?即执行哪个对应的print()方法呢?解决这个问题有一个新的概念叫做绑定,将方法调用(即引用)与方法体进行的关联叫做绑定,绑定分为前期绑定和后期绑定,前期绑定发生在程序执行前,C语言只有这一种默认的调用方式。上述程序的疑惑在于,如果进行前期绑定,那么对于Instrument类的引用i来说,它是不知道应该绑定到哪个方法上的。所以解决上述问题的办法就是使用后期绑定,后期绑定就是程序运行时根据对象的类型来进行绑定,也叫做动态绑定。一种语言要想实现动态绑定,那么它必须具有某种特定的机制来支持它在运行时准确的找到对象引用对应的类型,随着语言的不同这种机制有所不同,但大体上都是在对象中增加了某种类型信息。
    Java中除了static和final之外,其它所有的方法都是后期绑定,所以我们无需显示的去做什么操作,因为动态绑定会自动发生。而前期绑定并不会对性能造成什么影响,使用final修饰的意图是防止被覆盖,并且告诉编译器这个是前期绑定,那么编译器可以更好的为其分配资源。
    了解了前期绑定与后期绑定,我们能够更好的理解了上述向上转型的问题,并且可以设计一个只针对基类的方法,供基类以及它所有的子类进行调用。最常见的示例就是第一章所说的几何形的问题,所有的几何形都有同样的绘制擦除操作,我们可以在几何形这个基类中编写这些同样的操作,然后当传递不同的子类型时,由于动态绑定的操作,它们每个子类型都可以被正确操作。

    上述代码中,不同的子类与基类都有相同的方法(返回值、方法名、参数列表都相同),但是方法体内部不相同,这种操作叫做方法的重写或者方法的覆盖,即子类覆盖了父类方法的实现,当参数传递为子类对象的引用时,虽然看似调用了父类的这个方法,但是实际上由于动态绑定调用了子类的方法,实现了不同的功能。这也就是面向对象编程中多态的意义所在。而对于调用方法来说,比如上述代码中的Music.play来说并没有什么变化,也就是子类方法重写的改动,对方法调用来说没有什么影响,换句话说就是多态是一项让程序员“将改变的事物与不变的事物分离开来”的重要技术!

    方法的重写有如下几点缺陷:
    1.重写私有方法
package com.chenxyt.java.practice;
public class PrivateOverride{
	private void f(){
		System.out.println("private void f");
	}
	public static void main(String[] args) {
		PrivateOverride po = new Derived();
		po.f();
	}
}
class Derived extends PrivateOverride{
	public void f(){
		System.out.println("public void f");
	}
}

    我们期望Derived类重写了f方法,但是实际结果并不是这样,因为f方法为private方法,因此它不能被重写,这种情况子类中的f方法被当做了新的方法。虽然编译器没有报错,但是实际结果也不是预期那样,解决这种问题的办法是避免子类中的方法与父类的方法重名。

    
    2.域与静态方法:

    实际上只有普通的方法调用可以是多态的,对于域和静态方法都不是多态的。域是在访问的时候编译期进行解析的。

package com.chenxyt.java.practice;
class Super{
	public int field = 0;
	public int getField(){
		return field;
	}
}
class Sub extends Super{
	public int field = 1;
	public int getField(){
		return field;
	}
	public int getSuperField(){
		return super.field;
	}
}
public class fieldAccess{
	public static void main(String[] args) {
		Super sup = new Sub();
		System.out.println("sup.field" + sup.field + "---sup.GetField" + sup.getField());
		Sub sub = new Sub();
		System.out.println("sub.field" + sub.field + "---sub.GetField" + sub.getField() + "---sub.GetSuperField" + sub.getSuperField());
	}
}

      

当Sub对象转型为Super引用时,任何域访问操作都将由对象编译器解析,因此不是多态的,所以第一行第一个值通过直接访问域的形式返回的是0,对于普通方法getField则是多态的,所以第一行第二个值返回1。第二行就比较好理解了,不涉及向上转型的问题,唯一使用了getSuperField方法显示的获取了基类的值。

    在本例中为sub.field域和super.filed域分配了不同的内存空间,也就是对于sub来说他有两个field值,一个是它本身的field值,另一个是来自super继承的值,而在sub引用域field时使用的并非是来自super的值,而是它自己本身的默认值。在实际工作中,这种问题基本不会发生,避免问题出现的有效做法是将基类与子类的域分别起不同的名字。

     静态方法由于只与类有关,而不与对象牵连,因此它不存在多态的形式。

三、构造器和多态

    构造器的调用顺序在第五章和第七章都已经讨论过了,构造器区别于其它普通的方法,它是隐式的static函数,所以它不存在多态性。这里我们再次说一下构造器的调用过程,尤其是在复杂的继承与多态的情况下。


package com.chenxyt.java.practice;
class First{
	public First(){
		System.out.println("First---");
	}
}
class Second{
	public Second(){
		System.out.println("Second---");
	}
}
class Third{
	public Third(){
		System.out.println("Third---");
	}
}
class Fourty extends Third{
	public Fourty(){
		System.out.println("Fourty---");
	}
}
class Fifty extends Fourty{
	public Fifty(){
		System.out.println("Fifty---");
	}
}
public class Sixty extends Fifty{
	public Sixty(){
		System.out.println("Sixty---");
	}
	First first = new First();
	Second second = new Second();
	public static void main(String[] args) {
		new Sixty();
	}
	
}

    前一章说到过,类的加载过程是从上向下的,也就是它会去找到最根部的那个类,然后进行加载。如上程序Sixty继承了Fifty类,Fifty类继承了Fourty类,Fourty类继承了Third类,所以最先加载的构造器是Third类,当所有构造器都加载完成后,会加载子类中的成员变量,然后最后加载子类的构造函数。

    
    根据前一章跟这章的例子,总结程序初始化的顺序:
    1.初始化基类中用到的静态变量,静态方法;
    2.初始化main()方法中的常量,如果是有静态变量的对象,先初始化静态变量,然后加载其构造器;
    3.加载基类构造器;
    4.按顺序初始化成员变量;
    5.加载子类构造器。

四、协变返回类型

    Java SE5中添加了协变返回类型,它表示在子类中的覆盖方法,可以返回其导出类该方法返回类型的某一个子类型。读起来可能比较绕口,通俗来说就是一个子类覆盖了父类的func方法,这个func方法在父类中返回的是Father类,如果这个Father类有一个子类Son类,那么前边说到的写覆盖方法的子类就可以将这个方法返回Son类。
package com.chenxyt.java.practice;
class Father{
	public String toString(){
		return "Father";
	};
}
class Son extends Father{
	public String toString(){
		return "Son";
	}
}
class Mill{
	Father process(){
		return new Father();
	}
}
class wheatMill extends Mill{
	Son process(){
		return new Son();
	}
}
public class Test{
	public static void main(String[] args) {
		Mill m = new Mill();
		Father f = m.process();
		System.out.println(f);
		m = new wheatMill();
		f = new Son();
		System.out.println(f);
	}
}

  

五、用继承进行设计

    了解了继承中多态的特性,我们可能觉得继承很好用,设计程序的时候第一想到的就是继承,其实并不是这样,从某些角度来说,继承可能会加大程序的复杂性。我们在程序设计的过程中应优先选用组合,并且前面一章已经说到过,继承跟组合还是有区别的,如果单纯的是想使用某一个类,让这个类的对象完成一些功能,那么使用组合的设计会更好一些。此外, 对于继承中多态的实现,很大部分原因是由于向上转型与动态绑定,针对向上转型,与之相对的叫做向下转型,我们都知道向上转型是安全的,而由于扩展性的原因,向下转型并不是安全的。因为父类可能并没有子类中的一个方法。

六、总结

    多态是面向对象“封装”、“继承”、“多态”三大特性之一,理解多态的特性能够更好的设计程序,同时,掌握程序初始化加载的过程能够更好的理解程序,这也是重中之重。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值