43从零开始学Java之一文详解初学者难以理解的多态

news2024/12/23 22:37:16

作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

我们知道,面向对象有三大特征:封装、继承和多态。现在我们已经了解了封装和继承,接下来在本文中,壹哥会给大家讲解多态的内容。在这篇文章中,我们要弄清楚多态的含义、特点、作用,以及如何用代码进行实现。

-----------------------------------------------前戏已做完,精彩即开始---------------------------------------------

全文大约【6000】字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github:

https://github.com/SunLtd/LearnJava

Gitee:

一一哥/从零开始学Java

一. 多态简介

1. 概念

多态(polymorphism)本来是生物学里的概念,表示地球上的生物在形态和状态方面的多样性。

而在java的面向对象中,多态则是指同一个行为可以有多个不同表现形式的能力。也就是说,在父类中定义的属性和方法,在子类继承后,可以有不同的数据类型或表现出不同的行为。这可以使得同一个属性或方法,在父类及其各个子类中,可能会有不同的表现或含义比如针对同一个接口,我们使用不同的实例对象可能会有不同的操作,同一事件发生在不同的实例对象上会产生不同的结果。

当然,如果我们只是看这样干巴巴的概念,可能大家还是有点懵,壹哥给大家举个栗子。

我们都听过“龙生九子”的故事。长子是囚牛,喜欢搞音乐;次子是睚眦,喜欢打架。后面还有喜欢冒险登高的嘲风,爱大喊大叫的蒲牢,喜欢吸烟的狻猊,爱好举重的霸下,好打官司的狴犴,喜欢斯文的负屃,会灭火的螭吻。他们都是龙的儿子,自然也都是龙,但每个龙都有不同的个性和技能。假如有一天玉帝对龙王说,“让你的儿子来给我秀个技能”。大家说这个任务的执行结果会怎么样?这是不是得看龙王让哪个儿子来秀了!如果是让老大来表演,就是演奏音乐;如果是让老二来表演,就是表演打架.....

从这个故事中,我们就可以感受到,九个龙子虽然都继承了共同的父类,但子类在运行某个方法时却可能会有不同的结果,这就是多态!

2. 作用

根据多态的概念可知,多态机制可以在不修改父类代码的基础上,允许多个子类进行功能的扩展。比如父类中定义了一个方法A,有N个子类继承该父类,这几个子类都可以重写这个A方法。并且子类的方法还可以将自己的参数类型改为父类方法的参数类型,或者将自己的返回值类型改为父类方法的返回值类型。这样就可以动态地调整对象的调用,降低对象之间的依存关系,消除类型之间的耦合,使程序有良好的扩展,并可以对所有类的对象进行通用处理,让代码实现更加的灵活和简洁。

3. 分类

Java中的多态,分为编译时多态和运行时多态

  • 编译时多态主要是通过方法的重载(overload)来实现,Java会根据方法参数列表的不同来区分不同的方法,在编译时就能确定该执行重载方法中的哪一个。这是静态的多态,也称为静态多态性、静态绑定、前绑定。但也有一种特殊的方法重写的情况,属于编译时多态。在方法重写时,当对象的引用指向的是当前对象自己所属类的对象时,也是编译时多态,因为在编译阶段就能确定执行的方法到底属于哪个对象。
  • 运行时多态:主要是通过方法的重写(override)来实现,让子类继承父类并重写父类中已有的或抽象的方法。这是动态的多态,也称为”后绑定“,这是我们通常所说的多态性。

一句话,如果我们在编译时就能确定要执行的方法属于哪个对象、执行的是哪个方法,这就是编译时多态,否则就是运行时多态!

4. 特性

根据多态的要求,Java对象的类型可以分为编译类型和运行类型,多态有如下特性:

  • 一个对象的编译类型与运行类型可以不一致;
  • 编译类型在定义对象时就确定了,不能改变,而运行类型却是可以变化的;
  • 编译类型取决于定义对象时 =号的左边运行类型取决于 =号的右边

