05 | 类型匹配:怎么切除臃肿的强制转换

Java 的模式匹配是一个新型的、而且还在持续快速演进的领域。类型匹配是模式匹配的一个规范。类型匹配这个特性,首先在 JDK 14 中以预览版的形式发布。在 JDK 15 中,改进的类型匹配再次以预览版的形式发布。最后,类型匹配在 JDK 16 正式发布。

那么,什么是模式匹配,什么又是类型匹配呢?这就要说到模式的组成。通常,一个模式是匹配谓词和匹配变量的组合。其中,匹配谓词用来确定模式和目标是否匹配。在模式和目标匹配的情况下,匹配变量是从匹配目标里提取出来的一个或者多个变量。

对于类型匹配来说,匹配谓词用来指定模式的数据类型,而匹配变量就是一个属于该类型的数据变量。需要注意的是,对于类型匹配来说,匹配变量只有一个。

这样的描述还是太抽象,太难理解。我们还是通过案例和代码,一点一点地来理解类型匹配吧。

阅读案例

在程序员的日常工作中,一个重要的事情,就是把相似的东西抽象出来,设计成一个通用的、可以复用的接口。

比如说,我们从正方形、长方形、圆形这些看起来差异巨大的东西出发,抽象出了形状这个接口。我们希望使用一个实例时,如果我们不能确定它是正方形还是长方形,我们至少还能确定它是一个形状。这种模模糊糊的确定性(其实也是不确定性),其实对我们编写代码有巨大的帮助,包括但是不限于简化代码逻辑,减少代码错误。

但要注意的是,每一个实例都是具体的形状。它可以是正方形的对象,可以是长方形的对象,就是不能是一个抽象的形状。也就是说,抽象的类和接口不能直接实例化。

一个方法的规范,它的输入参数可能是一个表示形状的对象,也可能是一个更一般化的对象。比如说吧,我们要设计一个方法,来判断一个形状是不是正方形。那么,就需要一个表示形状的对象,作为这个方法的输入参数。而实现这个方法的代码,仅仅知道形状这个一般化的对象是远远不够的。下面的代码,就是一个这种方法的实现代码。

static boolean isSquare(Shape shape) {if (shape instanceof Rectangle) {Rectangle rect = (Rectangle) shape;return (rect.length == rect.width);}return (shape instanceof Square);
}

在这个 isSquare 方法的实现代码里,我们需要使用 instanceof 运算符,来判断输入参数是不是一个长方形的实例;如果判断成立,再使用类型转换运算符,把这个实例投射成长方形的实例;最后,我们开始使用这个长方形的实例,进行更多的运算。

其实,这样的操作是一个模式化的过程。如果我们把它揉碎了来看,这个模式有三个部分。

第一个部分是类型判断语句,也就是匹配谓词,使用的代码是“instanceof Rectangle”。第二个部分是类型转换语句,使用的是类型转换运算符((Rectangle) shape)。第三个部分是声明一个新的本地变量,也就是匹配变量,来承载转换后的数据,使用的是变量声明和赋值运算符(Rectangle rect =)。第二个部分和第三个部分,只有在类型判断成立的情况下,才能够执行。

使用这样的模式化操作,是一个 Java 程序员的基本功。这个模式直观而且便于理解。可是,这个模式很乏味,也很臃肿。调用了 instanceof 之后,除了类型转换之外,我们还可以做什么呢?一般情况下,在类型判断之后,我们总是紧跟着就进行类型转换。

把类型判断和类型转换切割成两个部分,增加了错误潜入的机会,平添了许多烦恼。比如说,一个活生生的程序员或者冷冰冰的机器,有可能无意地使用了错误的类型。下面例子中的两段代码,就是两个常见的类型转换错误。第一段代码误用了变量类型,第二段代码误用了判断结果。

