零基础 “入坑” Java--- 十一、多态

发布于:2025-07-15 ⋅ 阅读:(13) ⋅ 点赞:(0)


在前两个章节中,我们已经对封装和继承有了深入的了解,本章节我们就来学习面向对象的最后一个重要思想——多态!

一、何为多态

多态,通俗来讲就是多种形态,具体点就是当不同的对象去做同一件事时产生的不同的效果。

比如:当我们想要打印照片时,需要使用到打印机,当使用彩色打印机时,打印出的照片就是彩色的;使用黑白打印机时,打印的照片就是黑白的。

二、多态实现条件

多态是一种非常抽象的思想,要想真正理解多态以及多态的实现条件,我们需要先知道:

1.向上转型
2.子类和父类存在同名的重写(覆盖)方法(即子类对父类中的方法进行重写)。
3.通过父类引用,调用子类对象重写的方法。

满足以上这三点,可以说明发生了动态绑定:

什么是动态绑定,什么是静态绑定?

理解了以上这些,我们就能了解多态。

三、向上转型

语法格式:

父类类型 对象名 = new 子类类型();

class Animal {
    public String name;
    public int age;
    public Animal(String name) {
        this.name = name;
    }
    public void eat() {
        System.out.println(this.name + "吃饭");
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    public void wang() {
        System.out.println("汪汪");
    }
}

我们定义一个动物类,一个狗类,并让狗类继承动物类,向上转型即:父类的引用,引用了子类对象;简单来说,就是把子类对象给父类。需要注意:向上转型是从小范围向大范围进行转换。

public class Test {
    public static void main(String[] args) {
        //方法一
        Dog dog = new Dog("小狗1");
        Animal animal = dog;
        //方法二(简写)
        Animal animal1 = new Dog("小狗2");
    }
}

在上面这段代码中,因为狗类继承了动物类,就发生了向上转型。向上转型不只有一种形式:

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("小狗1");
        test(dog);
    }
    public static void test(Animal animal1) {
        System.out.println("向上转型");
    }
}

在这段代码中也发生了向上转型,test方法的参数为Animal,但是当我们在main方法中调用test方法时,传递的参数为dog,此时代码也能正常运行。

public class Test {
    public static void main(String[] args) {
        Animal animal = test();
    }
    public static Animal test() {
        System.out.println("向上转型");
        return new Dog("小狗");
    }
}

上面这段代码中,也发生了向上转型,我们将test方法的返回值类型定义为Animal类,返回一个Dog类,并用Animal类接收。

总结一下:

可以通过三种方式发生向上转型:
1.直接赋值
2.方法传参
3.返回值

在代码中使用向上转型可以使代码的实现更简单灵活,但是使用向上转型也会受到局限:

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog("小狗");
        animal.wang();
    }
}

在这段代码中,我们使用向上转型实例化了一个对象,当我们想要通过实例化出的对象调用狗类中的方法时,会发现没办法正常调用:
在这里插入图片描述
这就是向上转型的缺点,即:不能调用子类中独有的方法。

四、方法重写

我们增加一个狗类中的成员方法,并尝试通过向上转型实例化出的对象去调用:

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    public void wang() {
        System.out.println("汪汪");
    }
    public void eat() {
        System.out.println(this.name + "吃狗粮");
    }
}
public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog("小狗");
        animal.eat();
    }
}

此时代码可以正常运行,运行结果为:“小狗吃狗粮”。我们刚才明明学习过,发生向上转型时,无法调用子类中独有的方法,此时,却又可以调用了,这是为什么呢?

在这里插入图片描述
我们在Animal类中也存在eat方法,此时子类中的eat方法就相当于对父类的同名方法进行重写,所以,才可以调用eat方法。

重写:也称为覆盖,是子类对父类中 非静态、非private修饰、非final修饰、非构造方法等 的实现过程进行重新编写。重写可以根据子类需要,定义专属于自己的行为。

重写时,需要确保重写的方法:1.方法名相同;2.参数列表相同(参数的类型、顺序、个数等都必须相同);3.返回值相同。满足以上这三点,才会发生方法的重写。

对于重写的方法,可以使用@Override注释显式指定,当我们添加了这行注释之后,编译器就可以帮助我们进行校验:
在这里插入图片描述
在上一章节中,重写toString方法就用到了这一点。


重写注意事项:

1.被private修饰的方法不能进行重写。
2.被static修饰的方法不能进行重写。
3.被final修饰的方法不能进行重写(被final修饰的方法也叫密封方法)。
4.构造方法不能进行重写。
5.方法的返回值可以不同,但必须为父子类关系:

在这里插入图片描述
6.子类中重写的方法的访问权限 不能比 父类中被重写的方法的访问权限低,如:若父类中方法被public修饰,那么当子类重写父类的该方法时,访问修饰限定符就只能为public(访问权限:private < 默认权限 < protected < public)。

