八、复用(2)

本章概要

  • 结合组合和继承
    • 保证适当的清理
    • 名称隐藏
  • 组合与继承的选择
  • protected
  • 向上转型
    • 再论组合和继承

结合组合与继承

你将经常同时使用组合和继承。下面的例子展示了使用继承和组合创建类,以及必要的构造函数初始化:

class Plate {Plate(int i) {System.out.println("Plate constructor");}
}class DinnerPlate extends Plate {DinnerPlate(int i) {super(i);System.out.println("DinnerPlate constructor");}
}class Utensil {Utensil(int i) {System.out.println("Utensil constructor");}
}class Spoon extends Utensil {Spoon(int i) {super(i);System.out.println("Spoon constructor");}
}class Fork extends Utensil {Fork(int i) {super(i);System.out.println("Fork constructor");}
}class Knife extends Utensil {Knife(int i) {super(i);System.out.println("Knife constructor");}
}// A cultural way of doing something:
class Custom {Custom(int i) {System.out.println("Custom constructor");}
}public class PlaceSetting extends Custom {private Spoon sp;private Fork frk;private Knife kn;private DinnerPlate pl;public PlaceSetting(int i) {super(i + 1);sp = new Spoon(i + 2);frk = new Fork(i + 3);kn = new Knife(i + 4);pl = new DinnerPlate(i + 5);System.out.println("PlaceSetting constructor");}public static void main(String[] args) {PlaceSetting x = new PlaceSetting(9);}
}

在这里插入图片描述

尽管编译器强制你初始化基类,并要求你在构造函数的开头就初始化基类,但它并不监视你以确保你初始化了成员对象。注意类是如何干净地分离的。你甚至不需要方法重用代码的源代码。你最多只导入一个包。(这对于继承和组合都是正确的。)

保证适当的清理

Java 没有 C++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。原因可能是,在 Java 中,通常是忘掉而不是销毁对象,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。初始化和清理章节提到,你无法知道垃圾收集器何时会被调用,甚至它是否会被调用。因此,如果你想为类清理一些东西,必须显式地编写一个特殊的方法来完成它,并确保客户端程序员知道他们必须调用这个方法。最重要的是——正如在"异常"章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。

请考虑一个在屏幕上绘制图片的计算机辅助设计系统的例子:

