写点什么

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

发布于: 2021 年 02 月 10 日
区分重载和重写,轻松掌握 Java 多态

🎓 尽人事,听天命。博主东南大学研究生在读,热爱健身和篮球,正在为两年后的秋招准备中,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上我们一起进步


🎁 本文已收录于 CS-Wiki(Gitee 官方推荐项目,现已 0.9k star),致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习

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 中除了 staticfinal方法(private方法属于 final方法)之外,其他所有方法都是动态绑定。这意味着通常情况下,我们不需要判断动态绑定是否会发生,它是自动发生的。


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

  • 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 数组。

🎉 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学研究生在读,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专注分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操作系统 + Linux)、Java 基础和面试指南的相关原创技术好文。本公众号的目的就是让大家可以快速掌握重点知识,有的放矢。希望大家多多支持哦,和小牛肉一起成长 😃



发布于: 2021 年 02 月 10 日阅读数: 35
用户头像

🦄 公众号『飞天小牛肉』 2021.02.01 加入

🎓 博主东南大学研究生在读,热爱篮球和健身,正在为一年后的秋招准备中,进击的 Javaer,深耕计算机基础和 Java 相关技术栈

评论

发布
暂无评论
区分重载和重写,轻松掌握 Java 多态