mami mami
Home
知识体系总览
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • MySQL
  • Java 基础
  • Java 集合
  • Java 并发
  • Java 虚拟机
  • Spring
  • Redis
  • 唠唠嗑儿
  • 读书笔记
归档
GitHub (opens new window)
Home
知识体系总览
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • MySQL
  • Java 基础
  • Java 集合
  • Java 并发
  • Java 虚拟机
  • Spring
  • Redis
  • 唠唠嗑儿
  • 读书笔记
归档
GitHub (opens new window)
  • 万物皆对象
  • HelloWorld
  • 运算符与控制流
  • 对象初始化和清理
  • Java是如何实现封装的-图解访问修饰符
  • 为什么说要慎用继承-优先使用组合
  • 区分重载和重写-轻松掌握Java多态
    • 0. 前言
    • 1. 什么是多态
    • 2. 多态发生的三个必要条件
    • 3. 多态是如何发生的
    • 4. 重载和重写
    • 5. main 方法是否可以重载
  • 如何理解Java中接口存在的意义
  • 面试常备-字符串三剑客String-StringBuffer-StringBuilder
  • String的不可变真的是因为final吗
  • 小米二面-送分题-反射的原理作用以及优劣
  • 字节一面-作文题-动态代理具体是怎么实现
  • 深入理解Java数组
  • Java中为什么设计了包装类
  • Java基础
itslae
2022-03-20
目录

区分重载和重写-轻松掌握Java多态

# Java 小白成长记 · 第 7 篇「区分重载和重写,轻松掌握 Java 多态」


# 0. 前言

陆续讲完了抽象、封装和继承,终于到多态了,说实话这四个概念的耦合性比较高,明明每个概念都很清晰明了,但是拆分开来就确实不太好写,每篇写之前都需要构思很久。OK,本章写完面向对象的基本特征就全部结束喽,作为开胃小菜,接下来才是 Java 漫漫征程的开始。

# 1. 什么是多态

多态的概念并不难,并且在实际编码中可以说是最最高频使用率。多态就是使得同一个行为具有多个不同表现形式或形态的能力。举个形象点的例子:对于 “打印” 这个行为,使用彩色打印机 “打印” 出来的效果就是彩色的,而使用黑白打印机 “打印” 出来的效果就是黑白的。我们就称 “打印” 这个行为是多态的,彩色打印效果和黑白打印效果就是 “打印” 这个行为的两个不同的表现形式。

还可以这样理解,同一个行为在不同的对象上会产生不同的结果。再举个形象点的例子:比如我们按下 F1 键这个行为:如果当前在 Word 下弹出的就是 Word 帮助和支持;在 Windows 下弹出的就是 Windows 帮助和支持。

# 2. 多态发生的三个必要条件

先看下面这段代码,首先,我们有一个基类 Shape,三个子类,并且都重写了基类的 draw 方法:

class Shape {
    void draw() {}
}
 
class Circle extends Shape {
    void draw() {
        System.out.println("Circle.draw()");
    }
}
 
class Square extends Shape {
    void draw() {
        System.out.println("Square.draw()");
    }
}
 
class Triangle extends Shape {
    void draw() {
        System.out.println("Triangle.draw()");
    }
}

下面这几行代码就充分体现了多态性:

Shape circle = new Circle();
Shape square = new Square();
Shape triangle = new Triangle();

大家应该不会太陌生,就是上篇文章继承中提到的向上转型,没错,它就是多态的体现。同样的一个 draw 方法,在这三个不同的对象上产生了三种不同的行为,多态在此体现的淋漓尽致。

这里需要注意的是,当使用多态方式调用方法时,编译器会首先检查父类中是否有该方法,如果没有,则编译错误;如果父类中有该方法,并且被子类重写,就会调用子类的这个方法;如果父类的方法没有被子类重写,就会调用父类的方法。

Shape circle = new Circle();
circle.draw(); // 调用的是 Circle 的 eat

简单来说:当父类引用变量指向子类对象后(多态),只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。

结合上述这段简单的代码,我们总结一下多态产生的必要条件:

  • 1)继承
  • 2)重写
  • 3)父类引用指向子类对象:Parent p = new Child();

# 3. 多态是如何发生的

❓ 那么,多态到底是如何发生的?编译器是如何知道父类 Shape 引用指向的是 Circle 而不是 Triangle 或 Square 呢?

首先,我们需要了解静态绑定和动态绑定的概念。什么是绑定?将一个方法调用同一个方法主体关联起来的过程就称作绑定。

若绑定发生在程序运行前,叫做静态绑定,也称前期绑定。你可能从来没有听说这个术语,因为它是面向过程语言不需选择默认的绑定方式,例如在 C 语言中就只有前期绑定这一种方法调用。

