Java中的多态到底啥意思

理解

面向对象的三大特征:封装,继承,多态

普遍认为多态指这两种类型:

1.函数的多态性 :函数重载,同一个类中有多个同名函数,它们的参数类型或者个数或者顺序不同。

2.对象的多态性:函数重写,子类重写了父类中的函数,它们的参数类型或者个数及返回值都相同。

1.函数重载

package com.xtm;

class B {
	void fun() {
		System.out.println("this is B fun()");
	}

	void fun(String param) {
		System.out.println("this is B fun(String param):" + param);
	}

}

public class Main {
	 
	public static void main(String[] args) {
		B b = new B();
		b.fun();
		b.fun("fun Overload");
	}

}

输出:

this is B fun()
this is B fun(String param):fun Overload

2.函数重写(覆写)

package com.xtm;

class B {
	void fun() {
		System.out.println("this is B fun()");
	}

}

class A extends B{
	@Override
	void fun() {
		System.out.println("this is A fun()");
	}
}

public class Main {
	 
	public static void main(String[] args) {
		B b = new B();
		b.fun();
		A a = new A();
		a.fun();
		// 父类的引用指向子类对象
		B b1 = null;// 声明一个B对象的引用
		A a1 = new A();// 创建一个A对象
		b1 = a1;// B对象的引用指向A对象
		b1.fun();// 调用B的fun函数
		
	}

}

输出:

this is B fun()
this is A fun()
this is A fun()

分析上述代码:

B b1 = null; 这句在栈空间声明了一个B的引用,即b1可存放一个4字节的地址(假设32位操作系统)

A a1 = new A();这句在堆中开辟了一块空间(假设这块空间的地址为0x0...0A),栈中a1存放这块空间的地址(a1指向这块空间)。

b1 = a1;这句使b1的存放的值变为了a1的值,它也指向了上述开辟的堆空间。

这里就有疑问了,为啥b1=a1可以赋值,两个不同的类,一个父类一个子类,就是单纯的赋值吗?这就需要了解java的继承机制,内部到底怎么实现的?

A  extend B{} 这种形式的,到底做了什么?

这需要了解虚拟函数表

当java编译的时候,每个类都对应着一张虚拟函数表,创建对象的时候,这些对象都指向这张函数表。

为方便分析,将上述代码修改如下:

package com.xtm;

class B {
	void fun() {
		System.out.println("this is B fun()");
	}
	
	void otherBFun(){
		
	}

}

class A extends B{
	@Override
	void fun() {
		System.out.println("this is A fun()");
	}
	
    void otherAFun(){
		
	}
}

public class Main {
	 
	public static void main(String[] args) {
		// 父类的引用指向子类对象
		B b1 = null;// 声明一个B对象的引用
		A a1 = new A();// 创建一个A对象
		b1 = a1;// B对象的引用指向A对象
		b1.fun();// 调用B的fun函数
		
	}

}

上述B的函数表为:

A的函数表:

可见A的虚拟函数表中包含了自己的函数oherAFun继承自B的函数otherBFun(被子类直接从父类拷贝了过来),以及复写的fun(并非直接从父类拷贝过来,它不再是B中的fun而是全新的函数)。

可以想象每个类都对应着各自的函数表,而new 关键字会去创建一段内存来初始化这个类(new关键字后面的类)中属性和函数地址。

 可见函数重写的本质就是子类的函数表中把重写的函数换成了自己的函数。

这样父类调用被重写的函数的时候实际就是去看是哪个子类(new 后面的类)创建的堆,然后在这个堆中找到对应函数的地址,接着找到对应函数表去执行对应函数。

ps:

1.new哪个类,new出来的堆空间就和这个类的虚拟函数表相关联。

2.栈中引用也叫参考reference,也可以看做是一个指针,这个参考也有类型,即它可以指向的那种类型,它可以指向自己也可以指向它的子类型。

3.引用的作用,指向作用,它可以指向任何可以分配到的空间,达到链式存储的效果(想象下链表),这样分配到的空间可以不是连续的,比较灵活。

4.假如

A extend B

B extend C

C extend D

看代码:

package com.xtm;
class D{
	public D(){
		System.out.println("init D");
	}
}

class C extends D{
	public C(){
		System.out.println("init C");
	}
}

class B extends C{
	
	public B(){
		System.out.println("init B");
	}
}

class A extends B{
	public A(){
		System.out.println("init A");
	}
}

public class Main {
	 
	public static void main(String[] args) {
		D d = new A();
		
	}

}

输出:

init D
init C
init B
init A

得出结论:

当你new A()的时候,实际上A的构造函数中会先new B(),而new B()的构造中又先new C(),而new C()构造中会先new D();这就无形之中把这四个类的对象串联了起来,形成了类似一个链表的状态。

类比:A表示孙子,B表示儿子,C表示你,D表示父亲

看看如下形式:

D d = new D()// 出生了D

C c = new C()// 出生了D,C

B b = new B()// 出生了D,C,B

A a = new A()// 出生了D,C,B,A

假如赋值号“=”左边的部分可以看做是别人要抓的人(假如家族中某个人是犯人,朝廷命官要捉拿归案,必须是已经出生的人才可以被访问):

A a = new A()     别人抓到了孙子A并且可以访问他四代A,B,C,D,因为new A()的时候B,C,D也都被new了

B b = new B()     别人抓到了儿子B并且可以访问他三代B,C,D,因为new B()的时候C和D也被new了

D d = new C()     别人抓到了父亲D并且可以访问他两代C,D,因为new C()的时候B和A并未被new出来,只new出了C,D

D d = new A()     别人抓到了父亲D并且可以访问他四代A,B,C,D, 因为new A()的时候B,C,D也都被new了 

// 以下出错

A a = new D()     因为new D()的时候只出生了D而A并没有出生

抓谁最明智?

ABCD都可以抓,关键要看后面new了谁,因为他可以确保你抓的人是否已经出生了(被实例化)!

如果你抓住了儿子B(即赋值号左边是B的申明),则必须保证B已经被实例化了即只能是B b = new B()或者B b = new A();那么你无法B b = new C()或者B b = new D();因为new C()只保证CD的出生,new D()只保证D的出生,B还没出生呢,你抓了个不存在的东西(NULL)。

如果你抓住了父亲D(即赋值号左边是D的申明),那么是很有前途的,因为后面不管谁出生(new 谁)都确保了D已经出生了(D一定已经被实例化了),你都可以访问他们。

可见

只要持有了D就不怕后面new谁,后面不管你有诞生(new)多少子孙类都可以确保D已经存在(实例化)。

另外,new A()已经把整个家族都给new出来了,并串到了一起,A->B->C->D即A对象持有B对象引用(super),B持有C对象引用(super),C持有D对象引用(super)。

所以这时候,别人只要持有A对象即链表头,就可以访问其祖辈的可见数据。

猜你喜欢

转载自blog.csdn.net/qq_17441227/article/details/109505341