定义
对于同一个类型的不同实例对象,同一个行为具备不同的表现形式。
作用
- 消除同一类型之间的耦合关系;
- 使得不同对象对于同一行为具备多种表现形式;
原理
变量的静态类型 & 动态类型
变量的静态类型 = 引用类型 = 编译时变量 :不会被改变、在编译期可知
变量的动态类型 = 实例对象类型 = 运行时变量 :会变化、在运行期才可知
如下示例:
1 | public class Test { |
实现原理
通过将 子类对象实例
赋值给 父类引用变量
,使得编译时的静态变量 与 运行时的动态变量不一样
,在调用方法 / 变量时,多态就发生了。
应用示例:
1 | // 设 B类是A类的子类 |
结论:因将 子类对象引用 赋值给 父类对象变量,即A a = new B(),即 编译时变量和运行时变量不一样,所以多态发生
实现过程(直接指针访问)
JVM
通过 引用类型(reference
,即A
的引用)查询Java
栈中的本地变量表,得到堆中的对象类型的数据地址;- 根据地址,从而找到方法区中的对象类型数据(
B
的对象类型数据); - 查询对象类型数据中的方法表定位到实际类(
B
类)的方法,从而运行。
对于A a = new B()
数据存储方式
- 对于
A a
:作为引用类型数据,存储在JVM栈的本地变量表中;- 对于
new B()
:
- 作为实例对象数据存储在堆中;
B
的对象实例数据(接口、方法、对象类型等)的地址存储在堆中;B
的对象类型数据(对象实例数据的地址所执行的数据)存储在方法区中,在方法区中,对象类型数据中有1个指向该类方法的方法表;
引用类型访问实现方式
Java
程序通过 栈上的引用类型数据(reference) 来访问Java
堆上的对象。
由于引用类型数据(reference)在 Java
虚拟机中只规定了一个指向对象的引用,但没定义该引用应该通过何种方式去定位、访问堆中的对象的具体位置所以对象访问方式 取决于 虚拟机实现。目前主流的对象访问方式有两种:
- 句柄访问
- 直接指针访问
多态实现方式
方法重载(Overload)
定义
同一个类中具备多个重名的方法。
特点
- 方法名字必须相同;
- 参数列表必须不同:个数不同;个数不同但对应参数类型不同;
- 返回类型/访问修饰符可同可不同;
- 可声明新的或更广的检查异常;
- 能够在同一个类或子类中被重载;
应用场景
针对同一类的对象,对于不同条件同一行为的不同表现;
原理
静态分派,发生在编译阶段。
示例
1 | package main.java.com.study.polymorphic; |
执行结果:
1 | hello,guy! |
结果解析:
- 方法重载(
OverLoad
)的原理 = 静态分派 = 根据 变量的静态类型 确定执行(重载)哪个方法;- 所以上述的方法执行时,是根据变量(
man
、woman
)的静态类型(Human
、Man
)确定重载sayHello()
中参数为Human guy
、Man guy
的方法,即sayHello(Human guy)
、sayHello(Man guy)
;
方法重写(Override)
定义
子类重写父类方法的实现。
特点
- 返回值、函数名、形参都不能改变,即外壳不变,重写内在实现;
- 不能被重写的方法:构造方法;不能继承/不具备访问权限的方法(如
private
);声明为final
、static
的方法; - 子类方法不能缩小父类方法的访问权限;
- 子类方法不能比父类方法抛出更广的异常;
原理
动态分派,发生在运行阶段。
示例
1 | package main.java.com.study.polymorphic; |
执行结果
1 | man say hello |
结果分析
- 方法重写(
Override
) = 动态分派 = 根据 变量的动态类型 确定执行(重写)哪个方法; - 对于情况1:根据变量(
Man
)的动态类型(man
)确定调用man
中的重写方法sayHello()
; - 对于情况2:根据变量(
Man
)的动态类型(woman
)确定调用woman
中的重写方法sayHello()
;