前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java的多态

Java的多态

作者头像
鲜于言悠
发布2024-05-31 12:51:05
750
发布2024-05-31 12:51:05
举报
文章被收录于专栏:c/c++的学习笔记c/c++的学习笔记
Java的多态

  • 前言
  • 一、多态是什么
  • 二、向上转型
    • 简介
    • 为什么叫 "向上转型"
    • 向上转型发生的时机
      • 方法传参
      • 方法返回
    • 什么是UML图
  • 三、动态绑定
  • 四、方法重写
    • 关于重写的注意事项
    • 方法权限示例
      • 将子类的 eat 改成 private
    • 重载和重写的区别
  • 五、整合上述方法使用多态来设计程序
    • 打印多种形状
    • 举例理解多态
    • 使用多态的好处是什么
      • 类调用者对类的使用成本进一步降低.
      • 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
        • 什么叫 "圈复杂度"
      • 可扩展能力更强
  • 六、向下转型
    • 注意事项
  • 七、super 关键字
    • 使用了 super 来调用父类的构造器
    • 使用 super 来调用父类的普通方法
  • 八、多态中可能会出现的问题
    • 在构造方法中调用重写的方法
    • 结论
  • 九、总结

前言

推荐一个网站给想要了解或者学习人工智能知识的读者,这个网站里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏网站,请点击下方链接查看。 https://www.captainbed.cn/f1

Java的多态指的是允许将父类对象设置为与子类对象相等的技术,允许将子类对象当作父类对象使用,运行时才确定实际调用的方法。


一、多态是什么

Java的多态是指对象在被声明为父类类型时,可以以其子类类型的实例来引用。简单来说,多态就是同一个方法在不同的对象上产生不同的行为。

多态的实现机制是通过继承和方法重写来实现的。当一个父类引用指向一个子类对象时,父类引用可以调用子类重写的方法,而不是父类中定义的方法。这样就可以实现同一个方法在不同对象上产生不同的行为。

多态的好处是增加了代码的可维护性和扩展性。通过使用多态,可以在不修改已有代码的情况下,通过添加新的子类来扩展程序的功能。

二、向上转型

简介

代码语言:javascript
复制
class Animal {
    public String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat(String food) {
        System.out.println(this.name + "正在吃" + food);
    }
}

class Cat extends Animal {
    public Cat(String name) {
        // 使用 super 调用父类的构造方法. 
        super(name);
    }
}

class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }

    public void fly() {
        System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
    }
}

public class Test{
    public static void main(String[] args) {
        Cat cat = new Cat("小黑");
        cat.eat("猫粮");
        Bird bird = new Bird("圆圆");
        bird.fly();
    }
} 

在上述示例代码中,我们写了形如下面的代码

代码语言:javascript
复制
Bird bird = new Bird("圆圆"); 

这个代码也可以写成这个样子

代码语言:javascript
复制
Bird bird = new Bird("圆圆"); 
Animal bird2 = bird; 
 
// 或者写成下面的方式 
Animal bird2 = new Bird("圆圆"); 

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.

向上转型这样的写法可以结合 is - a 语义来理解.

例如, 我让我媳妇去喂圆圆, 我就可以说, “媳妇你喂小鸟了没?”, 或者 "媳妇你喂鹦鹉了没?

因为圆圆确实是一只鹦鹉, 也确实是一只小鸟

为什么叫 “向上转型”

在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方.

所以我们就称为 “向上转型” , 表示往父类的方向转.

在这里插入图片描述
在这里插入图片描述

向上转型发生的时机

  • 直接赋值
  • 方法传参
  • 方法返回

直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别.

方法传参
代码语言:javascript
复制
public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird("圆圆");
        feed(bird);
    }

    public static void feed(Animal animal) {
        animal.eat("谷子");
    }
} 
 
// 执行结果 
圆圆正在吃谷子 

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例.

方法返回
代码语言:javascript
复制
public class Test {
    public static void main(String[] args) {
        Animal animal = findMyAnimal();
    }

    public static Animal findMyAnimal() {
        Bird bird = new Bird("圆圆");
        return bird;
    }
} 

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.

什么是UML图