class Shape {Shape(int i) {System.out.println("Shape constructor");}void dispose() {System.out.println("Shape dispose");}
}class Circle extends Shape {Circle(int i) {super(i);System.out.println("Drawing Circle");}@Overridevoid dispose() {System.out.println("Erasing Circle");super.dispose();}
}class Triangle extends Shape {Triangle(int i) {super(i);System.out.println("Drawing Triangle");}@Overridevoid dispose() {System.out.println("Erasing Triangle");super.dispose();}
}class Line extends Shape {private int start, end;Line(int start, int end) {super(start);this.start = start;this.end = end;System.out.println("Drawing Line: " + start + ", " + end);}@Overridevoid dispose() {System.out.println("Erasing Line: " + start + ", " + end);super.dispose();}
}public class CADSystem extends Shape {private Circle c;private Triangle t;private Line[] lines = new Line[3];public CADSystem(int i) {super(i + 1);for (int j = 0; j < lines.length; j++)lines[j] = new Line(j, j * j);c = new Circle(1);t = new Triangle(1);System.out.println("Combined constructor");}@Overridepublic void dispose() {System.out.println("CADSystem.dispose()");// The order of cleanup is the reverse// of the order of initialization:t.dispose();c.dispose();for (int i = lines.length - 1; i >= 0; i--) {lines[i].dispose();}super.dispose();}public static void main(String[] args) {CADSystem x = new CADSystem(47);try {// Code and exception handling...} finally {x.dispose();}}
}

在这里插入图片描述

这个系统中的所有东西都是某种 Shape (它本身是一种 Object,因为它是从根类隐式继承的) 。除了使用 super 调用该方法的基类版本外,每个类还覆盖 dispose() 方法。特定的 Shape 类——CircleTriangleLine,都有 “draw” 构造函数,尽管在对象的生命周期中调用的任何方法都可以负责做一些需要清理的事情。每个类都有自己的 dispose() 方法来将非内存的内容恢复到对象存在之前的状态。

main() 中,有两个关键字是你以前没有见过的,在"异常"一章之前不会详细解释: tryfinallytry 关键字表示后面的块 (用花括号分隔 )是一个受保护的区域,这意味着它得到了特殊处理。其中一个特殊处理是,无论 try 块如何退出,在这个保护区域之后的 finally 子句中的代码总是被执行。(通过异常处理,可以用许多不同寻常的方式留下 try 块。)这里,finally 子句的意思是,“无论发生什么,始终调用 x.dispose()。”

在清理方法 (在本例中是 dispose() ) 中,还必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。首先,按与创建的相反顺序执行特定于类的所有清理工作。(一般来说,这要求基类元素仍然是可访问的。) 然后调用基类清理方法,如这所示。

在很多情况下,清理问题不是问题;你只需要让垃圾收集器来完成这项工作。但是,当你必须执行显式清理时,就需要多做努力,更加细心,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果调用,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 finalize()

名称隐藏

如果 Java 基类的方法名多次重载,则在派生类中重新定义该方法名不会隐藏任何基类版本。不管方法是在这个级别定义的,还是在基类中定义的,重载都会起作用:

class Homer {char doh(char c) {System.out.println("doh(char)");return 'd';}float doh(float f) {System.out.println("doh(float)");return 1.0f;}
}class Milhouse {
}class Bart extends Homer {void doh(Milhouse m) {System.out.println("doh(Milhouse)");}
}public class Hide {public static void main(String[] args) {Bart b = new Bart();b.doh(1);b.doh('x');b.doh(1.0f);b.doh(new Milhouse());}
}

在这里插入图片描述

Homer 的所有重载方法在 Bart 中都是可用的,尽管 Bart 引入了一种新的重载方法。正如你将在下一章中看到的那样,比起重载,更常见的是覆盖同名方法,使用与基类中完全相同的方法签名和返回类型。否则会让人感到困惑。

你已经看到了Java 5 **@Override **注解,它不是关键字,但是可以像使用关键字一样使用它。当你打算重写一个方法时,你可以选择添加这个注解,如果你不小心用了重载而不是重写,编译器会产生一个错误消息:

// reuse/Lisa.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// {WillNotCompile}class Lisa extends Homer {@Override void doh(Milhouse m) {System.out.println("doh(Milhouse)");}
}

{WillNotCompile} 标记将该文件排除在本书的 Gradle 构建之外,但是如果你手工编译它,你将看到:method does not override a method from its superclass.方法不会重写超类中的方法, **@Override ** 注解能防止你意外地重载。

组合与继承的选择

组合和继承都允许在新类中放置子对象(组合是显式的,而继承是隐式的)。你或许想知道这二者之间的区别,以及怎样在二者间做选择。

当你想在新类中包含一个已有类的功能时,使用组合,而非继承。也就是说,在新类中嵌入一个对象(通常是私有的),以实现其功能。新类的使用者看到的是你所定义的新类的接口,而非嵌入对象的接口。

有时让类的用户直接访问到新类中的组合成分是有意义的。只需将成员对象声明为 public 即可(可以把这当作“半委托”的一种)。成员对象隐藏了具体实现,所以这是安全的。当用户知道你正在组装一组部件时,会使得接口更加容易理解。下面的 car 对象是个很好的例子:

// reuse/Car.java
// Composition with public objects
class Engine {public void start() {}public void rev() {}public void stop() {}
}class Wheel {public void inflate(int psi) {}
}class Window {public void rollup() {}public void rolldown() {}
}class Door {public Window window = new Window();public void open() {}public void close() {}
}public class Car {public Engine engine = new Engine();public Wheel[] wheel = new Wheel[4];public Door left = new Door(), right = new Door(); // 2-doorpublic Car() {for (int i = 0; i < 4; i++) {wheel[i] = new Wheel();}}public static void main(String[] args) {Car car = new Car();car.left.window.rollup();car.wheel[0].inflate(72);}
}

因为在这个例子中 car 的组合也是问题分析的一部分(不是底层设计的部分),所以声明成员为 public 有助于客户端程序员理解如何使用类,且降低了类创建者面临的代码复杂度。但是,记住这是一个特例。通常来说,属性还是应该声明为 private