所以我们在使用多态方式调用方法时,首先会检查父类中是否有该方法,如果没有,则会产生编译错误;如果有,再去调用子类中的同名方法。即编译时取决于父类,运行时取决于子类

5. 必要条件

我们要想实现多态,需要满足3个必要条件:

  • 继承:多态发生在继承关系中,必须存在有继承关系的父类和子类中,多态建立在封装和继承的基础之上;
  • 重写:必须要有方法的重写,子类对父类的某些方法重新定义;
  • 向上转型:就是要将父类引用指向子类对象,只有这样该引用才既能调用父类的方法,又能调用子类的方法。

只有满足了以上3个条件才能实现多态,开发人员也才能在同一个继承结构中,使用统一的代码实现来处理不同的对象,从而执行不同的行为。

二. 多态的实现

1. 实现方式

在Java中,多态的实现有如下几种方式:

  • 方法重载:重载可以根据实际参数的数据类型、个数和次序,在编译时确定执行重载方法中的哪一个。
  • 方法重写:这种方式是基于方法重写来实现的多态;
  • 接口实现:接口是一种无法被实例化但可以被实现的抽象类型,是对抽象方法的集合。定义一个接口可以有多个实现,这也是多态的一种实现形式,与继承中方法的重写类似。

2. 实现过程

2.1 需求分析

现在我们有一个需求:有一个客户要求我们给他生产设备器材,他需要的产品类型比较多,可能要圆形的器材,也可能需要三角形、矩形等各种形状的器材,我们该怎么生产实现?

如果是按照我们之前的经验,可以分别创建圆形类、三角形类、矩形类等,里面各自有对应的生产方法,负责生产出对应的产品。但是如果这样设计,其实不符合面向对象的要求。以后客户可能还会有很多其他的需求,如果针对每一个需求都设计一个类和方法,最终我们的项目代码就会很啰嗦。

实际上,在客户的这些需求中,有很多要求是具有共性的!比如,无论客户需要什么形状的器材,我们都要进行”绘制生产“,在绘制生产的过程中,可能用到的材料都是一样的,无非就是形状不同!就好比生产巧克力,有圆的方的奇形怪状的,不管怎么样,基础原料都是巧克力。既然如此,我们总不能针对每一种形状的器材都从头到尾搞一遍吧?

所以既然它们有很多内容都一样,我们就可以定义一个共同的父类,在父类中完成共性的功能和特征,然后由子类继承父类,每个子类再扩展实现自己个性化的功能。如下图所示:

这样就是符合面向对象特征的代码设计了!接下来壹哥就通过一些代码案例,来给大家演示该如何实现这个需求。

2.2 代码实现

接下来壹哥会采用实现接口的方式来演示多态的代码实现过程。方法重载和方法重写的方式,其实我们在前面的文章中已经有所讲解,这里不再赘述。

2.2.1 定义Shape接口

我们首先定义出一个Shape接口,这个接口就是一个父类。在Java中,子类可以继承父类,也可以实现接口。一个子类只能继承一个父类,但是却可以实现多个接口这些接口,属于是子类的”间接父类“,你可以理解为是子类的”干爹“或者爷爷等祖辈。关于接口的内容,壹哥会在后面的文章中专门讲解,敬请期待哦,此处大家先会使用即可。

/**
 * @author 一一哥Sun
 * 千锋教育
 * 定义一个“图形”接口----属于父类!
 */
public interface Shape {
	//绘制方法。接口中的方法一般没有实现,需要子类进行实现。
	void draw();
}

2.2.2 定义Circle类

定义一个Circle子类,实现Shape接口,注意我们这里使用了implements关键字!

/**
 * @author 一一哥Sun
 * 千锋教育
 * “圆形”类---实现Shape接口,并对接口中的方法进行实现
 */
public class Circle implements Shape{
	@Override
	public void draw() {
		System.out.println("绘制圆形");
	}

    //子类中定义了一个独有的方法。
    //当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,
    //而不能执行子类独有的成员方法。
    public void scroll(){
        System.out.println("圆形类独有的方法");
    }
}

