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.避免在构造方法中调用重写的方法(建议)
相关推荐
Maỿbe10 分钟前
阻塞队列的学习以及模拟实现一个阻塞队列
java·数据结构·线程
we风1 小时前
【SpringCache 提供的一套基于注解的缓存抽象机制】
java·缓存
趙卋傑3 小时前
网络编程套接字
java·udp·网络编程·tcp
两点王爷3 小时前
Java spingboot项目 在docker运行,需要含GDAL的JDK
java·开发语言·docker
万能螺丝刀16 小时前
java helloWord java程序运行机制 用idea创建一个java项目 标识符 关键字 数据类型 字节
java·开发语言·intellij-idea
zqmattack6 小时前
解决idea与springboot版本问题
java·spring boot·intellij-idea
Hygge-star6 小时前
【Java进阶】图像处理:从基础概念掌握实际操作
java·图像处理·人工智能·程序人生·职场和发展
Honmaple6 小时前
IDEA修改JVM内存配置以后,无法启动
java·ide·intellij-idea
小于村6 小时前
pom.xml 文件中配置你项目中的外部 jar 包打包方式
xml·java·jar
Tom@敲代码6 小时前
Java构建Tree并实现节点名称模糊查询
java