JAVA SE(9)——多态

1.多态的概念&作用

多态 (Polymorphism)是面向对象编程的三大基本特性之一(封装和继承已经讲过了),它允许不同类的对象对同一消息做出不同的响应。具体来说,多态允许基类/父类的引用 指向派生类/子类的对象 (向上转型),并通过该引用调用子类中重写的方法,从而实现不同的行为

2.实现多态的条件

在Java中,要实现多态必须满足以下条件,缺一不可:

  • 1.在继承体系下
  • 2.父类引用指向子类对象(向上转型)
  • 3.子类必须对父类中的可重写方法进行重写
  • 4.通过父类引用调用子类中重写的方法

在实现多态之前,我们必须先搞清楚向上转型和重写是什么

3.向上转型

3.1 概念

向上转型是面向对象编程中的一种类型转换机制,它允许将子类的对象引用转换为父类的引用。这种类型转换是安全的,隐式的

3.2 语法格式

父类类型 变量名 = new 子类对象

java 复制代码
public class Animal {
    public int age;
    public String name;
}
public class Dog extends Animal {
    public String color;
}
public class Test {
    public static void main(String[] args) {
        Animal dog = new Dog();
    }
}

在上述代码中,Dog类继承自Animal类,在实例化Dog对象的时候,创建了一个父类类型 的引用(变量)来指向该对象,这就是向上转型,下面我画个图来表示父类引用和子类对象在内存中的关系图:

实现向上转型有三种方式:

(1)直接赋值:

java 复制代码
Animal dog = new Dog();

(2)方法传参:

java 复制代码
public class Test {
    public static void func(Animal a) {
        a.eat();
    }
    public static void main(String[] args) {
        Dog dog = new Dog();
        func(dog);
    }
}

(3)方法返回值:

java 复制代码
public class Test {
    public static Animal func(String value) {
        if (value.equals("cat")) {
            return new Cat();
        }else if (value.equals("dog")) {
            return new Dog();
        }else {
            return null;
        }
    }
    public static void main(String[] args) {
        Animal animal = func("cat");
    }
}

注意:当发生向上转型后,父类引用无法访问子类中原有的属性和方法,只能访问从父类中继承而来的属性和方法,也就是说父类引用的访问范围如下:

那么,在已经发生向上转型后,如何才能访问子类原有的属性和方法呢?

有两个办法:

  • 1.向下转型
  • 2.使用多态性

4.向下转型

4.1 如何使用向下转型

通过显式地将父类引用转换为子类引用,可以访问子类原有的属性和访问

java 复制代码
public class Animal {
    public int age;
    public String name;
}
public class Dog extends Animal {
    public String color;
}
public class Test {
    public static void main(String[] args) {
        //向上转型
        Animal dog = new Dog();
        System.out.println(dog.age = 10);
        System.out.println(dog.name = "Dog");
        //向下转型
        Dog dog1 = (Dog) dog;
        System.out.println(dog1.color = "白色");
    }
}

允许结果:

10

Dog

白色

4.2 向下转型存在的风险

注意:
向下转型在**编译**时是允许的,但如果没有正确地检查对象的实际类型,运行时可能会抛出ClassCastException异常。这是因为父类引用可能实际上引用的是父类本身或其他子类的对象,而不是目标子类的对象

java 复制代码
public class Animal {
    public int age;
    public String name;
}
//Dog类继承Animal
public class Dog extends Animal {
    public String color;
}
//Cat类继承Animal
public class Cat extends Animal {
    public int weight;
}
public class Test {
    public static void main(String[] args) {
        //向上转型
        Animal dog = new Dog();
        System.out.println(dog.age = 10);
        System.out.println(dog.name = "Dog");
        //向下转型
        Dog dog1 = (Dog) dog;
        System.out.println(dog1.color = "白色");
        //错误的向下转型
        Cat cat = (Cat) dog;
        cat.weight = 10;
    }
}

上述代码中,Dog和Cat都继承自Animal,但是在发生向下转型的时候,使用Cat类型的引用指向Dog对象。这在编译时是不会报错的,因为Cat和Dog同属于Animal的子类,但是当程序运行之后就会抛出ClassCastException异常

