Java动态绑定和多态性详解

首先开始我们以下面的程序来引出今天所讲的多态

一、动态绑定

代码如下

public class Main {
	public static void main(String[] args) {
		Zi b = new Zi();
        b.view();
	}
}

class Fu {
    public int m = 1;
    public void common() {
        System.out.println("这是Fu的common方法");
    }
    public void view() {
    	common();     // 这里是关键,父类和子类都有common方法,那么调用
                      //哪个呢,这里根据下面运行结果就知道是调用的子类的方法
                      //具体原理是什么呢,跟动态绑定有关
                         
    }

}
 
class Zi extends Fu {
	public int m = 2;
	public void common() {
        System.out.println("这是Zi的common方法");
    }
	public void look() {
		view();      // 子类没有该方法,这里涉及继承,子类对象会通过继承链
                          // 找到父类对象中的该方法调用
    }
}

在这里插入图片描述
接下来我们把子类的common方法注释起来又会发生什么
在这里插入图片描述
要想搞懂上面两个程序运行的结果,我们就先要了解下面这些

Java允许程序员不必在编制程序时就确定调用哪一个方法,而是在程序运行的过程中,当方法被调用时,系统根据当时对象本身所属的类来确定调用哪个方法,这种技术被称为后期(动态)绑定。当然这会降低程序的运行效率,所以只在子类对父类方法进行覆盖时才使用