2.2.3 定义Traingle类

然后再定义一个Traingle子类,也实现Shape接口。

/**
 * @author 一一哥Sun
 * 千锋教育
 * “矩形”类---实现Shape接口,并对接口中的方法进行实现
 */
public class Traingle implements Shape{
	@Override
	public void draw() {
		System.out.println("绘制矩形");
	}
}

2.2.4 定义Square类

最后定义一个Square子类,同样实现Shape接口。

/**
 * @author 一一哥Sun
 * 千锋教育
 * “三角形”类---实现Shape接口,并对接口中的方法进行实现
 */
public class Square implements Shape{
	@Override
	public void draw() {
		System.out.println("绘制三角形");
	}
}

2.4.5 定义测试类

父子关系确定好之后,接下来我们再定义一个额外的测试类。在这个测试类中,我们创建出以上三个图形对象。注意,在=等号左侧,变量的类型都是Shape父类;=等号右侧,变量的值是具体的子类!这种变量的定义过程,其实就是符合了多态的第三个必要条件,也就是所谓的向上转型,父类引用指向子类对象

/**
 * @author 一一哥Sun
 * 千锋教育
 */
public class ShapeTest {
	public static void main(String[] args) {
		//多态测试
		Shape shape01=new Circle();
		shape01.draw();
        //当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,
    	//而不能执行子类独有的成员方法。否则在编译阶段就会出现:
        //The method drink() is undefined for the type Father
        //shape01.scroll();
		
		Shape shape02=new Traingle();
		shape02.draw();
		
		Shape shape03=new Square();
		shape03.draw();
	}
}

我们可以看到上述代码,满足了多态的3个必要条件:继承、重新、向上转型!有子类继承父类,有方法重写,有向上转型。而且根据这个案例,我们可以进一步理解多态的含义和特点。在多态中,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法!

本案例最终的执行结果如下图所示:

2.3 结果分析

在上述案例中,我们有如下一些代码:

Shape shape01=new Circle();
//无法确定运行时究竟调用哪个run()方法
shape01.draw();
		
Shape shape02=new Traingle();
shape02.draw();
		
Shape shape03=new Square();
shape03.draw();

上述代码中,我们实际的类型是Circle、Traingle、Square,他们共同的父类,其引用类型是Shape变量。当我们调用shape.draw()时,大家可以想一下,执行的是父类Shape的draw()方法还是具体子类的draw()方法?大多数同学应该能够想出来,执行的应该是具体子类的draw()方法!

基于以上这个案例,我们可以得出一个结论:

Java实例方法的调用,是基于运行时实际类型的动态调用,而非声明的变量类型!通俗地说,就是我们调用的到底是哪个对象的方法,不是由=号左侧声明的引用变量来决定的,而是由=号右侧的实际对象类型来决定的!

这也是多态的一个重要特征!所以我们说在多态中,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法!即只有在运行期,才能动态决定调用哪个子类的方法。这种不确定性的方法调用,究竟有什么作用呢?其实主要就是允许我们能够添加更多类型的子类,实现对父类功能的扩展,而不需要修改父类的代码。

三. 扩展补充

1. 方法重写时的编译时多态

当对象的引用指向的是当前对象所属类的对象,即使是方法重写,依然属于编译时多态。

1.1 定义父类

我们先定义一个Father父类,内部定义一个eat()方法。

/**
 * @author 一一哥Sun 
 * 千锋教育
 * 定义父类
 */
public class Father {
	// 吃
	public void eat() {
		System.out.println("爹吃馒头");
	}
}

1.2 定义子类

接着定义一个Son子类继承Father父类,并重写eat()方法。

public class Son extends Father {
	// 吃
	@Override
	public void eat() {
		// 方法重写时,子类可以对父类的同名方法进行扩展实现,方法体的内容可以和父类中的实现不一样
		System.out.println("儿子吃肉");
	}