因为向下转型本质上是强制类型转换,将Animal dog引用(指向Dog对象的引用)强转为Dog类型是允许的,因为Dog类型的引用指向Dog对象很合理;但是,如果将Animal dog引用(指向Dog对象的引用)强转为Cat类型,这是不允许的,因为Cat类型的引用无法指向Dog对象。这就相当于无法将boolean类型强转为int类型

4.3 instanceof运算符

如果用户错误地使用向下转型,在编译阶段是不容易被察觉出来的,只有运行阶段才会报错。如果程序运行起来之后才发现错误,可能已经带来了损失,所以为了规避这一情况,Java引入了instanceof运算符来帮助用户检测错误

java 复制代码
public class Animal {
    public int age;
    public String name;
}
//Dog类继承Animal
public class Dog extends Animal {
    public String color;
}
//Cat类继承Animal
public class Cat extends Animal {
    public int weight;
}
public class Test {
    public static void main(String[] args) {
        //向上转型
        Animal dog = new Dog();
        System.out.println(dog.age = 10);
        System.out.println(dog.name = "Dog");
        //向下转型
        if (dog instanceof Dog) {
            //dog instanceof Dog 为true
            //说明该向下转型是安全的
            Dog dog1 = (Dog) dog;
        }
        if (dog instanceof Cat) {
            //dog instanceof Cat 为false
            //说明该向下转型是不安全的,不执行if语句中的代码
            Cat cat = (Cat) dog;
        }
    }
}

5.重写

5.1 概念

**重写(Override)**是面向对象编程中的一个重要概念,它允许子类提供一个与父类中已定义的方法具有相同名称、参数列表和返回类型的方法。重写使得子类能够改变或扩展父类方法的实现

5.2 语法格式

java 复制代码
public class Animal {
    public int age;
    public String name;
    //父类的eat方法
    public Animal eat() {
        System.out.println("Animal is eating.");
        return null;
    }
}
public class Dog extends Animal {
    public String color;
    //重写父类的eat方法
    @Override
    public Dog eat() {
        System.out.println("Dog is eating.");
        return null;
    }
}

重写的规则:

  • 1.子类重写父类的方法时,必须和父类的方法名、参数列表保持一致
  • 2.返回值类型可以不一样,但必须具有父子关系。上述代码中,子类重写的方法的返回值类型可以是被重写方法返回值类型的子类
  • 3.子类中重写方法的访问权限必须大于等于父类中被重写的方法
  • 4.父类中被static、final、private修饰的方法以及构造方法不能被重写
  • 5.重写方法是可以借助@Override注解。虽然这个注解不影响方法的实现逻辑,但是可以帮助我们进行合法性检查。比如:如果不小心将方法名写错了(写成了ate),注解就会帮我们报错
  • 6.重写只针对方法,和属性/变量无关

5.3 重写和重载的区别

特性 重载(Overload) 重写(Override)
定义 在同一个类中,方法名相同但参数列表不同 在子类中重新定义一个与父类中已定义的方法具有相同签名、返回值一样或这具有父子关系的方法
目的 提供方法的不同实现,以适应不同的参数类型和数量 改变或扩展父类方法的实现
访问限定修饰符 无要求 子类方法的访问权限必须大于等于父类方法
调用时机 编译时根据参数列表来确定调用哪个方法 运行时确定
关键字 无关键字 可以借助@Override注解

5.4 动态绑定

上面讲向上转型时讲过,当父类引用指向子类对象时,该父类引用只能访问子类中继承自父类的属性和方法。那么,当向上转型和方法重写同时发生时,会碰撞出怎么样的火花呢?

java 复制代码
public class Animal {
    public int age;
    public String name;
    //父类的eat方法
    public Animal eat() {
        System.out.println("Animal is eating.");
        return null;
    }
}
public class Dog extends Animal {
    public String color;
    //重写父类的eat方法
    @Override
    public Dog eat() {
        System.out.println("Dog is eating.");
        return null;
    }
}
public class Test {
    public static void main(String[] args) {
        Animal dog = new Dog();
        dog.eat();
    }
}

在上述代码中,引用指向子类的对象,再通过该父类引用去调用父类中被重写的eat方法,按照我们之前学习的知识,此时运行结果应该是:Animal is eating.,但实际上:

运行结果:

Dog is eating.

这里可以得出一个结论:当父类引用去调用父类中被重写的方法时,真正被调用的方法是子类中重写的方法。