if (shape instanceof Circle) {Rectangle rect = (Rectangle) shape;return (rect.length == rect.width);
}
if (!(shape instanceof Rectangle) {Rectangle rect = (Rectangle) shape;return (rect.length == rect.width);
}

类型判断之后,我们原本就可以开始关注更重要的后续代码逻辑了,但现在不得不停下来编写类型转化代码,或者审视类型转换代码是否恰当。这当然影响力了生产效率。

我们可以用什么方法改进这个模式,提高生产效率呢? 这个问题的答案就是类型匹配。

类型匹配

那么,类型匹配是怎么改进这个模式的呢?我们先来看看使用了类型匹配的代码的样子。下面的例子,就是使用类型匹配的一段代码。

if (shape instanceof Rectangle rect) {return (rect.length == rect.width);
}

为了便于更直观地比较,我把传统的实现代码和使用了类型匹配的实现代码列在了下面的表格里。你可以找找其中的差异,体会下类型匹配带来的改进。

img

就像我们前面拆解的一样,传统的实现代码有三个部分;而使用类型匹配的代码,只有匹配谓词和本地变量两个部分,而且是在同一个语句里。为了帮助你理解这些概念,我画了下面的这张图,标记出了类型匹配的组成部分和关键概念。

img

你可能已经注意到了,使用类型转换运算符的语句,没有出现在使用类型匹配的代码里。但是,这并不影响类型匹配代码所要表达的基本逻辑。

这个基本逻辑就是:如果目标变量是一个长方形的实例,那么这个目标变量就会被赋值给一个本地的长方形变量,也就是我们所说的匹配变量;相反,如果目标变量不是一个长方形的实例,那么这个匹配变量就不会被赋值。

前面,我们讨论了两个常见的类型转换错误:误用变量类型和误用判断结果。在使用类型匹配的代码里,不再需要重复使用匹配类型,也不再需要使用强制类型转换符。所以,使用类型匹配的代码,不用再担心误用变量类型的错误了。

误用判断结果的错误,是不是也被解决了呢? 似乎,我们还能写出下面的代码。在这样的代码里,如果目标变量不是一个长方形的实例,我们是不是也有可能使用匹配的变量呢?

if (!(shape instanceof Rectangle rect)) {return (rect.length == rect.width);
}

幸运的是,类型匹配已经考虑到了这个问题,Java 编译器能够检测出上面的错误,不会允许使用没有赋值的匹配变量。这样,在代码编译期间,就有机会纠正代码的错误。比如说,我们可以尝试修改成下面的逻辑:如果目标变量不是一个长方形的实例,我们就不使用匹配变量;否则,我们就使用匹配变量。把这个逻辑映射到代码,大致是下面的样子。

if (!(shape instanceof Rectangle rect)) {return false;
} else {return (rect.length == rect.width);
}

在上面的代码里,使用匹配变量的条件语句 else 分支并没有声明这个匹配变量。为什么 if 语句声明的变量,可以在 else 语句里使用呢?要弄清楚这个问题,我们还要了解匹配变量的作用域。掌握匹配变量的作用域,是学会使用类型匹配的关键。

匹配变量的作用域

匹配变量的作用域,就是目标变量可以被确认匹配的范围。如果在一个范围内,无法确认目标变量是否被匹配,或者目标变量不能被匹配,都不能使用匹配变量。 如果我们从编译器的角度去理解,也就是说,在一个范围里,如果编译器能够确定匹配变量已经被赋值了,那么它就可以在这个范围内使用;如果编译器不能够确定匹配变量是否被赋值,或者确定没有被赋值,那么他就不能在这个范围内使用。

我们还是通过代码来理解这个有点抽象的概念吧。

第一段代码,我们看看最常规的使用。我们可以在确认类型匹配的条件语句之内使用匹配变量。这个条件语句之外,不是匹配变量的作用域。

public static boolean isSquareImplA(Shape shape) {if (shape instanceof Rectangle rect) {// rect is in scopereturn rect.length() == rect.width();}// rect is not in scope herereturn shape instanceof Square;
}

第二段代码,我们看看有点意外的使用。我们可以在确认类型不匹配的条件语句之后使用匹配变量。这个条件语句之内,不是匹配变量的作用域。

public static boolean isSquareImplB(Shape shape) {if (!(shape instanceof Rectangle rect)) {// rect is not in scope herereturn shape instanceof Square;}// rect is in scopereturn rect.length() == rect.width();
}

第三段代码,我们看看紧凑的方式。这一段代码的逻辑,和第一段代码一样,我们只是换成了一种更紧凑的表示方法。

在这一段代码里,我们使用逻辑与运算符表示第一段里的条件语句:类型匹配并且匹配变量满足某一个条件。这样的表示是符合匹配变量的作用域规则的。逻辑与运算符从左到右计算,只有第一个运算成立,也就是类型匹配,才能进行下一个运算。所以,我们可以在逻辑与运算的第二部分,使用匹配变量。

public static boolean isSquareImplC(Shape shape) {return shape instanceof Square ||  // rect is not in scope here(shape instanceof Rectangle rect &&rect.length() == rect.width());   // rect is in scope here
}

第四段代码,我们看看逻辑或运算。它类似于第三段代码,只是我们把逻辑与运算符替换成了逻辑或运算符。这时候的逻辑,就变成了“类型匹配或者匹配变量满足某一个条件”。逻辑或运算符也是从左到右计算。

不过和逻辑与运算符不同的是,一般来说,只有第一个运算不成立,也就是说类型不匹配时,才能进行下一步的运算。下一步的运算,匹配变量并没有被赋值,我们不能够在这一部分使用匹配变量。所以,这一段代码并不能通过编译器的审查。

public static boolean isSquareImplD(Shape shape) {return shape instanceof Square ||  // rect is not in scope here(shape instanceof Rectangle rect ||rect.length() == rect.width());   // rect is not in scope here
}

第五段代码,我们看看位与运算。

这段代码和第三段代码类似,只是我们把逻辑与运算符(&&)替换成了位与运算符(&)。

和第三段代码相比,这一段代码的逻辑其实并没有变化。只不过,位与运算符两侧的表达式都要参与计算。也就是说,不管位与运算符左侧的运算是否成立,位与运算符右侧的运算都要计算出来。换句话说,无论左侧的类型匹配不匹配,右侧的匹配变量都要使用。这就违反了匹配变量的作用域原则,编译器不能够确定匹配变量是否被赋值。所以,这一段代码,也不能通过编译器的审查。

public static boolean isSquareImplE(Shape shape) {return shape instanceof Square |  // rect is not in scope here(shape instanceof Rectangle rect &rect.length() == rect.width());   // rect is in scope here
}

第六段代码,我们把匹配变量的作用域的影响延展一下,看看它对影子变量(Shadowed Variable)的影响。

既然我们讨论变量的作用域,我们就不能不看看影子变量。假设我们定义了一个静态变量,它和匹配变量使用相同的名字。在匹配变量的作用域内,除非特殊处理,这个静态变量就被遮掩住了。这时候,这个变量名字代表的就是匹配变量;而不是静态变量。类似地,在匹配变量的作用域之外,这个变量名字代表的就是这个静态变量。

在这段代码里,我们使用类似于第一段代码的代码组织方式,来表述类型匹配部分的逻辑。另外,我在代码里标注了变量的作用域。你可以看看,这两个变量的作用域,和你想象的作用域是不是一样的?

public final class Shadow {private static final Rectangle rect = null;public static boolean isSquare(Shape shape) {if (shape instanceof Rectangle rect) {// Field rect is shadowed, local rect is in scopeSystem.out.println("This should be the local rect: " + rect);return rect.length() == rect.width();}// Field rect is in scope, local rect is not in scope hereSystem.out.println("This should be the field rect: " + rect);return shape instanceof Shape.Square;}
}

第七段代码,我们还是来看一看影子变量。只不过,这一次,我们使用类似于第二段代码的代码组织方式,来表述类型匹配部分的逻辑。我在代码里标出的这两个变量的作用域,和你想象的作用域是一样的吗?

public final class Shadow {private static final Rectangle rect = null;public static boolean isSquare(Shape shape) {if (!(shape instanceof Rectangle rect)) {// Field rect is in scope, local rect is not in scope hereSystem.out.println("This should be the field rect: " + rect);return shape instanceof Shape.Square;}// Field rect is shadowed, local rect is in scopeSystem.out.println("This should be the local rect: " + rect);return rect.length() == rect.width();}
}

如果回头看看这七段代码,你会倾向于哪一种编码的风格?我们把这些代码放在一起,分析一下它们的特点。

第四段和第五段代码,不能通过编译器的审查,所以我们不能使用这两种编码方式。

第二段和第七段代码,匹配变量的作用域,远离了类型匹配语句。这种距离上的疏远,无论在视觉上还是心理上,都不是很舒适的选择。不舒适,就给错误留下了空间,不容易编码,也不容易排错。这种代码逻辑和语法上都没有问题,但是不太容易阅读。

第一段和第六段代码,匹配变量的作用域,紧跟着类型匹配语句。这是我们感觉舒适的代码布局,也是最安全的代码布局,不容易出错,也容易阅读。

第三段代码,它的匹配变量的作用域也是紧跟着类型匹配语句。只不过,这种代码的编排方式不太容易阅读,阅读者需要认真拆解每一个条件,才能确认逻辑是正确的。相对于第一段和第六段代码,第三段代码的组织方式,是一个次优的选择。

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

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

相关文章

电子画册如何制作,教你几分钟简单上手制作?

电子画册不同于纸质画册,它可以不受时间、空间及地域的限制,以更直观、新颖的形式展示在读者面前,还能快速传播效益。所以,当下,越来越多人想要用电子画册来传递内容信息。 如何制作电子画册?其实只要使用…

Python 框架学习 Django篇 (五) Session与Token认证

我们前面经过数据库的学习已经基本了解了怎么接受前端发过来的请求,并处理后返回数据实现了一个基本的登录登出效果,但是存在一个问题,我们是将所有的请求都直接处理了,并没有去检查是否为已经登录的管理员发送的,如果…

WebAPI项目在Linux服务器上部署记录

对已有的WebAPI项目进行发布 发布流程 需要把publish的文件夹直接上传至linux服务器 在Linux服务器上部署环境 检查是否安装了dotnet环境 直接命令行输入 dontnet,如果弹出的是下面的语句,说明没有安装dotnet环境 -bash: dotnet:command not found…

React 框架

1、React 框架简介 1.1、介绍 CS 与 BS结合:像 React,Vue 此类框架,转移了部分服务器的功能到客户端。将CS 和 BS 加以结合。客户端只用请求一次服务器,服务器就将所有js代码返回给客户端,所有交互类操作都不再依赖服…

elementUI 中 date-picker 的使用的坑(vue3)

目录 1. 英文显示2. format 与 value-format 无效3. date-picker 时间范围4. 小结 1. 英文显示 <el-date-pickerv-model"dateValue"type"date"placeholder"选择日期"></el-date-picker>解决方案&#xff1a; 引用 zhCn <script&g…

短视频矩阵系统源码/技术应用搭建

短视频矩阵系统开发围绕的开发核心维度&#xff1a; 1. 多账号原理开发维度 适用于多平台多账号管理&#xff0c;支持不同类型账号矩阵通过工具实现统一便捷式管理。&#xff08;企业号&#xff0c;员工号&#xff0c;个人号&#xff09; 2. 账号矩阵内容开发维护 利用账号矩…

C#调用C/C++从零深入讲解

C#调用非托管DLL从零深入讲解 一、结构对齐 结构对齐是C#调用非托管DLL的必备知识。 在没有#pragma pack声明下结构体内存对齐的规则为: 第一个成员的偏移量为0,每个成员的首地址为自身大小的整数倍子结构体的第一个成员偏移量应当是子结构体最大成员的整数倍结构体总大小…

使用Spyder进行动态网页爬取:实战指南

导语 知乎数据的攀爬价值在于获取用户观点、知识和需求&#xff0c;进行市场调查、用户画像分析&#xff0c;以及发现热门话题和可能的新兴领域。同时&#xff0c;知乎上的问题并回答也是宝贵的学习资源&#xff0c;用于知识图谱构建和自然语言处理研究。爬取知乎数据为决策和…

探索现代IT岗位:职业机遇的海洋

目录 1 引言2 传统软件开发3 数据分析与人工智能4 网络与系统管理5 信息安全6 新兴技术领域 1 引言 随着现代科技的迅猛发展&#xff0c;信息技术&#xff08;IT&#xff09;行业已经成为了全球经济的关键引擎&#xff0c;改变了我们的生活方式、商业模式和社会互动方式。IT行…

【C++和数据结构】模拟实现哈希表和unordered_set与unordered_map

目录 一、哈希的概念与方法 1、哈希概念 2、常用的两个哈希函数 二、闭散列的实现 1、基本结构&#xff1a; 2、两种增容思路 和 插入 闭散列的增容&#xff1a; 哈希表的插入&#xff1a; 3、查找 4、删除 三、开散列的实现 1、基本结构 2、仿函数Hash 3、迭代器…

React 中 keys 的作用是什么?

目录 前言&#xff1a;React 中的 Keys 的重要性 为什么 Keys 重要&#xff1f; 详解&#xff1a;key 属性的基本概念 用法&#xff1a;key 属性的示例 解析&#xff1a;key 属性的优势和局限性 优势&#xff1a; 局限性&#xff1a; key 属性的最佳实践 稳定的唯一标…

代码随想录二刷 Day46

10背包&#xff1a; 二维内侧与外侧都是正序遍历&#xff0c;二维的内侧与外侧是背包还是物品无所谓&#xff1b; 10背包&#xff1a; 一维外侧是正序&#xff0c;内侧是倒序&#xff1b; 目的是为了一个物品只选取一次&#xff1b;一维内侧一定要是背包&#xff1b;原因我想了…

SQL关于日期的计算合集

前言 在SQL Server中&#xff0c;时间和日期是常见的数据类型&#xff0c;也是数据处理中重要的一部分。SQL Server提供了许多内置函数&#xff0c;用于处理时间和日期数据类型。这些函数可以帮助我们执行各种常见的任务&#xff0c;例如从日期中提取特定的部分&#xff0c;计…

【2021研电赛】基于动态无线充电技术的自动驾驶小车

本作品介绍参与极术社区的有奖征集|分享研电赛作品扩大影响力&#xff0c;更有重磅电子产品免费领取! 参赛单位&#xff1a;北京交通大学 作品简介 近年来&#xff0c;电动汽车的发展得到了很多国家和车企的大力支持&#xff0c;但其仍然存在充电时间长、充电设施不齐全等问…

迷你洗衣机哪个牌子好又实惠?小型洗衣机全自动

现在洗内衣内裤也是一件较麻烦的事情了&#xff0c;在清洗过程中还要用热水杀菌&#xff0c;还要确保洗衣液是否有冲洗干净&#xff0c;还要防止细菌的滋生等等&#xff0c;所以入手一款小型的烘洗全套的内衣洗衣机是非常有必要的&#xff0c;专门的内衣洗衣机可以最大程度减少…

SpringMVC(三)获取请求参数

1.1通过ServletAPI获取 SpringMVC封装的就是原生的servlet 我们进行测试如下所示&#xff1a; package com.rgf.controller.service;import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.…

学习MAVEN

MAVEN的详细介绍和作用、意义 好的&#xff0c;小朋友们&#xff0c;我们今天来聊聊一个非常神奇的工具箱&#xff0c;它的名字叫做Maven! &#x1f31f; 1. **神奇的工具箱Maven**: Maven就像是一个神奇的工具箱&#x1f9f0;&#xff0c;它可以帮助大人们把他们的电脑工…

【Docker】Dockerfile常用指令

参考官方文档&#xff1a;https://docs.docker.com/engine/reference/builder/ Dockerfile常用指令 指令说明from基础镜像&#xff0c;当前镜像基于&#xff08;依赖&#xff09;哪个镜像maintainer镜像的维护者和邮箱run镜像构建时需要执行的命令workdir镜像的工作目录expos…

基于springboot实现基于Java的超市进销存系统项目【项目源码+论文说明】

基于springboot实现基于Java的超市进销存系统演示 摘要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;超市进销存系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#x…

最详细STM32,cubeMX外部中断

这篇文章将详细介绍 cubeMX外部中断的配置&#xff0c;实现过程。 文章目录 前言一、外部中断的基础知识。二、cubeMX 配置外部中断三、自动生成的代码解析四、代码实现。总结 前言 实验开发板&#xff1a;STM32F103C8T6。所需软件&#xff1a;keil5 &#xff0c; cubeMX 。实…