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);
	}
}

  

五、用继承进行设计

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

六、总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 多态是面向对象编程中的一个重要概念,它指的是同一个方法或者同一个类在不同的情况下表现出不同的行为。在实际编程中,我们可以通过继承、接口等方式来实现多态。通过多态,我们可以提高代码的可复用性和可扩展性,使得程序更加灵活和易于维护。在使用多态时,我们需要注意一些细节,比如要遵循里氏替换原则,保证子类可以替换父类,不破坏程序的正确性。 ### 回答2: 多态,是面向对象编程中一个非常重要的概念,也是面向对象编程的三大特性之一,同封装和继承。它允许不同的对象对同一个消息作出不同的响应,也就是说,多态性指的是同一操作符或方法作用于不同的对象时,会产生不同的结果。在实际开发中,多态性可以使我们在编写程序时更加灵活、方便和高效。 多态性的实现,通常是通过继承和接口来实现的。当一个类实现了一个接口时,该类可以被看作是该接口的实例,并且可以被统一的调用。而在实际的应用中,多态常常会被用于设计模式中的策略模式、工厂模式等等。 在阿猫阿狗的第3关中,我们需要使用多态来实现不同种类的动物之间的交互。在这个例子中,我们使用了一个抽象的动物类 Animal 去定义所有动物的公共属性和方法,然后针对不同的种类动物,我们分别定义了不同的子类。例如,猫和狗是不同的子类,它们继承了 Animal 类,然后根据自己的需要进行了方法的重写,使得它们能够根据不同的动物状态进行不同的动作。 在游戏中,玩家需要根据游戏的要求,选择不同种类的动物进行操作。因为所有动物都继承了 Animal 类,所以我们可以通过 Animal 类的实例来操作不同种类的动物,而且操作的方式和方法都是一样的,这就是多态性的实现。 总之,多态性作为面向对象编程的重要特性之一,可以极大的提高程序的灵活性和可扩展性。在实际开发中,我们可以将多态性应用于更加复杂的系统设计中,从而使我们的程序设计更加高效、合理和可靠。 ### 回答3: 多态是面向对象编程的核心之一,它体现了面向对象编程的一个重要特征:重载。以“阿猫阿狗”为例,猫和狗都是宠物,它们有相同的属性,例如名字、年龄和品种等,但猫和狗又具有不同的行为,比如狗会汪汪叫,猫会喵喵叫。这就是多态多态的实现,需要利用面向对象的继承、封装和重写机制。 在“阿猫阿狗”这个例子中,我们可以定义一个Pet类,表示所有宠物都需要具备的属性和行为。然后,定义猫和狗的类,它们都继承自Pet类。在Pet类中,我们定义一个抽象的叫声方法,让猫和狗各自去重写这个方法,在具体的实现中,狗会输出“汪汪”,而猫会输出“喵喵”。 这样,我们就可以实现对猫和狗的多态操作了。在程序中,我们可以定义一个Pet类型的引用变量,它可以指向一个猫或者一个狗对象。当我们调用叫声方法时,具体执行哪个方法是由实际的对象来决定的。例如,如果Pet变量指向一个狗对象,调用叫声方法时就会输出“汪汪”。 多态的使用让程序变得更加灵活和可扩展,我们可以通过它来管理不同种类的对象,简化代码结构,提高代码的重用性和可维护性。在实际开发中,多态也是非常常用的,比如在Java中,很多核心API都支持多态调用,例如List、Set和Map等容器类,可以存储和操作不同类型的对象。 总之,多态是面向对象编程中非常重要的概念,实现多态需要继承、封装和重写技术的支持,它使得程序更加灵活和可扩展,提高了程序的重用性和可维护性。在实际开发中,尤其是大型项目中,多态的应用更是不可或缺的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值