Java入门——继承和多态(中)

组合

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.

public class Student { ... 
} public class Teacher { ... 
} public class School { public Student[] students; public Teacher[] teachers; 
} 

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段. 这是我们设计类的一种常用方式之一.

  • 组合表示 has - a 语义
  • 在刚才的例子中, 我们可以理解成一个学校中 "包含" 若干学生和教师.
  • 继承表示 is - a 语义
  • 在上面的 "动物和猫" 的例子中, 我们可以理解成一只猫也 "是" 一种动物.

多态

向上转型

在刚才的例子中, 我们写了形如下面的代码

Bird bird = new Bird("圆圆"); 

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

Bird bird = new Bird("圆圆"); 
Animal bird2 = bird; // 或者写成下面的方式 
Animal bird2 = new Bird("圆圆");

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

为啥叫 "向上转型"?

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

向上转型发生的时机:

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

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

方法传参

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 (父类) 的实例.

方法返回

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 的实例.

动态绑定

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

// 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("谷子"); } 
} // 执行结果 
我是一只小动物 
圆圆正在吃谷子 
我是一只小鸟 
扁扁正在吃谷子
  • animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向 Bird 类型的实例.
  • 针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而 animal2.eat() 实际调用了子类的方法.

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引 用指向的是父类对象还是子类对象.

这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

方法重写

针对刚才的 eat 方法来说: 子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖

1. 重写和重载完全不一样. 不要混淆

 2. 普通方法可以重写, static 修饰的静态方法不能重写.

3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.

4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

如果将子类的 eat 改成 private

// 编译出错 
Error:(8, 10) java: com.csdn.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
eat(java.lang.String) 正在尝试分配更低的访问权限; 以前为public

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

// Bird.java 
public class Bird extends Animal { @Override private void eat(String food) { ... } 
} 

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

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

事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述 的是相同的事情, 只是侧重点不同.

理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态的形式来设计程序了. 我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.

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 Test { public static void main(String[] args) { Shape shape1 = new Flower(); Shape shape2 = new Cycle(); Shape shape3 = new Rect(); drawMap(shape1); drawMap(shape2); drawMap(shape3); } // 打印单个图形 public static void drawShape(Shape shape) { shape.draw(); } 
} 

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

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当 前的 shape 引用指向的是哪个类型(哪个子类)的实例.

此时 shape 这个引用调用 draw 方法可能会有多种不同的表现 (和 shape 对应的实例相关), 这种行为就称为 多态.

使用多态的好处是什么?

1) 类调用者对类的使用成本进一步降低.

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

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

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

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 分支语句, 代码更简单.

public static void drawShapes() { // 我们创建了一个 Shape 对象的数组. Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()}; for (Shape shape : shapes) { shape.draw(); } 
} 

3) 可扩展能力更强.

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

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

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

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

向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象.

相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途.

// 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 + "正在飞"); } 
} 
Animal animal = new Bird("圆圆"); 
animal.eat("谷子"); // 执行结果 
圆圆正在吃谷子 

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

animal.fly(); // 编译出错 
找不到 fly 方法 

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

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

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

编译器检查有哪些方法存在, 看的是 Animal 这个类型

执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.

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

// (Bird) 表示强制类型转换 
Bird bird = (Bird)animal; 
bird.fly(); // 执行结果 
圆圆正在飞 

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

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 实例, 再来转换

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { Bird bird = (Bird)animal; bird.fly(); 
} 

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

super 关键字

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

super 表示获取到父类实例的引用.

涉及到两种常见用法.

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

public Bird(String name) { super(name); 
} 

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

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 关键字, 才是调用父类的方法.

总结

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

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

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

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

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

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

相关文章

2023愚人杯 )————被遗忘的反序列化

<?php# 当前目录中有一个txt文件哦 error_reporting(0); show_source(__FILE__); include("check.php");class EeE{public $text;public $eeee;public function __wakeup(){if ($this->text "aaaa"){echo lcfirst($this->text);}}public functi…

量化交易:日内网格交易策略.md

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 本文将详细介绍日内网格交易策略的原理&#xff0c;并结合Python代码示例&#xff0c;展示如何在掘金平台上实现这一策略。 策略原理 日内网格交易策略的核心思想是在一天的交易时间内&#xff0c;通过设置多个买卖…

【谷粒商城】02安装和配置git

1.下载和安装git 地址&#xff1a;https://git-scm.com/download/win 傻瓜式安装 2.配置git DELLLJL MINGW64 ~/Desktop $ git config --global user.name "yufuabu"DELLLJL MINGW64 ~/Desktop $ git config --global user.email "463999534qq.com"DELL…

详解循环队列——链表与数组双版本

前言&#xff1a;本节内容主要是讲解循环队列。 在本篇中会讲到两个版本——数组版本、链表版本。本篇内容适合正在学习数据结构队列章节或者已经学过队列但对循环队列感觉模糊的友友们 。 首先先来看一下什么是循环队列 什么是循环队列 因为是刚开始讲解&#xff0c; 所以我们…

git知识总结

要知道 本地回退后&#xff0c;反悔了&#xff0c;可以恢复。前提是已经提交了&#xff0c;提交了就丢不了。 git reflog git reset --hard commitId 以前git push不让推&#xff0c;就是没有对应关系。第一次推要setxxx参数。 前奏 设置用户名和邮箱&#xff0c;设置错…

回溯之组合总和II