注意:

  • 1.具体最终调用哪个方法,在编译时无法确定,在运行时根据对象的实际类型来确定

    在编译时认为调用Animal中的eat方法,但是根据运行结果来看,实际上调用的是子类中重写的eat方法
  • 2.在运行时确定调用的具体方法,这称之为动态绑定/后期绑定,也是实现多态的基础

6. 多态

6.1 多态的具体实现

java 复制代码
public class Animal {
    public int age;
    public String name;
    //父类的eat方法
    public Animal eat() {
        System.out.println("Animal is eating.");
        return null;
    }
}
public class Dog extends Animal {
    public String color;
    //重写父类的eat方法
    @Override
    public Dog eat() {
        System.out.println("Dog is eating.");
        return null;
    }
}
public class Cat extends Animal {
    public int weight;
    //重写父类的eat方法
    @Override
    public Cat eat() {
        System.out.println("Cat is eating.");
        return null;
    }
}
public class Test {
    public static void func(Animal animal) {
        animal.eat();
    }
    public static void main(String[] args) {
        Dog dog = new Dog();
        Cat cat = new Cat();
        func(dog);
        func(cat);
    }
}

运行结果:

Dog is eating.

Cat is eating.
这里我们再回顾一下多态的概念:多态允许基类/父类的引用 指向派生类/子类的对象 (向上转型),并通过该引用调用子类中重写的方法,从而实现不同的行为。

在上述func方法中,同样是通过animal形参来调用eat方法,两个运行的结果发生了变化,这个过程就叫做多态

6.2 使用多态降低圈复杂度

什么叫圈复杂度?

圈复杂度是一种描述一段代码复杂程度的方式。如果一段代码平铺直叙,那么就比较容易理解;如果一段代码使用很多的条件分支或者循环语句,就认为代码理解起来比较复杂。

我们可以简单地举个例子,一层if-else语句表示一圈复杂度,如果一段代码的圈复杂度太高,就需要考虑重新构建该代码的结果。一般来说,圈复杂度不应该超过10。

现在我们需要打印多个图形

java 复制代码
public class Shape {
    public void draw() {
        System.out.println("Shape");
    }
}
public class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("flower");
    }
}
public class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("rect");
    }
}
public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("circle");
    }
}

下面是不使用多态的代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        String[] array = new String[]{"flower","rect","circle"};
        //
        for(String cur : array) {
            if(cur.equals("flower")) {
                new Flower().draw();
            }else if(cur.equals("rect")) {
                new Rect().draw();
            }else {
                new Circle().draw();
            }
        }
    }
}

下面是使用多态的代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Shape[] array = new Shape[]{new Flower(), new Rect(), new Circle()};
        for(Shape cur : array) {
            cur.draw();
        }
    }
}

6.3 避免在构造方法中调用重写的方法

实际执行结果:

Son0

期望执行结果:

Son10

上述代码中,在父类的构造方法中调用func方法,此时调用的是子类中重写的func方法。在子类的func方法中访问了实例成员变量age,但是此时age还没有开始初始化。因为此时仍然处于父类的构造方法中,而子类的实例成员变量初始化发生在父类构造方法结束之后。解决办法有两个:

  • 1.将age变量使用static修饰(不建议)
  • 2.避免在构造方法中调用重写的方法(建议)
相关推荐
LiLiYuan.9 分钟前
关于Stream
java·开发语言·windows·python
hzj61 小时前
GateWay使用
java·spring·gateway
苹果酱05675 小时前
【Azure Redis】Redis导入备份文件(RDB)失败的原因
java·vue.js·spring boot·mysql·课程设计
每次的天空5 小时前
Android第六次面试总结之Java设计模式(二)
android·java·面试
JAVA百练成神6 小时前
Java引用RabbitMQ快速入门
java·rabbitmq·java-rabbitmq
元亓亓亓6 小时前
Java后端开发day42--IO流(二)--字符集&字符流
java·开发语言
一刀到底2116 小时前
idea内存过低 设置 Maximum Heap Size 终极解决方案
java·ide·intellij-idea
振鹏Dong7 小时前
Java基础问题——八股盛宴 | 3w字分享
java
JH30737 小时前
【SpringBoot】SpringBoot中使用AOP实现日志记录功能
java·spring boot·后端
学java的cc8 小时前
Spring AI快速入门
java·大数据·spring