云计算百科
云计算领域专业知识百科平台

Java多态基础全面解析,动态绑定,注意事项,向上和向下转型,看完包懂!

前言❤️❤️

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹 多态是Java入门的一个重点,多态是 Java 做项目最核心、最高频、必用的特性之一,项目中基本上到处都是多态,这篇博客就会详细的解析多态的用法和注意事项 请添加图片描述 🎇个人主页:洋不写bug的博客 🎇所属专栏:Java学习之旅,从入门到进阶 🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~

1,多态的简介

多态属于是JavaSE中最难的语法 多态、继承、封装,属于是Java的三大特征

多态简单来说,就是一个对象的引用,具备多种形态 就比如说大明家里养了一只狗叫大黄,大明叫他儿子 大明还有个真正的儿子,小明 有一次,大明的老婆给大明说,给你儿子整点吃的,那这时候究竟谁给狗整吃的,还是给小明整吃的,就要结合上下文的语境来进行确定了

代码如下所示:

package package1;

public class Son {
public void eat(){
System.out.println("Son吃东西");
}
}

package package1;

public class XiaoMing extends Son{
public void eat(){
System.out.println("小明吃炒米饭");
}

}

package package1;

public class Dog extends Son{
public void eat(){
System.out.println("大黄吃狗粮");
}
}

package package1;

public class Test {
public static void main(String[] args) {
Son son = new Son();
son.eat();

son = new XiaoMing();
son.eat();

son = new Dog();
son.eat();

}
}

在Test类中,使用父类的引用指向子类实例,执行结果如下所示: 在这里插入图片描述

son这个引用,可能有多种形态,具体是哪种形态,就需要结合上下文来看了,不同形态执行son.eat(),执行的是不同的逻辑

多态也有其他的例子,如下所示:

package package1;

public class Test {
public static void run(Son son){
son.eat();
}

public static void main(String[] args) {
run(new Son());
run(new XiaoMing());
run(new Dog());
}
}

在这里插入图片描述

这样看起来是在调用父类Son的eat方法,但是实际上执行的逻辑还是取决于animal实际指向的对象是哪个类型的

2,动态绑定

当代码运行到son.eat()的时候,JVM就会在运行过程中,分析son真实指向的对象是啥类型的,进一步调用匹配类型的方法

这样的一个过程就可以称为动态绑定,也就是eat方法执行哪个版本,不是在程序编译时(Java程序在运行前先编译成字节码,编译完成的字节码可以反复执行多次,也可以拷贝大其他机器上执行)就确定的,而是在运行时确定的

和编译操作相关的动作/过程,一般称为是静态的 和运行相关的动作/过程,一般称为是动态的 Java的多态,就是基于动态绑定机制来实现的

3,多态使用的注意事项