UML(统一建模语言)图是一种用于可视化和描述软件系统设计的图形化表示方法。它使用一系列图表来表示系统的不同方面,如结构、行为和交互。UML图可以帮助开发人员和设计师更好地理解系统的需求和设计,并促进团队之间的沟通和协作。

常见的UML图包括:

  1. 类图:描述系统中的类、接口和它们之间的关系。类图用于表示系统的静态结构。
  2. 用例图:描述系统的功能需求和用户之间的交互。用例图用于表示系统的功能和行为。
  3. 序列图:描述系统中不同对象之间的交互和消息传递顺序。序列图用于表示系统的动态行为。
  4. 活动图:描述系统中的业务流程和活动。活动图用于表示系统的工作流程和行为。
  5. 状态图:描述系统中对象的不同状态和状态之间的转换。状态图用于表示系统的状态和行为。
  6. 组件图:描述系统中的组件和它们之间的关系。组件图用于表示系统的组件结构和依赖关系。

这些UML图形可以与文本描述和说明配合使用,以更清晰地描述和设计软件系统。

三、动态绑定

当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?

对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志.

代码语言:javascript
复制
// Animal.java 
public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
    }
}

// Bird.java 
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }

    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
}

// Test.java 
public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Animal("圆圆");
        animal1.eat("谷子");
        Animal animal2 = new Bird("扁扁");
        animal2.eat("谷子");
    }
}

// 执行结果 
我是一只小动物
        圆圆正在吃谷子
我是一只小鸟
        扁扁正在吃谷子 

此时, 我们发现:

  • animal1animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向Bird 类型的实例.
  • 针对 animal1animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而animal2.eat() 实际调用了子类的方法.

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

四、方法重写

针对刚才的 eat 方法来说:

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).

关于重写的注意事项

  • 重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
  • 普通方法可以重写, static 修饰的静态方法不能重写.
  • 重写中子类的方法的访问权限不能低于父类的方法访问权限.
  • 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

方法权限示例

将子类的 eat 改成 private
代码语言:javascript
复制
// Animal.java 
public class Animal {
    public void eat(String food) { 
 ...
    }
}

// Bird.java 
public class Bird extends Animal {
    // 将子类的 eat 改成 private 
    private void eat(String food) { 
 ...
    }
} 
 
// 编译出错 
Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
eat(java.lang.String) 
 正在尝试分配更低的访问权限; 以前为public 

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.

代码语言:javascript
复制
// Bird.java 
public class Bird extends Animal {
    @Override
    private void eat(String food) { 
 ...
    }
}

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写.

我们推荐在代码中进行重写方法时显式加上 @Override 注解.

重载和重写的区别

在这里插入图片描述
在这里插入图片描述

五、整合上述方法使用多态来设计程序

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.

我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.

打印多种形状

代码语言:javascript
复制
class Shape {
    public void draw() {
        // 啥都不用干
    }
}

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

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

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

/我是分割线// 

// Test.java 
public class Test1 {
    public static void main(String[] args) {
        Shape shape1 = new Flower();
        Shape shape2 = new Cycle();
        Shape shape3 = new Rect();
        drawShape(shape1);
        drawShape(shape2);
        drawShape(shape3);
    }
    // 打印单个图形
    public static void drawShape(Shape shape) {
        shape.draw();
    }
} 

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的.

当类的调用者在编写 drawShape 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态.

举例理解多态

多态顾名思义, 就是 “一个引用, 能表现出多种不同形态”

举个具体的例子. 某人家里养了两只鹦鹉(圆圆和扁扁)和一个小孩(核弹). 我媳妇管他们都叫 “儿子”. 这时候我对我媳妇说, “你去喂喂你儿子去”. 那么如果这里的 “儿子” 指的是鹦鹉, 我媳妇就要喂鸟粮; 如果这里的 “儿子” 指的是核弹, 我媳妇就要喂馒头.

那么如何确定这里的 “儿子” 具体指的是啥? 那就是根据我和媳妇对话之间的 “上下文”.

代码中的多态也是如此. 一个引用到底是指向父类对象, 还是某个子类对象(可能有多个), 也是要根据上下文的代码来确定.

使用多态的好处是什么

类调用者对类的使用成本进一步降低.
  • 封装是让类的调用者不需要知道类的实现细节.
  • 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.

因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.

这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.

能够降低代码的 “圈复杂度”, 避免使用大量的 if - else