	public static void main(String[] args) {
		//创建出父类对象
		Father father=new Father();
		father.eat();
		
		//创建子类对象
		//虽然子类继承了父类,并重写了父类的方法,但对象的引用指向的是当前对象所属类的对象,即son引用指向的是new Son()对象,这也是编译时多态!
		Son son = new Son();
		son.eat();
	}
}

虽然这里的Son子类继承了父类Father,并重写了父类的方法,但对象的引用指向的是当前对象所属类的对象,即son引用指向的是new Son()对象,这也是编译时多态!

2. 实现多态时的若干细节

2.1 定义Father父类

我们定义一个Father父类,类中定义了name属性,成员方法eat(),静态方法play()。

/**
 * @author 一一哥Sun 
 * 千锋教育
 * 定义父类
 */
public class Father {
	// 父类中的成员变量
	String name = "老子";

	// 吃
	public void eat() {
		System.out.println("爹吃馒头");
	}

    // 父类中的静态方法。静态方法不能被重新,只会被子类隐藏!
	public static void play() {
		System.out.println("爹玩球");
	}
}

2.2 定义Son子类

接着再定义一个Son子类,类中定义了同名的name属性和特有的age属性,重写成员方法eat(),特有的drink()方法,并定义一个同名的静态方法play()。

//Son子类
public class Son extends Father {
	// 成员变量--变量隐藏
	// 变量的类型和名称,都与父类中的成员变量相同
	String name = "儿子";
	//子类中独有的属性
	int age = 10;

	// 吃
	@Override
	public void eat() {
		// 方法重写时,子类可以对父类的同名方法进行扩展实现,方法体的内容可以和父类中的实现不一样
		System.out.println("儿子吃肉");
	}

	// 喝---子类新增的方法。
	// 当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,而不能执行子类独有的成员方法。
	public void drink() {
		System.out.println("儿子吃肉");
	}

	// 子类中与父类同名的静态方法,这不是重写,而是子类对父类同名静态方法的隐藏!
	public static void play() {
		System.out.println("儿子玩火");
	}

	public static void main(String[] args) {
		// 当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,而不能执行子类独有的成员方法。
		// Father son=new Son();
		// The method drink() is undefined for the type Father
		// son.drink();
		
		Father son=new Son();
		//当子类和父类有相同属性时,父类会调用自己的属性。
		System.out.println("name="+son.name);//老子
		//当父类引用指向子类对象向上转型时,若父类调用子类特有的属性,在编译时期就会报错:
		//age cannot be resolved or is not a field
		//son.age;

        //虽然我们不建议通过“对象.方法名”的形式来调用类的静态方法,但这么调用也不会出错。
		son.play();//爹玩球
	}
}

2.3 执行结果

上述代码执行结果如下图所示:

根据上述代码的执行结果可知,当父类引用指向子类对象时,父类只能调用执行那些在父类中声明、被子类覆盖的子类方法,而不能执行子类独有的成员方法否则在编译阶段就会出现”The method drink() is undefined for the type Father“异常。

另外当子类和父类有相同属性时,父类会调用自己的属性当父类引用指向子类对象向上转型时,若父类调用子类特有的属性,在编译时期就会报错”age cannot be resolved or is not a field“。

如果Father父类中定义了一个静态方法play(),子类也定义了一个同名的静态方法play(),上述代码中son.play()执行的是Father类中的play()方法。在进行向上转型时,父类引用调用同名的静态方法时,执行的是父类中的方法。这是因为在运行时,虚拟机已经确定了static方法属于哪个类。“方法重写”只适用于实例方法,对静态方法无效。静态方法,只能被隐藏、重载、继承,但不会被重写子类会将父类的静态方法隐藏,但不能覆盖父类的静态方法,所以子类的静态方法体现不了多态,这和子类属性隐藏父类属性一样。

------------------------------------------------​​​​​​​正片已结束,来根事后烟----------------------------------------------

四. 结语