(这里我们贴上动态绑定的定义:动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。

Java中只有static,final,private和构造方法,以及成员变量是静态绑定,其他的都属于动态绑定,而private的方法其实也是final方法(隐式),而构造方法其实是一个static方法(隐式),所以可以看出把方法声明为final,第一可以让他不被重写,第二也可以关闭它的动态绑定。

所以根据这些,我们就可以解释上面的例子为什么是调用的子类的方法,例子中public方法是动态绑定,因为实例对象是子类,所以调用的是子类方法。第一个是子类重写了父类方法,所以运行结果为"这是子类的common方法";而对于第二个来说也是调用子类方法,只不过子类没有重写父类的方法,所以根据继承链找到父类的实现调用,运行结果为"这是父类的common方法"

这里就可以引出我们要讲的多态,多态的实现关键就是靠这种动态绑定(Java中的大多数方法都是属于动态绑定,也就是实现多态的基础。)

二、多态详解

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

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

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

2.多态的前提

  1. 要有继承关系
  2. 要有方法重写
  3. 要有父类引用指向子类对象

多态的前提条件就决定了只有成员方法才有多态,成员变量是没有的

对于成员变量来说
无论是实例成员变量还是静态成员变量,都没有多态这一特性,通过引用变量来访问它包含的成员变量时,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量,成员变量是静态绑定的(只根据对象的当前表示类型决定使用那个变量)

对于成员方法来说:
调用方法时,成员方法支持动态绑定(根据对象的实际类型确定执行那个方法),这里注意静态方法不算

这个口诀可以帮我们理解,但我们一定要弄懂其中原理

  • 成员变量:编译看左边(父类),运行看左边(父类)
  • 成员方法:编译看左边(父类),运行看右边(子类)
  • 静态方法:编译看左边(父类),运行看左边(父类)
    (静态和类相关,算不上重写,所以,访问还是左边的)
    只有非静态的成员方法,编译看左边,运行看右边

我们来个实例解释一下上面这段话

public class Main {
	public static void main(String[] args) {
		Fu f = new Zi();
        System.out.println(f.m);   //与父类一致
        f.method1();			   //与父类一致
        f.method2();			   //编译时与父类一致,运行时与子类一致
        System.out.println("-------------------");
        Zi z = new Zi();
        System.out.println(z.m);
        z.method1();
        z.method2();
	}
}

class Fu {
    public int m = 1;
    
    public static void method1() {
        System.out.println("这是Fu的静态method方法");
    }
    
    public void method2() {
        System.out.println("这是Fu的method方法");
    }
  
}
 
class Zi extends Fu {
	public int m = 2;
	
	public static void method1() {
        System.out.println("这是Zi的静态method方法");
    }
	
	public void method2() {
        System.out.println("这是Zi的method方法");
    }
}

运行结果:
在这里插入图片描述
分析:

Fu f = new Zi();          ----------首先了解变量F到底是什么

把这句子分2段:Fu f;这是声明一个变量f为Fu这个类,那么知道了f肯定是Fu类。然后我们f=newZi();中建立一个子类对象赋值给了f,结果是什么??

结果是,拥有了被Zi类函数覆盖后的Fu类对象          ----------f

也就是说:

一、只有子类的函数覆盖了父类的函数这一个变化,但是f肯定是Fu这个类,也就是说f不可能变成其他比如Zi这个类等等(突然f拥有了Zi类特有函数,成员变量等都是不可能的)。所以f所代表的是函数被复写后(多态的意义)的一个Fu类,而Fu类原来有的成员变量(不是成员函数不可能被复写)没有任何变化。那么获得结论:A:成员变量:编译和运行都看Fu

二、但是f的Fu类函数被复写了。那么获得结论:B:非静态方法:编译看Fu,运行看Zi

三、对于静态方法:编译和运行都看Fu!!(这个又是怎么来的)

其实很简单,首先我们要理解静态情况下发生了什么?

当静态时,Fu类的所有函数跟随Fu类加载而加载了。也就是Fu类的函数(是先于对象建立之前就存在了,无法被后出现的Zi类对象所复写的,所以没发生复写,那么获得结论:C:静态方法:编译和运行都看Fu

下面也是一个典型的多态案例

class Demo1_Polymorphic {
	public static void main(String[] args) {
		Animal a = new Cat();			//父类引用指向子类对象
		System.out.println(a.color);
		a.eat();
	}
}

class Animal {
	public String color = "黑色";
	
	public void eat() {
		System.out.println("动物吃饭");
	}
}

class Cat extends Animal {
	public String color = "白色";
	
	public void eat() {
		System.out.println("猫吃鱼");
	}
}

这里前提是我们知道a是Animal类引用指向的Cat对象,所以它的成员变量编译是跟Animal类绑定在一起的,方法是跟它运行时的Cat对象绑定在一起的
在这里插入图片描述

程序输出结果为黑色,猫吃鱼。那么这个是如何实现的呢,首先,对于color这个成员变量来说,它是静态绑定的,系统总是试图访问它编译时类型所定义的成员变量,而不是它运行时类型所定义的成员变量,所以我们这里访问的就是Animal类所定义的成员变量 —color=“黑色”,而对于eat()这个方法来说,是根据对象的实际类型(Cat类)确定执行这个方法,所以我们得到的是—“猫吃鱼”。(根据上面的口诀也很快能得出答案)

通过这些,基本上对多态有了详细的了解,成员变量和成员方法多态是不同的表现的,这也就是成员在被子类重写时,变量称为"隐藏",而方法称为"覆盖"的主要原因。

然后我们这里要特别强调一点,就是如果要实现多态,父类一定要有被重写的方法,这里我们以下面为例:
在这里插入图片描述

这里我们就明显看到了报错,说这个方法未定义,所以要想用多态,多态的第二条前提是十分重要的(要有方法重写,重点是父类要有被重写的方法,子类不重写父类方法还可以通过继承运行,但是父类没有被重写的方法是会完全报错的,编译都通不过,干脆就是错的)。
在这里插入图片描述
这个就是子类没有重写父类方法,但是程序没错,还是可以完整运行,跟刚刚上面那个是不同的。

多态中向上转型和向下转型

对象的向上转型:父类 父类对象 = 子类实例

   1.父类有的方法,都可以调用,如果被子类重写了,则会调用子类的方法。

   2. 父类没有的方法,而子类存在,则不能调用。

   3.向上转型只对方法有影响,对属性没影响。属性不存在重写。

对象的向下转型:子类 子类对象 = (子类)父类实例

   为什么要发生向下转型?当父类需要调用子类的扩充方法时,才需要向下转型。(这是因为多态的弊端就是不能使用子类的特有功能)

下面这个实例讲述了向上转型和向下转型的具体操作
在这里插入图片描述
运行结果:
在这里插入图片描述

四、多态的好处和弊端

  • A:多态的好处
    1.提高了代码的维护性(继承保证)
    2.提高了代码的扩展性(由多态保证)

  • B:多态的弊端
    不能使用子类的特有属性和行为。

  • C:常用应用场景
    可以当作形式参数,可以接收任意子类对象

五、这里讲一下静态绑定和动态绑定的区别

静态绑定(前期绑定):即在程序执行前,即编译的时候已经实现了该方法与所在类的绑定,像C就是静态绑定,针对Java简单的可以理解为程序编译期的绑定
具体过程就是执行这个方法,只要到这个类的方法表里拿出这个方法在内存里的地址,然后就可以执行了。

Java中只有static,final,private和构造方法,以及成员变量是静态绑定,其他的都属于动态绑定,而private的方法其实也是final方法(隐式),而构造方法其实是一个static方法(隐式),所以可以看出把方法声明为final,第一可以让他不被重写,第二也可以关闭它的动态绑定。


动态绑定(后期绑定):运行时根据对象的类型进行绑定,Java中的大多数方法都是属于动态绑定,也就是实现多态的基础。

Java实现了后期绑定,则必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译的时候该方法不与所在类绑定,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。Java里实现动态绑定的是JVM.

发布了149 篇原创文章 · 获赞 84 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/weixin_43465312/article/details/101542326