重写和重载:
在这里插入图片描述

五、动态绑定和静态绑定

在这里插入图片描述
如果在子类中没有重写eat方法,那么将会正常调用父类自己的eat方法,但是当子类中重写了eat方法之后,就会调用子类中重写的方法,这就是动态绑定。

动态绑定/运行时绑定:需要等到程序运行时,才能确定调用哪个类的方法(可能与编译时认为调用的类的方法不同),如:方法的重写。


静态绑定/编译时绑定:在编译的时候,根据用户传递的参数的类型即可确定调用哪个方法,如:方法的重载。

六、向下转型(instanceof)

我们重新来编写一段代码:

class Animal {
    public String name;
    public int age;
    public Animal(String name) {
        this.name = name;
    }
}
class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    public void wang() {
        System.out.println("汪汪");
    }
}
class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void fly() {
        System.out.println("起飞");
    }
}

我们已经知道,发生向上转型时,无法调用子类中独有的方法:

    public static void main(String[] args) {
        Animal animal = new Dog("小狗");
        animal.wang(); //error
    }

但是,如果我们执意要调用,就可以通过强制类型转换来使用:

    public static void main(String[] args) {
        Animal animal = new Dog("小狗");
        //向下转型
        Dog dog = (Dog)animal;
        dog.wang();
    }

此时,我们就通过向下转型调用了子类中的方法,但是向下转型很不安全,比如:

    public static void main(String[] args) {
        Animal animal = new Dog("小狗");
        //向下转型
        Dog dog = (Dog)animal;
        dog.wang();

        Animal animal1 = new Bird("小鸟");
        //向下转型
        Dog dog1 = (Dog)animal1;
        dog1.wang();
    }

运行这段代码:
在这里插入图片描述
第一次实例化的对象:animal,可以正常使用;第二次实例化出的对象:animal1,会因为强转的类型不匹配而编译异常;但是在编写代码过程中,编译器并没有通过红色波浪线提示错误。因此,在向下转型时,需要进行检查!!

此时,instanceof关键字就可以帮助我们完成检查这个操作:

    public static void main(String[] args) {
        Animal animal = new Dog("小狗");
        //向下转型
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal;
            dog.wang();
        }
        Animal animal1 = new Bird("小狗");
        //向下转型
        if (animal1 instanceof Dog) {
            Dog dog1 = (Dog) animal1;
            dog1.wang();
        } else {
            System.out.println("不安全");
        }
    }

instanceof表达式的返回值为布尔类型,如果表达式结果为true,则代表可以安全转换。

七、多态

class Shape {
    public void draw() {
        System.out.println("画画");
    }
}
class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("矩形");
    }
}
class Cycle extends Shape {
    @Override
    public void draw() {
        System.out.println("圆形");
    }
}
class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("花花");
    }
}
public class Test {
    public static void drawMap(Shape shape) {
        shape.draw();
    }
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        Cycle cycle = new Cycle();
        Flower flower = new Flower();
        drawMap(rectangle);
        drawMap(cycle);
        drawMap(flower);
    }
}

在上面这段代码中,我们就充分利用了多态的思想:
在这里插入图片描述
虽然在drawMap方法中我们只使用了 一个引用 调用了一个方法,但是当我们传递的参数不同时,得出的结果也不同。这是因为引用的对象不同:当我们引用的对象不同,但调用同一个重写的方法,就会产生不一样的表现行为,这种思想就叫做多态!!!

在上面这个例子中,也发生了动态绑定,因此我们说动态绑定是多态的基础。

八、多态优缺点及注意事项

1.优点

  • 可以降低代码的圈复杂度,避免使用大量的if-else语句,使代码的可读性更高。(圈复杂度是一种描述代码复杂程度的方式。)
  • 代码的可扩展能力更强。

2.缺点

  • 使用多态会导致代码的运行效率降低。

3.注意事项

  • 成员变量没有多态性
  • 构造方法没有多态性
  • 应避免在构造方法中调用重写的方法,这会导致程序的运行结果与我们想要的结果不同,如:
class B {
    public B() {
        func();
    }
    public void func() {
        System.out.println("B.func()");
    }
}
class D extends B {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("D.func() " + num);
    }
}
public class Test2 {
    public static void main(String[] args) {
        D d = new D(); //D.func() 0
    }
}

在这段代码中,我们实例化一个D类型的对象,在子类中需要先帮助父类完成构造,因此调用父类B构造方法,B的构造方法中调用了func方法,此时会产生动态绑定,调用D中的func;但是此时D还没开始构造,因此num为未初始化状态,值为0。


Ending。


网站公告

今日签到

点亮在社区的每一天
去签到