至此,我们就把面向对象的三大特征都学习完毕了,现在你对这三大特征都熟悉了吗?最后我们再来看看多态的要点都有哪些吧:

  • 多态指的是不同子类型的对象,对同一行为作出的不同响应;
  • 实现多态要满足继承、重新、向上转型的条件;
  • 多态分为编译时多态和运行时多态,我们常说的多态是指运行时多态;
  • 方法重载是编译时多态,方法重写是运行时多态,但重写有例外情况;
  • 父类引用指向子类对象时,调用的实例方法是子类重写的方法,父类引用不能调用子类新增的方法和子类特有属性;
  • 父类引用指向子类对象时,父类引用只会调用父类自己的属性和static方法,不会调用子类的;
  • 多态使得代码更加灵活,方便了代码扩展。

另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

五. 配套视频

如果你不习惯阅读技术文章,或是对文中的技术概念不能很好地理解,可以来看看壹哥帮你筛选出的视频教程。与本文配套的Java学习视频,链接如下:

Bilibili External Player

六. 今日作业

1. 第一题

评论区写出封装、继承和多态各自的要求和特性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/644874.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IBM Spectrum LSF 针对要求苛刻、任务关键型计算环境的全面工作负载管理

IBM Spectrum LSF 针对要求苛刻、任务关键型计算环境的全面工作负载管理 亮点 通过卓越的可重复性能加快求解时间; 使用可靠且可扩展的架构管理大量作业; 面向管理员和用户的直观界面提高工作效率; IBM Spectrum LSF 系列是一套完整的工作负载管理解决方案组合 &#xff0…

由于找不到msvcr120.dll,无法继续执行代码,多种解决方法修复这个故障

在使用电脑时,我们常常会遇到各种各样的问题。其中一个比较常见的问题是“由于找不到msvcr120.dll,无法继续执行代码”。这个问题可能会让一些用户感到困惑和无助。那么,究竟什么是msvcr120.dll?它缺失了会有什么后果?如何修复这个…

“拓界·创变”——2023K+全球软件研发行业创新峰会上海站圆满收官

众望所归!6月9-10日,为期两天的K全球软件研发行业创新峰会,于上海明捷万丽大酒店成功举办。本届峰会吸引了来自金融、通信、互联网、消费电子、企服等各行业逾千位技术从业者的参与,活动现场大咖云集、座无虚席,同时也…

10. WebGPU 旋转变换

单位圆是半径为 1.0 的圆。 下图是一个单位圆。 [注释1] 在上图中,当围绕圆拖动蓝色手柄时,X 和 Y 位置会发生变化,代表该点在圆上的位置。且在顶部,Y 为 1,X 为 0。在右侧,X 为 1,Y 为 0。 …

互联网JAVA工程师必背面试题和项目面试通关要点(带答案)

前言 大家好,前几天我将各个大厂的高频考点以及 Java 岗需掌握的核心知识整理出的 Java 核心知识大全分享给了大家,今天为了能让看过这份资料的小伙伴更加好地去理解和灵活运用。小编今天将把前段时间和那份资料一块儿收集的 1100 道 Java 岗大厂面试真…

你知道Unity IoC Container是如何创建对象的吗?

ty是微软P&P推出的一个开源的IoC框架,最新的官方版本是2.0。Unity之前的版本建立在一个称为ObjectBuild的组件上,熟悉EnterLib的读者,相信对ObjectBuild不会感到陌生。对于EnterLib 5.0之前的版本,ObjectBuild可以说是所有App…

Linux:初识

1 诞生 创始人:林纳斯 托瓦兹 时 间:1991年,上大学期间 2 Linux的组成 linux主要由内核、系统级应用程序组成 图2-1 Linux系统结构(图自黑马程序员课程) 内核:对硬件进行调度,比如调度CPU、内…

CSDN原力值解析:功能作用、获取方法、积分对应等级关系详解

开篇声明:本博主非官方人员,也是非所谓的 CSDN 内容合伙人,所以本文博主站在一个中立的角度、以博主自身的主观观点的角度来解答 CSDN 的一个叫做 “原力值” 东西,本文欢迎随时在留言区讨论,但是拒绝硬杠,…

推挽电路应用