上一篇文章使用回溯解决了组合总和I&#xff0c;这次使用回溯解决组合总和II&#xff0c;下面先给出回溯的模板代码。 private void backtracking(参数1,参数2,...){if(递归终止条件){收集结果;return;}for(遍历集合){处理;backtracking(参数1,参数2,...); // 递归;回溯;} }组…

5. FactoryTalk View SE -- 模拟量趋势记录

step1&#xff1a; 在项目列表下找到数据记录–数据记录模型–新建。 step2&#xff1a;更改描述、文件标识符、存储格式。 step3&#xff1a;更改文件缓存路径。 step4&#xff1a;更改缓存文件保存的周期。 step5&#xff1a;5s保存一次数据。 step6&#xff1a;添加标…

从零开始搭建Springboot项目脚手架2:配置文件、返回值、日志等

1、多个环境与配置文件 2、统一返回值 返回值包括两种场景&#xff1a;正常controller的返回、异常发生之后返回 正常controller的返回&#xff1a;通过在controller的默认返回Response实现 异常发生之后返回&#xff1a;通过全局异常处理统一捕获返回 首先创建类StatusCode…

[Spring Cloud] (7)gateway防重放拦截器

文章目录 简述本文涉及代码已开源Fir Cloud 完整项目防重放防重放必要性&#xff1a;防重放机制作用&#xff1a; 整体效果后端进行处理 后端增加防重放开关配置签名密钥 工具类防重放拦截器 前端被防重放拦截增加防重放开关配置请求头增加防重放签名处理防重放验证处理函数bas…

打造清洁宜居家园保护自然生态环境,基于YOLOv5全系列【n/s/m/l/x】参数模型开发构建自然生态场景下违规违法垃圾倾倒检测识别系统

自然生态环境&#xff0c;作为我们人类赖以生存的家园&#xff0c;其健康与否直接关系到我们的生活质量。然而&#xff0c;近年来&#xff0c;一些不法分子为了个人私利&#xff0c;在河边、路边等公共区域肆意倾倒垃圾&#xff0c;严重破坏了环境的健康与平衡。这种行为不仅损…

18.04版本的ubuntu没有连接网络的图标(坑人版)

以下更新内核别看&#xff0c;因为后面安装驱动报一堆错误!!! 不升级内核成功方法跳转连接&#xff1a;https://blog.csdn.net/weixin_53765004/article/details/138771613?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%2213877…

单调栈问题

原理 单调栈的核心原理是&#xff1a;在栈内保持元素的单调性&#xff08;递增或递减&#xff09; 单调递增栈&#xff1a; 用于处理“下一个更小的元素”问题。当新元素比栈顶元素小或等于时&#xff0c;直接入栈&#xff1b;否则&#xff0c;一直从栈顶弹出元素&#xff0c…

OBS直播二次开发_OBS直播软件介绍

OBS工作室版 免费且开源的用于视频录制以及直播串流的软件。 下载以在Windows, Mac以及Linux上简单且快速的开始串流。 功能 实时高性能的视频/音频捕捉与混合,以及无限的场景模式使您可以通过自定义实现无缝转换。为视频源设计的滤镜例如图片蒙版,色彩校正,色度/色彩键控…

软件体系结构风格

目录 一、定义 二、.经典软件体系结构风格&#xff1a; 1.管道和过滤器 2.数据抽象和面向对象系统 3.基于事件系统&#xff08;隐式调用&#xff09; 4.分层系统 5.仓库 6.C2风格 7.C/S 8.三层C/S 9.B/S 题&#xff1a; 一、定义 软件体系机构风格是描述某一特定应用…

通过内网穿透实现远程访问个人电脑资源详细过程(免费)(NatApp + Tomcat)

目录 1. 什么是内网穿透 2. 内网穿透软件 3. NatApp配置 4. 启动NatApp 5. 通过内网穿透免费部署我们的springboot项目 通过内网穿透可以实现远程通过网络访问电脑的资源&#xff0c;本文主要讲述通过内网穿透实现远程访问个人电脑静态资源的访问&#xff0c;下一章节将讲…

C语言/数据解构——(随即链表的复制)

一.前言 嗨嗨嗨&#xff0c;大家好久不见。已经有好几天没更新了。今天我们就分享一道链表题吧——随即链表的复制https://leetcode.cn/problems/copy-list-with-random-pointer废话不多说&#xff0c;让我们直接开始今天的题目分享吧。 二.正文 1.1题目描述 他和单链表不同…

华为OD机试 - 求幸存数之和(Java 2024 C卷 100分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

机器学习面试篇

如何理解机器学习数据集的概念 数据集是机器学习的基础&#xff0c;它包括了用于训练和测试模型所需的数据。数据集通常以矩阵的形式存在&#xff0c;其中每一行代表一个样本&#xff08;或实例&#xff09;&#xff0c;每一列代表一个特征&#xff08;或属性&#xff09;。…

JVM从1%到99%【精选】-类加载子系统

目录 1.类的生命周期 1.加载 2.连接 3.初始化 2.类的加载器 1.类加载器的分类 2.双亲委派机制 3.面试题&#xff1a;类的双亲委派机制是什么&#xff1f; 4.打破双亲委派机制 1.类的生命周期 类加载过程&#xff1a;加载、链接&#xff08;验证、准备、解析&a…

数据与结构--堆

堆 堆的概念 堆&#xff1a;如果有一个关键码的集合K{k0,k1,k2,…,kn-1}&#xff0c;把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中&#xff0c;并满足ki<k2i1且ki<k2i2&#xff08;或满足ki>k2i1且ki>k2i2&#xff09;&#xff0c;其中i0,1,2,…