在Java中,要实现多态,需要注意以下几点:

  • 必须在继承条件下
  • 子类必须对父类的方法进行重写,子类重写的方法要与父类方法的方法名字、参数列表、返回值类型相容(返回值类型完全相同或者子类方法的返回值类型是父类对应方法的返回值类型的子类)
  • 通过父类的引用调用重写的方法
  • 访问权限不能比父类中被重写的方法的访问权限更低,如果父类方法被public修饰,则子类中重写该方法就不能被声明为protected
  • 父类被static、private修饰的方法,构造方法都不能被重写
  • 重写的方法,推荐使用@Override注解来显示指定,这个注解能帮我们进行一些合法性的校验,例如不小心将方法的名字拼写错了,加了这个注解后就能检验出来
  • 返回值类型相容,这是个比较冷门的语法,一般也用不到,这里铁汁们了解下即可 如下所示,这里子类Cat重写父类Animal的eat方法,子类eat方法返回的是Cat类型的,父类eat方法返回的是Animal类型的,子类方法返回的类型是父类方法返回类型的子类,因此这样也是可行的,如下所示:

    package package2;

    public class Animal {
    public Animal eat(){
    System.out.println("Animal is eating");
    return this;
    }
    }

    package package2;

    public class Cat extends Animal{
    public Cat eat(){
    System.out.println("Cat is eating");
    return this;
    }
    }

    package package2;

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Animal();
    animal.eat();

    animal = new Cat();
    animal.eat();
    }
    }

    在这里插入图片描述

    接下来是注解,当子类重写父类方法的时候,最好在上面加上个@Override注解,标注这个方法是重写的,对方法做一些合法性的校验 例如这里的子类的方法名打错了,这里并不会报错,因为这样只是相当于没有进行方法重写 在这里插入图片描述

    程序运行结果如下图,这时候子类中没有检测出eat方法,就会自动使用父类中的eat方法,就不是我们想调用的效果 在这里插入图片描述

    加了@Override后,这里就会报错,我们看报错信息,这个方法没有重写它的超类(父类又称超类)的方法,就会提醒我们 在这里插入图片描述

    另外,方法重写时,需要我们尽量遵守开闭原则,也就是对于修改关闭,对于扩展开放

    开闭原则的核心就是:对于已经投入的类,尽量不要进行修改,最好的方式是重新定义一个类,来重复其中共性的内容,并且添加或者改动新的内容

    就比如手机通信,现在的新手机都普遍显示地区,但早期的手机只显示号码,那在这个过程中,就不该在原来老的类上进行修改,因为可能还有使用老式手机的人,它的手机上没有这个功能,盲目修改类,就会导致bug 应该新建一个手机类,重写方法,新手机来电显示这个重写的方法即可 在这里插入图片描述

    4,向上转型

    向上转型:父类引用指向子类实例

    这里是以对象本体来看的,对象是子类对象,本来应该使用子类引用的,使用了父类引用,所以说就是向上转型

    前面提到的多态其实都是向上转型 平时使用最多的就是下面这种:

    package package1;

    public class Test {
    public static void main(String[] args) {
    Son son = new Son();
    son.eat();

    son = new XiaoMing();
    son.eat();

    son = new Dog();
    son.eat();

    }
    }

    前面提到的这种写法也是多态(使用父类引用来表示子类对象)

    package package1;

    public class Test {
    public static void run(Son son){
    son.eat();
    }

    public static void main(String[] args) {
    run(new Son());
    run(new XiaoMing());
    run(new Dog());
    }
    }

    包括这种写法,也是向上转型,只要父类引用指向子类对象的,都是向上转型

    public class Test {
    public static Animal createAnimal(){
    return new Cat();
    }

    public static void main(String[] args) {
    Animal animal = createAnimal();
    }
    }

    5,向下转型

    接着是向下转型,向下转型是依赖向上转型的,平常也比较少用,要求父类的引用实际上指向的是子类的实例,并且需要加上显示的转换 向下转型说白了就是父类引用赋值给子类引用

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Cat();
    Cat cat = (Cat) animal;
    }
    }

    下面这种情况就必须使用向下转型,这个铁汁们了解下即可,一般很少这样写代码,我们可以借此了解下编译器的检查规则

    铁汁们看下面这几段代码,猜下这个会报错吗?

    package package2;

    public class Animal {
    public void eat(){
    System.out.println("Animal is eating");
    }
    }

    package package2;

    public class Cat extends Animal{
    public void eat(){
    System.out.println("Cat is eating");
    }

    public void meow(){
    System.out.println("喵喵喵");
    }

    }

    package package2;

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Cat();
    animal.meow();
    }
    }

    其实是会报错的,如下图,这里显示在Animal类里面找不到这个meow()方法 在这里插入图片描述

    可能有的铁汁就会说:不对呀,前面提到的动态规划中说JVM在运行到这里时,会分析这个引用真实指向的对象类型,这里真实指向的对象类型是Cat呀,那为什么会报错在Animal类中找不到这个方法呢?

    其实,这个错误是在编译器编译阶段报的错误,在编译阶段,看到animal就是父类类型的引用,于是尝试在父类类型中找meow(),找不到,就报错了 编译器只做最基本的语法检查,不知道代码的逻辑是怎么样的,也不知道代码是如何执行的,无法确定animal的真是类型,只能把animal当作父类的引用

    那么编译器报错,代码就无法执行了,动态绑定是在代码运行起来进行的

    解决这个问题,就可以依靠向下转型,将父类引用赋值给子类引用,如下所示:

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Cat();
    Cat cat = (Cat)animal;
    cat.meow();
    }
    }

    另外,向下转型的时候,需要确保父类引用确实是指向子类实例的,否则就算强转了,也无法调用里面的方法,如下所示,会报出一个ClassCastException(类型转化异常)

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Animal();
    Cat cat = (Cat)animal;
    cat.meow();
    }
    }

    在这里插入图片描述

    向下转型如果想要避免报出类型转化异常,可以写个if判断一下,利用instaceof判断下animal引用指向的对象是不是Cat类型的,如果是,才进行向下转型 如下所示

    public class Test {
    public static void main(String[] args) {
    Animal animal = new Animal();
    if(animal instanceof Cat){
    Cat cat = (Cat)animal;
    cat.meow();
    }else{
    System.out.println("类型不匹配");
    }
    }
    }

    在这里插入图片描述

    6,多态的优缺点分析

    多态的主要优点如下:

  • 便于针对已有的代码进行扩展,同时不影响旧的逻辑
  • 降低代码的“圈复杂度”
  • 第一点前面已经提到过了,便于代码扩展,这里就主要解析下第二点

    “圈复杂度”就是衡量一个代码/方法,复杂程度的一种指标,把一段代码中出现分支的个数,称为圈复杂度 如果代码是从上到下执行完的,就是最简单的代码,圈复杂度就是1 如果代码中的条件、循环特别多,而且还进行嵌套,那这个代码圈复杂度也就很高,理解起来就有难度

    一般在公司中,要求最好圈复杂度在5以下,最多不超过10

    那如何使用多态,来降低圈复杂度呢,示例如下(Windows计算机可以按win 加上 >键来获取图案):

    package package3;

    public class Shape {
    public void draw(){
    System.out.println("打印形状");
    }
    }

    package package3;

    public class Rect extends Shape{
    @Override
    public void draw(){
    System.out.println("□");
    }
    }

    package package3;

    public class Circle extends Shape{
    @Override
    public void draw(){
    System.out.println("〇");
    }
    }

    package package3;

    public class Flower extends Shape {
    @Override
    public void draw(){
    System.out.println("🌹");
    }
    }

    在这里插入图片描述

    注:在Java中字符串类型就是String(后面会出一期博客来详细解析这个类型),判断字符串是否相同不能用“==”,需要用equals方法来判断

    package package3;

    public class Test {
    public static void main(String[] args) {
    String[] shapes = {"circle","rect","rect","flower"};
    for(String shapeName : shapes){
    if(shapeName.equals("circle")){
    Shape shape = new Circle();
    shape.draw();
    }else if(shapeName.equals("rect")){
    Shape shape = new Rect();
    shape.draw();
    }else if(shapeName.equals("flower")){
    Shape shape = new Flower();
    shape.draw();
    }
    }
    }
    }

    打印结果如下: 代码的圈复杂度就是4,共有4个分支,进行if判断 在这里插入图片描述

    利用多态,就可以把圈复杂度降为1,代码如下所示: 直接搞一个对象数组,然后再遍历整个数组即可,多态是不是非常牛

    public class Test {
    public static void main(String[] args) {
    Shape[] shapes = {new Circle(),new Rect(),new Rect(),new Flower()};
    for(Shape shape : shapes){
    shape.draw();
    }
    }
    }

    其实多态解决的核心问题,本质上是在封装的基础上更进一步了 封装是使用者不知道类的内部实现,就能使用,降低使用者的成本 多态是使用者不光不需要知道类的内部实现,甚至都不需要知道类的真实类型,就可以使用

    那多态有什么缺点呢? 那就是程序在运行过程中,消耗的时间和耗费的空间越来越多,因为动态绑定是需要一定的开销的,但是现在机器的硬件配置相比以前有了很大的提高 因此,这里的缺点基本上可以忽略(像C++就是把效率发挥到极致的语言)

    7,初始化问题

    铁汁们看这样一个程序: 子类重写父类的func()方法,父类在无参构造方法中调用了func()方法,接着在main方法中创建子类对象

    package package4;

    public class Parent {
    public Parent(){
    func();
    }

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

    package package4;

    public class Child extends Parent{
    public Child(){

    }
    private int num = 10;

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

    package package4;

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

    那这里执行main方法后,会打印什么 创建子类对象,在执行子类的构造方法之前,会先执行父类的构造方法,执行 父类的构造方法时,就会触发多态,实际上执行子类的版本 执行结果如下: 在这里插入图片描述

    这里执行的是子类的func方法没错,但是打印出的num的值却是0,这是为什么呢?

    因为此处的打印,是在Parent的构造中调用的,这时候Child里面的属性还没有进行初始化呢,此处看到的0,就是JVM申请内存时,自动填充的0,那这里打印的结果就是错误的

    使用多态的时候,要注意,不要在父类的构造方法中去触发多态,因为父类构造方法执行的时候,子类的属性还没有初始化,此时执行子类的方法,使用子类的属性,也就处于“未初始化”的状态中

    结语💕💕

    到现在,Java的三大特征(多态,封装,继承)已经全部解析完了,这部分东西可能有的东西稍微有点抽象,学起来不知道有什么实用价值,这是很正常的 后面写完抽象类和接口的博客后,博主会写一个简单的图书管理系统的解析博客,在里面会使用各种JavaSE的基础内容,最好的理解方法不是硬记概念,而是在项目代码中理解🐵在这里插入图片描述

    以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java多态基础全面解析,动态绑定,注意事项,向上和向下转型,看完包懂!
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!