那么对于这段代码:

Shape circle = new Circle();
Shape square = new Square();
circle.draw(); 

Shape 即引用类型在编译期可知,不会被改变,而 Circle 作为实例对象类型在运行期才可知,可能会发生变化。所以如果使用前期绑定,在运行之前,编译器只知道有一个 Shape 引用,它无法得知究竟会调用哪个方法。

解决方法就是动态绑定 Dynamic Binding,在运行时根据对象的类型自动的进行绑定,所以动态绑定也称运行时绑定。动态绑定是多态的基础。

注意:Java 中除了 static和 final方法(private方法属于 final方法)之外,其他所有方法都是动态绑定。这意味着通常情况下,我们不需要判断动态绑定是否会发生,它是自动发生的。

final 和 static 关键字后续会单独出文章讲解,此处就笼统的概述一下为什么这两个关键字修饰的方法是静态绑定的:

  • final 不允许方法重写,而多态发生的条件之一就是重写,所以 final 方法会在编译期间就进行绑定,即静态绑定
  • static 方法是类直接拥有的的,与该类的任何一个对象都无关(该类的所有对象共同维护),所以也是静态绑定

# 4. 重载和重写

重载和重写在之前的文章中都说过了,此处正好借着多态这个主题将这两个容易混淆的概念总结一波。

方法的重写 Overriding 和重载 Overloading 都是是 Java 多态性的表现。

🔸 1)方法重写是父类与子类之间多态性的表现。其子类和父类方法的名字相同,参数个数相同,返回类型也相同,并且子类的访问权限不能比父类的严格,比如父类是 public,那么子类也只能是 public,不能比 public 更严格。也就是说,方法重写,只有方法体是不一样的,访问权限可以有限制的修改。

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

🚨 其实,上面说的返回类型完全相同并不严格正确。下面我们来解释一下。

首先,我们需要知道方法的名字和参数列表称为方法的签名。例如,draw() 和 draw(String) 是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖/重写了超类中的这个相同签名的方法。

不过,返回类型不是签名的一部分, 因此,在覆盖/重写方法时, 一定要保证返回类的兼容性。 允许子类将覆盖方法的返回类型定义为原返回类型的子类型。

例如, 假设 Shape 类有

class Shape {
    public Shape draw() {
    	......
    }
}

在后面的子类 Circle 中, 可以按照如下所示的方式覆盖这个方法

class Circle extends Shape {
    public Circle draw() {
        ......
    }
}

用专业术语来说,这两个 draw 方法具有可协变的返回类型。

🔸 2)方法重载并非多态的必要条件,不过可以理解成某个类的多态性的表现。所谓方法重载,就是一个类中定义了多个方法名相同,但是参数的数量或者类型不同。方法的返回类型和访问权限可以任意修改,不以它俩作为方法重载的标志。

class Circle extends Shape {
    public void draw() {
        System.out.println("Circle.draw()");
    }
    
    public void draw(int i) {
        System.out.println("Circle.draw()" + i);
    }
}

总结一下方法重载和重写:

方法重载 方法重写
方法名 相同 相同
参数列表 必须不同 必须相同
返回类型 可以不同 子类方法的返回类型可以是原父类方法返回类型的子类型
访问修饰符 可以不同 子类不能做更严格的限制(可以降低限制)

再附一张经典网图

# 5. main 方法是否可以重载

IBM 早些年出过这方面的题,考倒了一片人。首先,答案是肯定的,main 既然作为一个方法,那它当然可以被重载。

但是,如果是作为程序的入口,那么 main 函数只有一种写法,Java 虚拟机在运行的时候只会调用带有参数是 String 数组的那个 main() 方法,而其他重载的写法虚拟机是不认的,只能人为的调用。

举个例子:

class Test {
	public static void main(String[] args) {
		main(1);
	}
	public static void main(int i) {
		System.out.println("重载的 main 方法");
	}
}

该程序运行结果如下:

重载的 main 方法

可以看出第一个 main 方法正常调用了重载的第二个 main 方法,即 main 方法能够被完美重载。但是程序的入口仍然是第一个 main 方法即参数为 String 数组。

上次更新: 2022/06/13, 02:15:48
为什么说要慎用继承-优先使用组合
如何理解Java中接口存在的意义

← 为什么说要慎用继承-优先使用组合 如何理解Java中接口存在的意义→

最近更新
01
器件课程
06-13
02
上传github
06-13
03
研究生
06-03
更多文章>
Theme by Vdoing | Copyright © 2019-2022 itslae| 心之所愿,素履以往
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式