例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:

代码语言:javascript
复制
public static void drawShapes() {
    Rect rect = new Rect();
    Cycle cycle = new Cycle();
    Flower flower = new Flower();
    String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};

    for (String shape : shapes) {
        if (shape.equals("cycle")) {
            cycle.draw();
        } else if (shape.equals("rect")) {
            rect.draw();
        } else if (shape.equals("flower")) {
            flower.draw();
        }
    }
}

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.

代码语言:javascript
复制
public static void drawShapes() {
    // 我们创建了一个 Shape 对象的数组. 
    Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
            new Rect(), new Flower()};
    for (Shape shape : shapes) {
        shape.draw();
    }
} 
什么叫 “圈复杂度”

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

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构.

不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .

可扩展能力更强

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

代码语言:javascript
复制
class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("△");
    }
} 

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

六、向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途.

代码语言:javascript
复制
// Animal.java 
public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
    }
}

// Bird.java 
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }

    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }

    public void fly() {
        System.out.println(this.name + "正在飞");
    }
} 

接下来是我们熟悉的操作

代码语言:javascript
复制
Animal animal = new Bird("圆圆"); 
animal.eat("谷子"); 
 
// 执行结果 
圆圆正在吃谷子

接下来我们尝试让圆圆飞起来

代码语言:javascript
复制
animal.fly(); 
 
// 编译出错 
找不到 fly 方法 

注意事项

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.

虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.

对于 Animal animal = new Bird("圆圆") 这样的代码,

  • 编译器检查有哪些方法存在, 看的是 Animal 这个类型
  • 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.

那么想实现刚才的效果, 就需要向下转型.

代码语言:javascript
复制
// (Bird) 表示强制类型转换 
Bird bird = (Bird)animal; 
bird.fly(); 
 
// 执行结果 
圆圆正在飞 

但是这样的向下转型有时是不太可靠的. 例如

代码语言:javascript
复制
Animal animal = new Cat("小猫"); 
Bird bird = (Bird)animal; 
bird.fly(); 
 
// 执行结果, 抛出异常 
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird 
 at Test.main(Test.java:35) 

animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的. 运行时就会抛出异常

所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换

代码语言:javascript
复制
Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { 
 Bird bird = (Bird)animal; 
 bird.fly(); 
}

instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.

七、super 关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字.

super 表示获取到父类实例的引用. 涉及到两种常见用法

使用了 super 来调用父类的构造器

代码语言:javascript
复制
public Bird(String name) { 
 super(name); 
} 

使用 super 来调用父类的普通方法

代码语言:javascript
复制
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }

    @Override
    public void eat(String food) {
        // 修改代码, 让子调用父类的接口. 
        super.eat(food);
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
} 

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法.

注意 superthis 功能有些相似, 但是还是要注意其中的区别.

在这里插入图片描述
在这里插入图片描述

八、多态中可能会出现的问题

在构造方法中调用重写的方法

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

代码语言:javascript
复制
class B {
    public B() {
        // do nothing 
        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 Test {
    public static void main(String[] args) {
        D d = new D();
    }
}

// 执行结果 
D.func() 0 
  • 构造 D 对象的同时, 会调用 B 的构造方法.
  • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

结论

“用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

九、总结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.

另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.

  • C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
  • Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java的多态
  • 前言
  • 一、多态是什么
  • 二、向上转型
    • 简介
      • 为什么叫 “向上转型”
        • 向上转型发生的时机
          • 方法传参
          • 方法返回
        • 什么是UML图
        • 三、动态绑定
        • 四、方法重写
          • 关于重写的注意事项
            • 方法权限示例
              • 将子类的 eat 改成 private
            • 重载和重写的区别
            • 五、整合上述方法使用多态来设计程序
              • 打印多种形状
                • 举例理解多态
                  • 使用多态的好处是什么
                    • 类调用者对类的使用成本进一步降低.
                    • 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
                    • 可扩展能力更强
                • 六、向下转型
                  • 注意事项
                  • 七、super 关键字
                    • 使用了 super 来调用父类的构造器
                      • 使用 super 来调用父类的普通方法
                      • 八、多态中可能会出现的问题
                        • 在构造方法中调用重写的方法
                          • 结论
                          • 九、总结
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档