当使用继承时,使用一个现有类并开发出它的新版本。通常这意味着使用一个通用类,并为了某个特殊需求将其特殊化。稍微思考下,你就会发现,用一个交通工具对象来组成一部车是毫无意义的——车不包含交通工具,它就是交通工具。这种“是一个”的关系是用继承来表达的,而“有一个“的关系则用组合来表达。

protected

即然已经接触到继承,关键字 protected 就变得有意义了。在理想世界中,仅靠关键字 private 就足够了。在实际项目中,却经常想把一个事物尽量对外界隐藏,而允许派生类的成员访问。

关键字 protected 就起这个作用。它表示“就类的用户而言,这是 private 的。但对于任何继承它的子类或在同一包中的类,它是可访问的。”(protected 也提供了包访问权限)

尽管可以创建 protected 属性,但是最好的方式是将属性声明为 private 以一直保留更改底层实现的权利。然后通过 protected 控制类的继承者的访问权限。

// reuse/Orc.java
// The protected keyword
class Villain {private String name;protected void set(String nm) {name = nm;}Villain(String name) {this.name = name;}@Overridepublic String toString() {return "I'm a Villain and my name is " + name;}
}public class Orc extends Villain {private int orcNumber;public Orc(String name, int orcNumber) {super(name);this.orcNumber = orcNumber;}public void change(String name, int orcNumber) {set(name); // Available because it's protectedthis.orcNumber = orcNumber;}@Overridepublic String toString() {return "Orc " + orcNumber + ": " + super.toString();}public static void main(String[] args) {Orc orc = new Orc("Limburger", 12);System.out.println(orc);orc.change("Bob", 19);System.out.println(orc);}
}

输出:

Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob

change() 方法可以访问 set() 方法,因为 set() 方法是 protected。注意到,类 OrctoString() 方法也使用了基类的版本。

向上转型

继承最重要的方面不是为新类提供方法。它是新类与基类的一种关系。简而言之,这种关系可以表述为“新类是已有类的一种类型”。

这种描述并非是解释继承的一种花哨方式,这是直接由语言支持的。例如,假设有一个基类 Instrument 代表音乐乐器和一个派生类 Wind。 因为继承保证了基类的所有方法在派生类中也是可用的,所以任意发送给该基类的消息也能发送给派生类。如果 Instrument 有一个 play() 方法,那么 Wind 也有该方法。这意味着你可以准确地说 Wind 对象也是一种类型的 Instrument。下面例子展示了编译器是如何支持这一概念的:

// reuse/Wind.java
// Inheritance & upcasting
class Instrument {public void play() {}static void tune(Instrument i) {// ...i.play();}
}// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {public static void main(String[] args) {Wind flute = new Wind();Instrument.tune(flute); // Upcasting}
}

tune() 方法接受了一个 Instrument 类型的引用。但是,在 Windmain() 方法里,tune() 方法却传入了一个 Wind 引用。鉴于 Java 对类型检查十分严格,一个接收一种类型的方法接受了另一种类型看起来很奇怪,除非你意识到 Wind 对象同时也是一个 Instrument 对象,而且 Instrumenttune 方法一定会存在于 Wind 中。在 tune() 中,代码对 Instrument 和 所有 Instrument 的派生类起作用,这种把 Wind 引用转换为 Instrument 引用的行为称作_向上转型_。

该术语是基于传统的类继承图:图最上面是根,然后向下铺展。(当然你可以以任意方式画你认为有帮助的类图。)于是,Wind.java 的类图是:

在这里插入图片描述

继承图中派生类转型为基类是向上的,所以通常称作_向上转型_。因为是从一个更具体的类转化为一个更一般的类,所以向上转型永远是安全的。也就是说,派生类是基类的一个超集。它可能比基类包含更多的方法,但它必须至少具有与基类一样的方法。在向上转型期间,类接口只可能失去方法,不会增加方法。这就是为什么编译器在没有任何明确转型或其他特殊标记的情况下,仍然允许向上转型的原因。

也可以执行与向上转型相反的向下转型,但是会有问题,对于该问题会放在下一章和“类型信息”一章进行更深入的探讨。

再论组合和继承

在面向对象编程中,创建和使用代码最有可能的方法是将数据和方法一起打包到类中,然后使用该类的对象。也可以使用已有的类通过组合来创建新类。继承其实不太常用。因此尽管在教授 OOP 的过程中我们多次强调继承,但这并不意味着要尽可能使用它。恰恰相反,尽量少使用它,除非确实使用继承是有帮助的。一种判断使用组合还是继承的最清晰的方法是问一问自己是否需要把新类向上转型为基类。如果必须向上转型,那么继承就是必要的,但如果不需要,则要进一步考虑是否该采用继承。“多态”一章提出了一个使用向上转型的最有力的理由,但是只要记住问一问“我需要向上转型吗?”,就能在这两者中作出较好的选择。

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

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

相关文章

Java 常用编辑器 IntelliJ IDEA

文章目录 IDEA 概述IDEA 下载和安装IDEA 中的第一个代码IDEA 的项目和模块操作&#xff08;一&#xff09;类的操作&#xff08;二&#xff09;模块的操作&#xff08;三&#xff09;项目的操作 IDEA 概述 IntelliJ IDEA是一款由JetBrains开发的集成开发环境&#xff08;IDE&am…

【深度学习注意力机制系列】—— CBAM注意力机制(附pytorch实现)

CBAM&#xff08;Convolutional Block Attention Module&#xff09;是一种用于增强卷积神经网络&#xff08;CNN&#xff09;性能的注意力机制模块。它由Sanghyun Woo等人在2018年的论文[1807.06521] CBAM: Convolutional Block Attention Module (arxiv.org)中提出。CBAM的主…

pyscenic分析:视频教程

我们之前更新过pyscenic的教程&#xff1a;pySCENIC单细胞转录因子分析更新&#xff1a;数据库、软件更新。我们也说过&#xff0c;我们号是放弃R语言版的SCENIC的分析了&#xff0c;因为它比较耗费计算资源和时间&#xff0c;所以我们的单细胞转录因子分析教程都是基于pysceni…

【Linux】gcc编译器的使用和介绍

目录 一&#xff0c;GCC简介 二&#xff0c;GCC的主要组件 三&#xff0c;GCC的工作流程 四&#xff0c;GCC的一些重要特性和功能 五&#xff0c;GCC常用的编译选项 六&#xff0c;GCC的输入输出选项的具体用法 七&#xff0c;GCC的参考文档 一&#xff0c;GCC简介 GCC&…

小研究 - MySQL 数据库下存储过程的综合运用研究

信息系统工程领域对数据安全的要求比较高&#xff0c;MySQL 数据库管理系统普遍应用于各种信息系统应用软件的开发之中&#xff0c;而角色与权限设计不仅关乎数据库中数据保密性的性能高低&#xff0c;也关系到用户使用数据库的最低要求。在对数据库的安全性进行设计时&#xf…

企业服务器器中了360后缀勒索病毒怎么解决,勒索病毒解密数据恢复

随着网络威胁的增加&#xff0c;企业服务器成为黑客攻击的目标之一。近期&#xff0c;上海某知名律师事务所的数据库遭到了360后缀的勒索病毒攻击&#xff0c;导致企业服务器内的数据库被360后缀勒索病毒加密。许多重要的数据被锁定无法正常读取&#xff0c;严重影响了企业的正…

adb 通过wifi连接手机

adb 通过wifi连接手机 1. 电脑通过USB线连接手机2. 手机开启USB调试模式&#xff0c;开启手机开发者模式3.手机开启USB调试模式 更多设置-》开发者选项-》USB调试4.点击Wi-Fi 高级设置&#xff0c;可以查看到手机Wi-Fi的IP地址&#xff0c;此IP地址adb命令后面的ip地址&#xf…

面试题:说说vue2的生命周期函数?说说vue3的生命周期函数?说说vue2和vue3的生命周期函数对比?

说说vue2的生命周期函数&#xff1f;说说vue3的生命周期函数&#xff1f;说说vue2和vue3的生命周期函数对比&#xff1f; 一、说说vue2的生命周期函数1.1 vue生命周期分为四个阶段、8个钩子1.1.1 beforeCreate 和 created 初始化阶段1.1.2 beforeMount 和 mounted 挂载阶段1.1.…

基于熵权法对Topsis模型的修正

由于层次分析法的最大缺点为&#xff1a;主观性太强&#xff0c;影响判断&#xff0c;对结果有很大影响&#xff0c;所以提出了熵权法修正。 变异程度方差/标准差。 如何度量信息量的大小&#xff1a; 把不可能的事情变成可能&#xff0c;这里面就有很多信息量。 概率越大&…

基于facenet+faiss开发构建人脸识别系统

facenet是一款非常经典的神经网络模型&#xff0c;它可以直接学习从人脸图像到欧几里德空间的映射(直接将人脸映射到欧几里得空间)。在欧几里德空间中&#xff0c;距离直接对应于人脸相似性的度量。一旦这个空间产生&#xff0c;使用标准技术&#xff0c;将FaceNet嵌入作为特征…

【Python机器学习】实验08 决策树

文章目录 决策树1 创建数据2 定义香农信息熵3 条件熵4 信息增益5 计算所有特征的信息增益&#xff0c;选择最优最大信息增益的特征返回6 利用ID3算法生成决策树7 利用数据构造一颗决策树Scikit-learn实例决策树分类决策树回归Scikit-learn 的决策树参数决策树调参 实验1 通过sk…

js2-js中的数据结构

1、什么是数据结构 数据结构是计算机存储、组织数据的方式。 数据结构意味着接口或封装&#xff0c;一个数据结构可被视为两个函数之间的接口&#xff0c;或者是由数据类型联合组成的存储内容的访问方法封装。 每天的编码中都会用到数据结构&#xff0c;其中数组是最简单的内存…

FFmpeg安装和使用

sudo apt install ffmpeg sudo apt-get install libavfilter-devcmakelist模板 CMakeLists.txt cmake_minimum_required(VERSION 3.16) project(ffmpeg_demo)# 设置ffmpeg依赖库及头文件所在目录&#xff0c;并存进指定变量 set(ffmpeg_libs_DIR /usr/lib/x86_64-linux-gnu) …

【GO】 33.go-zero 示例

1. 获取go-zero库 go get -u github.com/zeromicro/go-zero 2. 安装goctl brew install goctlgoctl -v #goctl version 1.5.4 darwin/amd64 3. 创建.api文件&#xff0c; greet.api goctl api -o greet.api syntax "v1"info (title: // TODO: add titledesc: //…

如何使用appuploader制作apple证书​

转载&#xff1a;如何使用appuploader制作apple证书​ 如何使用appuploader制作apple证书​ 一.证书管理​ 点击首页的证书管理 二.新建证书​ 点击“添加”&#xff0c;新建一个证书文件 免费账号制作证书只有7天有效期&#xff0c;没有推送消息功能&#xff0c;推送证书…

UNet Model

论文地址 第一阶段 conv2d(33) first conv&#xff1a;5725721 → 57057064 second conv&#xff1a;57057064 → 56856864 代码 # first 33 convolutional layer self.first nn.Conv2d(in_channels, out_channels, kernel_size3, padding1) self.act1 nn.ReLU() # Seco…

浏览器无法连接网络问题

问题描述 电脑其他程序都能正常联网&#xff0c;但是所有的浏览器都无法联网&#xff0c;同时外部网站都能ping通 问题诊断 查看电脑Internet连接的问题报告显示&#xff1a;该设备或资源(Web 代理)未设置为接受端口"7890"上的连接。 解决方案 经过检查发现不是IP地址…

若依vue -【 100 ~ 更 】

100 主子表代码生成详解 1 新建数据库表结构&#xff08;主子表&#xff09; -- ---------------------------- -- 客户表 -- ---------------------------- drop table if exists sys_customer; create table sys_customer (customer_id bigint(20) not null…

浅谈AI浪潮下的视频大数据发展趋势与应用

视频大数据的发展趋势是多样化和个性化的。随着科技的不断进步&#xff0c;人们对于视频内容的需求也在不断变化。从传统的电视节目到现在的短视频、直播、VR等多种形式&#xff0c;视频内容已经不再是单一的娱乐方式&#xff0c;更是涉及到教育、医疗、商业等各个领域。 为了满…

crypto-js中AES的加解密封装

在项目中安装依赖&#xff1a; npm i crypto-js在使用的页面引入&#xff1a; import CryptoJS from crypto-jscrypto-js中AES的加解密简单的封装了一下&#xff1a; //加密const KEY 000102030405060708090a0b0c0d0e0f // 秘钥 这两个需要和后端统一const IV 8a8c8fd8fe3…