Java 多态性
多态性, 是面向对象中的重要概念,在Java中的体现:子类对象的多态性:父类的引用指向子类的对象;
在多态的场景下,调用方法时:父类类型 变量名 = 子类对象 ;
编译时:认为方法是左边声明的父类的类型的方法(即被重写的方法)
执行时:实际执行的是子类重写父类的方法
多态性使用前提:1.要有类的继承;2.要有方法的重写;
对象的多态:在 Java 中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象;
Java 引用便改良有两个类型:编译时类型 和 运行时类型。编译时类型由声明该变量的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译看左边,运行看右边
举例:
public class Person {
public void eat(){
System.out.println("大口吃肉");
}
public void drink(){
System.out.println("大口喝酒");
}
}
----------------------------------------
public class Man extends Person {
public void eat(){
System.out.println("男人大口吃肉");
}
public void drink(){
System.out.println("男人大口喝酒");
}
}
----------------------------------------
public class PersonTest {
public static void main(String[] args) {
// 父类类型 变量名 = 子类对象;
Person p1 = new Man();
p1.eat();
p1.drink();
}
}
方法内部变量的赋值体现多态:
public class Animal {
private String name;
// setName getName
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat() {
System.out.println(name + "吃东西");
}
}
--------------------------------------------
public class Dog extends Animal{
// eat() --- 子类重写父类方法
public void eat() {
System.out.println("狗狗:" + getName() + "啃骨头");
}
// watchHouse --- Dog类独有方法
public void watchHouse(){
System.out.println("狗狗看家");
}
}
----------------------------------------------
public class AnimalTset {
public static void main(String[] args) {
Animal an = new Dog();
an.setName("豆豆");
an.eat(); // 执行子类Dog重写方法
// 输出: 狗狗:豆豆啃骨头
}
}
方法的形参声明体现多态:
public class Home {
private Person person;
public void adopt(Person person) {
this.person = person;
}
public void show(){
person.eat();
}
}
----------------------------------
public class Person extends Home{
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
----------------------------------
public class Man extends Person {
public void eat(){
System.out.println("男人" +getName()+ "吃饭");
}
public void sleep(){
System.out.println("男人" +getName()+ "睡觉");
}
}
----------------------------------
public class Woman extends Person{
public void eat(){
System.out.println("女人" +getName()+ "吃饭");
}
public void sleep(){
System.out.println("女人" +getName()+ "睡觉");
}
}
---------------------------------
public class TestHome {
public static void main(String[] args) {
Person person = new Person();
Man man = new Man();
man.setName("占山");
person.adopt(man);
person.show(); // 男人占山吃饭
Woman woman = new Woman();
woman.setName("小丽");
person.adopt(woman);
person.show(); // 女人小丽吃饭
}
}
方法返回值类型体现多态:
public class Person{
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
-----------------------------------------
public class Man extends Person {
public void eat(){
System.out.println("男人" +getName()+ "吃饭");
}
public void sleep(){
System.out.println("男人" +getName()+ "睡觉");
}
}
--------------------------------------------
public class Woman extends Person {
public void eat(){
System.out.println("女人" +getName()+ "吃饭");
}
public void sleep(){
System.out.println("女人" +getName()+ "睡觉");
}
}
---------------------------------------
public class Choose {
public Person show01(String type){
switch (type){
case "Man":
return new Man();
case "Woman":
return new Woman();
}
return null;
}
}
--------------------------------------
public class TestChoose {
public static void main(String[] args) {
Choose choose = new Choose();
Person man = choose.show01("Man");
man.setName("小白");
man.eat(); // 男人小白吃饭
Person woman = choose.show01("Woman");
woman.setName("小张");
woman.eat(); // 女人小张吃饭
}
}
多态的好处与弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大、可维护性和扩展性更好;
极大的减少代码冗余,不需要定义多个重载的方法
弊端:在多态场景下,我们创建了子类的对象,也加载了子类特有的属性和方法,由于声明了父类的引用,导致没有办法直接调用子类的属性和方法;
虚拟方法调用
在 Java 中虚拟方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法;
子类中定义了与父类同名同参数的方法,在多数情况下,将此时父类的方法称为虚方法,父类根据赋给他的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的;
举例:
拓展:
静态链接(或早起绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下调用方法的符号引用转换为直接引用的过程称为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。
动态链接(或晚期绑定)
:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。
成员变量没有多态性
- 若子类重写父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中;
- 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量;
public class Test01 {
int a = 10;
}
-------------------------------
public class Test02 extends Test01{
int a = 20;
}
-------------------------------
public class Test_02 {
public static void main(String[] args) {
Test01 test01 = new Test02();
System.out.println(test01.a); // 获取 Test01 中的实例变量 a ;
System.out.println(((Test02)test01).a); // test01 向下转型 到 Test02 中的 a
System.out.println("----------------------");
Test02 test02 = new Test02();
System.out.println(test02.a); // 获取 Test02 中的实例变量 a ;
System.out.println( ( (Test01) test02) .a); // test02 向上转型 到 Test01 中的 a
}
}
向上转型与向下转型
首先,一个对象在 new 的时候,创建的是什么类型的对象就是什么类型的对象,从头到尾不会改变,但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同;
为什么要类型转换
因为多态,就一定会有把子类对象赋值给父类变量的时候,在编译期间,就会出现类型转换的现象;
但是,使用父类变量接收了子类对象后,我们就不能调用子类有而父类没有的方法,所以,想要调用子类特有的方法,必须做类型转换,使得编译通过;
- 向上转型:当左边变量的类型(父类)> 右边对象/变量的类型(子类),就称为向上转型;
- 编译时按照左边变量的类型处理;
- 运行时,仍然是对象本身的类型,执行的是子类重写的方法体;
- 向下转型:当左边变量的类型(子类)< 右边对象/变量的类型(父类),就成为向下转型;
- 编译时按照左边变量的类型处理;
- 运行时,仍然是对象本身的类型;
- 注意:不是所有通过编译的向下转型都是正确的,可能会发生 ClassCastException ,为了安全,可以通过 instanceof 关键字进行判断;
举例:
public class Person {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat(){
System.out.println("吃饭饭");
}
public void sleep(){
System.out.println("碎觉");
}
}
-------------------------------------------
public class Man extends Person{
public void eat(){
System.out.println("男人"+getName()+"吃饭饭");
}
public void sleep(){
System.out.println("男人"+getName()+"碎觉");
}
public void walk(){
System.out.println("男人"+getName()+"散步");
}
}
-------------------------------------------
public class Woman extends Person{
public void eat(){
System.out.println("女孩"+getName()+"吃饭饭");
}
public void sleep(){
System.out.println("女孩"+getName()+"碎觉");
}
public void makeup(){
System.out.println("女孩"+getName()+"化妆");
}
}
-------------------------------------------
public class TestPerson {
public static void main(String[] args) {
// 没有类型转换
Man man = new Man(); // man 编译和运行时都是 Man 类型
// 向上转型
Person pe = new Man(); // pe 编译时是 Person 类型,运行时是 Man 类型
pe.setName("小明");
pe.eat(); // 调用的是父类 Person 中的方法,但执行的是子类 Man 中重写的方法;
// pe.walk(); // 不能调用父类中没有的方法
System.out.println(" ============================ 分割");
// 向下转型
Man ma = (Man) pe;
System.out.println("ma.name = " + ma.getName());
ma.eat();
ma.walk(); // 可以调用子类扩展的方法
// -----------------
Woman wo = (Woman) pe; // 编译通过,运行报错 ClassCastException ,因为 pe 变量运行时类型是Man ,Man 和 Woman 没有继承关系;
}
}
instanceof 关键字
为了避免运行报 ClassCastException 的发生,Java 提供了 instanceof 关键字,给引用变量做类型的校验;
代码: 对象 a instanceof 数据类型A // 检验对象 a 是否是数据类型 A 的对象,返回值为 boolean 类型
说明:
- 如果用 instanceof 判断返回的是 true ,强制转换后就不会报 ClassCastException 异常;
- 如果对象 a 属于类 A 的子类 B ,a instanceof A 返回的值为 true;
- 要求对象 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误;
举例:
public class Person {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void eat(){
System.out.println("吃饭");
}
public void sleep(){
System.out.println("睡觉");
}
}
---------------------------------------
public class Man extends Person{
public void eat(){
System.out.println("男人"+getName()+"吃饭");
}
public void sleep(){
System.out.println("男人"+getName()+"睡觉");
}
public void walk(){
System.out.println("男人"+getName()+"散步");
}
}
-----------------------------------------
public class Woman extends Person{
public void eat(){
System.out.println("女人"+getName()+"吃饭");
}
public void sleep(){
System.out.println("女人"+getName()+"睡觉");
}
public void makeup(){
System.out.println("女人"+getName()+"化妆");
}
}
-----------------------------------------
public class TestPerson {
public static void main(String[] args) {
Person pe = new Man(); // 多态
pe.setName("张山"); // 设置名字
pe.eat();
Man ma = (Man) pe;
if (ma instanceof Man) { // 判断ma是否属于Man
ma.walk(); // 输出子类Man中有,但是父类Person中没有的方法
}
}
}