1. 推挽电路,常用上N下P型 2. 输出与输入同相: 输入低电平,输出低电平,输出受输入限制,输入输出同相 输入高电平,输出低电平,输出受输入限制,输入输出同相 3. N管烧坏原因分析 当…

华为诺亚极简网络,靠13层就拿下83%精度(附源代码)

点击蓝字 关注我们 关注并星标 从此不迷路 计算机视觉研究院 公众号ID|计算机视觉研究院 学习群|扫码在主页获取加入方式 论文地址:https://arxiv.org/pdf/2305.12972.pdf 项目代码:https://github.com/huawei-noah/VanillaNet ht…

【裸机开发】系统时钟分路 PLL2_PFDx、PLL3_PFDx 配置实验(二)—— 寄存器分析篇

上一篇介绍了 系统时钟的来源、时钟树 以及 PLL1 分路的配置步骤。我们注意到,PLL2、PLL3是固定倍频,无法修改,但是他们下分的 PFDx 分路是可以修改的。只不过我们在初始化的时候,依然按照官方给定的频率进行初始化。 目录 一、了…

2023年,程序员如何构建持续增长的被动收入?

大家好,我是晓衡! 我致力于帮助开发者通过技术,实现被动收入,并利用复利效应获得收益最大化。 经过 6 年的探索和实践,取得了一定的成效。 01 起点 我是从 2018 年 7 月份开始全职做这个 Cocos 内容公众号,…

room数据库升级

直接添加表字段,不升级会报异常如上图。 1.表字段或减少表字段 2.增加表 如上等情况需要升级数据库 方法如下(以添加表字段为例): Database( entities [XXInfo::class], version 2 // 旧版本为1 ) init { …

Linux:grep、wc命令和管道符

1、grep命令:从文件中根据关键词过滤文件行,语法: grep [-n] 关键词 文件路径 选项-n是可选的,表示在结果中输出匹配到的行的行号关键词:必填,表示要过滤的关键词文件路径:必填,表示…

海思如何编译驱动

一、安装海思的SDK 这一步在海思的说明文档中有,运行sdk.unpack 二、配置内核 进入osdrv/opensource/kernel/ 根据里面的说明文档,没有内核就去下载内核,如果在www.kernel.org网站下载内核十分慢,推荐使用镜像列表下载&#xff0c…

web动画(Animation) - 过渡效果transition

内容目录: 过渡动画;过渡动画的属性; 一、过渡动画 过渡(transition)作用:- 通过过渡可以指定一个属性发生变化时的切换方式- 通过过渡可以创建一些非常好的效果,提升用户的体验现在我们通过一…

Qt6之样式表2

一、样式选择器类型 一般情况下组件最终都会产生父子、子孙等关系,此时样式选择器类型非常重要,它决定着你的类型是否互相直接独立、互相影响和便捷高效的快速设置样式。 1、如下图常见的一个工具栏,切换时鼠标划过是灰色,选中后是…

QTYX量化系统实战案例分享|每日增量涨停股池叠加形态分析-202306第三弹

前言 “实战案例分享系列”是和大家分享一些股票量化分析工具QTYX在实战中的应用案例(包括失败的案例),这样能够帮助大家更好地去理解QTYX中的功能设计,也能更好地帮助大家搭建出属于自己的量化交易系统。 关于QTYX的使用攻略可以…

给大家分享下什么是「API接口」

作为产品经理,了解清楚接口的相关知识是非常有必要的,毕竟总不想被技术大佬认为自己时什么都不懂的需求搬运工。那就往下看下去吧 -----拿去餐馆吃饭的例子 模拟网络请求流程 厨师是后端提供API,服务员是前端请求调用API,我们是用…

Pyside6-第八篇-QLabel文本标签

本篇是Pyside6的第八篇,本章来看看另一个知识点。 文本标签QLabel。它不仅仅可以用于文本,还可以显示富文本和图像。它是一个多功能的小部件,可以根据需要显示不同类型的内容。 部分源码 class QLabel(QFrame):"""QLabel(self…