【Java】—— 多态

1.多态

1.1多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同 的状态。

1.2 多态的实现

在java中要实现多态,必须要满足如下几个条件,缺一不可:

  1. 必须在继承体系下
  2. 子类必须要对父类中方法进行重写
  3. 通过父类的引用调用重写的方法

多态体现:在代码运行时,当传递不同类对象时,会调用对应类中的方法。

//父类
public class School {
    public void eat(){
        System.out.println("在学校吃东西");
    }
}
//子类小王
public class Xiaowang extends School{
    public void eat(){
        System.out.println("小王在吃泡面");
    }
}
//子类小明
public class Xiaoming extends School{
    public void eat(){
        System.out.println("小明在吃拌面");
    }
}
//测试类
public class Test {
    public static void main(String[] args) {
        School school = new School();
        school.eat();
        
        school = new  Xiaoming();
        school.eat();
        
        school = new Xiaowang();
        school.eat();
    }
}

这就是多态(一个引用,有多种形态,具体的指向,需要结合上下文来判断)

如果是这样的写法就算不上多态了,因为已经涉及了多个引用

因为 Xiaoming 和 Xiaowang 都是 school 的子类,使用父类的引用,指向一个子类的实例,这种语法是完全可以的

一般多态会采用下列写法

public class Test {
    public static void eat(School school){
        school.eat();
    }
    public static void main(String[] args) {
        eat(new School());
        eat(new Xiaowang());
        eat(new Xiaoming());
    }
}

看起来是调用了父类的 School 的 eat ,实际上是取决于 school 实际指向的对象

还可以通过将子类的引用放到父类的数组中进行调用

public class Test {
    public static void main(String[] args) {
        School[] schools = {new Xiaowang(),new Xiaoming()};
        for (School school : schools){
            school.eat();
        }
    }
}

1.3 重写(override)

上文中 Xiaoming 和 Xiaowang 类中的 eat 就是对 School 中的 eat 方法进行了重写 

1.3.1 方法重写的规则

  • 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
  • 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为 protected
  • 父类被 static、private 修饰的方法、构造方法都不能被重写。
  • 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验. 例如不小心 将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法 构成重写.

1.3.2 动态绑定、静态绑定

当代码执行到 school.eat 时,就会在运行过程中,分析 school 真实指向的对象是什么类型,进一步去调用匹配的类型的方法,这就是动态绑定

动态绑定:在程序运行时确定具体调用哪个方法

静态绑定:在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。

1.3.3 重写的设计原则(开闭原则)

  对于一个已有代码进行开发,如果直接修改旧的类的逻辑,风险是很大的,无法准确判断旧的类都在哪些地方被用到了,贸然修改就可能把已有的功能改坏了,所以应该旧的类不变,基于旧的类创建新的子类,通过重写的方式插入新的逻辑

修改过的代码要能和之前的情况兼容

1.4 向上转型和向下转型

1.4.1 向上转型

父类引用指向子类实例

方式1

School school = new Xiaoming();

方式2(方法传参)

public class Test {
    public static void func(School school){
        school.eat();
    }
    public static void main(String[] args) {
        func(new Xiaowang());
    }
}

方式3(返回表达式 )

public class Test {
    public static School createSchool(){
        return new Xiaoming();
    }
    
    public static void main(String[] args) {
        School school = createSchool();
    }
}

Java中的多态都要基于这个展开

1.4.2 向下转型

父类引用赋值给子类引用

public class Test {
    public static void main(String[] args) {
        //向下转型,需要先创建一个父类的引用,这个父类引用指向子类实例
        School school = new Xiaoming();
        //向下转型依赖向上转型,相当于把向上转型的引用,转回来
        Xiaoming xiaoming = (Xiaoming) school;
    }
}

必须要求父类引用实际上指向的是子类实例,并且需要显示加上类型转换

当我们在子类创建了一个独有的方法

public class Xiaoming extends School{
    public void sleep(){
        System.out.println("小明在睡觉");
    }
}

虽然 School 确实是执行子类的实例,但是类型确实父类的类型,编译过程中是找不到 sleep 这个方法的

这里编译器编译的时候,看到的 School 就是父类类型的引用,尝试在父类中找 sleep ,没找到所以报错了,在编译器编译阶段是不知道 School 指向的真实类型是什么

JVM 运行的时候,就能够知道 School 是指向 Xiaoming 类型的引用,此时调用 eat 就会触发 动态绑定,找到对应的子类来执行

这就是编译和运行的本质区别

public class Test {
    public static void main(String[] args) {
        School school = new Xiaoming();
        Xiaoming xiaoming = (Xiaoming) school;
        xiaoming.sleep();
    }
}

进行完向下转型就可以调用 子类的特定方法 了

进行向下转型时需要确保当前这里的父类引用确实是指向子类实例的,如果不是就算强转也无法调用里面的方法

//错误代码
public class Test {
    public static void main(String[] args) {
        School school = new School();
        Xiaoming xiaoming = (Xiaoming) school;
        xiaoming.sleep();
    }
}

类型转换可能会导致 ClassCastException

为了避免这种异常可以显示使用 instanceof 关键字进行类型判断 

public class Test {
    public static void main(String[] args) {
        School school = new School();
        if (school instanceof Xiaoming) {
            Xiaoming xiaoming = (Xiaoming) school;
            xiaoming.sleep();
        }
    }
}

instanceof:判断某个引用是否是指向某个实例的类型

只有满足条件才会执行,可以避免出现这种异常

1.5 多态的优缺点

优点:

1、降低代码的 “圈复杂度”

圈复杂度:是衡量一个 代码/方法 复杂程度的一种指标,如果这个代码就是从上到下直接执行完就是最简单的代码,当代码中条件和循环越来越多,代码理解起来就更费劲。将一段代码中出现的分支的个数,称为 “圈复杂度”。

2、方便针对已有的代码进行拓展,同时不影响旧的逻辑

缺点:(代码的运行效率降低)

1.属性没有多态性

当父类和子类都有同名的属性时,通过父类引用,只能引用父类自己的成员属性

2.构造方法没有多态性

父类构造方法执行时,子类的属性还没有初始化,此时执行子类的方法,使用子类的属性就是 “未初始化” 的属性

public class Parent {
    public Parent(){
        //构造方法
        //这里就是一个语法的坑
        //尝试在构造方法中,调用被重写的方法
        func();
    }

    public void func(){
        System.out.println("Parent.func()");
    }
}

public class Child extends Parent{
    public Child(){
    }
    private int num = 10;

    public void func(){
        System.out.println("Child.func(): num =" + num);
    }
}

public class Test {
    public static void main(String[] args) {
        Child child = new Child();
    }
}

在 new Child 时,会执行 Child 构造方法之前,先执行父类的构造

父类构造会调用func,就会触发多态,实际上调用了子类的 func 版本

此处的打印是在 Parent 的构造中调用的,此时 Child 里面的属性还没有初始化所以打印出了0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值