第五十二条:谨慎使用重载

news2024/11/22 5:51:05

首先说明一下,在实际项目中出现重载还是很少的,一般重写的情况会比较多。

下面这个程序的意图是好的,它试图根据一个集合(collection)是Set、List,还是其他的集合类型,对它进行分类:

// 存在问题 - 这个程序会打印么?
public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }
    public static String classify(List<?> lst) {
        return "List";
    }
    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }
    public static void main(String[] args) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };
        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}


  你可能期望这个程序会打印出“Set”,紧接着是“List”,以及“Unknown Collection”。但实际上不是这样。它是打印“Unknown Collection”三次。为什么会这样呢?因为classify方法被重载(overloaded)了,而要调用哪个重载方法是在编译时做出决定的。对于for循环中的全部三次迭代,参数的编译时类型都是想用的:Collection<?>。每次迭代的运行时类型都是不同的,但这并不影响对重载方法的选择。因为该参数的编译时类型为Collection<?>,所以,唯一合适的重载方法时第三个:Collection<?>,在循环的每次迭代中,都会调用这个重载方法。

  这个程序的行为有悖常理,因为对于重载方法(overloaded method)的选择是静态的,而对于被覆盖的方法(overridden method)的选择则是动态的。 选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型。这里重新说明一下,当一个子类包含的方法声明与其祖先类中的方法声明具有相同的签名时,方法就被覆盖了。如果实例方法在子类中被覆盖了,并且这个方法是在该子类的实例上被调用,那么子类中的覆盖方法(overriding method)将会执行,而不管该子类实例的编译时类型到底是什么。为了更具体地说明,考虑下面这个程序:

class Wine {
    String name() { return "wine"; }
}
class SparklingWine extends Wine {
    @Override
    String name() { return "sparkling wine"; }
}
class Champagne extends SparklingWine {
    @Override
    String name() { return "champagne"; }
}
public class Overriding {
    public static void main(String[] args) {
        List<Wine> wineList = List.of(new Wine(), new SparklingWine(), new Champagne());
        for (Wine wine : wineList)
            System.out.println(wine.name());
    }
}


  name方法是在类Wine中被声明的,但是在子类SparklingWine和Champagne中被覆盖。正如你所预期的那样,这个程序打印出“wine,sparking wine和champagne”,尽管在循环的每次迭代中,实例的编译时类型都为Wine。当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法奖被执行;“最为具体地(most specific)”那个覆盖版本总是会得到执行。这与重载的情形相比,对象的运行时类型并不影响“哪个重载版本将被执行”;选择工作是在编译时进行的,完全基于参数的编译时类型。

  在CollectionClassifier这个示例中,该程序的意图是:根据参数的运行时类型自动将调用分发给适当的重载方法,以此来识别出参数的类型,就好像Wine的例子中的name方法所做的那样。方法重载机制完全没有提供这样的功能。假设需要有个静态方法,CollectionClassifier程序的最佳修正方案是,用单个方法来替换这三个重载的classify方法,并在这个方法中做一个显示的instanceof测试:

public static String classify(Collection<?> c) {
    return c instanceof Set ? "Set" : c instanceof List ? "List" : "Unknown Collection";
}


  因为覆盖机制是规范,而重载机制是例外,所以,覆盖机制满足了人们对于方法调用行为的期望。正如CollectionClassifier例子所示,重载机制很容易使这些期望落空。如果编写出来的代码的行为可能使程序猿感到困惑,他就是很糟糕的实践。对于API来说尤其如此。如果API的普通用户根本不知道“对于一组给定的参数,其中的哪个重载方法将会被调用”,那么,使用这样的API就很可能出错。这些错误要等到运行时发生了怪异的行为之后才会显现出来,许多程序猿无法诊断出这样的错误。因此,应该避免乱用重载机制。

  到底怎样才算乱用重载机制呢?这个问题仍然存在争议。安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法(实际项目中几乎没人会写,因为这样的代码被领导看到了会被喷,除非你们公司不审核代码)。如果方法使用可变参数(varargs),保守的策略是根本不需要重载它,除了第53项中描述的情形之外。如果你遵守这些限制,程序猿永远也不会陷入到“对于任何一组实际的参数,哪个重载方法是适用的”这样的疑问中。这项限制并不麻烦,因为你始终可以给方法起不同的名称,而不使用重载机制(项目里基本都是这种情况)

  例如,考虑ObjectOutputStream这个类。对于每个基本类型,以及几种引用类型,它的write方法都有一种变形。这些变形方法都有不一样的名字,而不是重载write方法,比如writeBoolean(boolean), writeInt(int)和writeLong(long)。实际上,ObjectInputStream类正是提供了这样的读方法。

  对于构造器,你没有选择使用不同名称的机会:一个类的多个构造器总是重载的。在许多情况下,可以选择导出静态工厂,而不是构造器(第1项)。而且,对于构造器,还不用担心重载和覆盖的相互影响,因为构造器不可能被覆盖(重写)。或许你有可能导出多个具有相同参数数目的构造器,所以有必要了解一下如何安全地做到这一点。

  如果对于“任何一组给定的实际参数将应用在哪个重载方法上”始终非常清楚,那么,导出多个具有相同参数数目的重载方法就不可能使程序猿感到困惑。如果对于每一对重载方法,至少有一个对应的参数在两个重载方法中具有“根本不同(radically different)”的类型,就属于这种情况。如果使用任何非空表达式都无法将两种类型相互转换,那么这两种类型就是完全不同的(Two types are radically different if it is clearly impossible to cast any non-null expression to both types)。在这种情况下,一组给定的实际参数应用于哪个重载方法上就完全由参数的运行时类型来决定,不可能受到其编译时类型的影响,所以主要的混淆根源就消除了。例如,ArrayList有一个构造器带一个int参数,另一个构造器带一个Collection参数。难以想象在什么情况下,会不清楚要调用哪一个构造器。

  在Java 1.5发行版之前,所有的基本类型都根本不同于所有的引用类型,但是当自动装箱出现之后,就不再如此了,它会导致真正的麻烦。请考虑下面这个程序:

public class SetList {
    public static void main(String[] args) {
        Set<Integer> set = new TreeSet<>();
        List<Integer> list = new ArrayList<>();

        for (int i = -3; i < 3; i++) {
            set.add(i);
            list.add(i);
        }
        for (int i = 0; i < 3; i++) {
            set.remove(i);
            list.remove(i);
        }
        System.out.println(set + " " + list);
    }
}


  程序将-3到2之间的整数添加到了排好序的集合列表中,然后在集合和列表中都进行3次相同的remove调用。如果你像大多数人一样,希望程序从集合和列表中去除非负数(0,1和2),并打印出[-3,-2,-1]、[-3,-2,-1]。事实上,程序从集合中去除了非负数,还从列表中去除了奇数值,打印出[-3,-2,-1] [-2,0,2]。将这种行为称之为混乱,已经是保守的说法了。

  实际上发生的情况是:set.remove(i)选择调用的是重载方法remove(E),这里的E是集合(Integer)的元素类型,将i从int自动装箱到Integer中。这是你所期待的行为,因此程序不会从集合中去除正值。另一方面,list.remove(i)选择调用的是重载方法remove(int i),它从列表的指定位置上去除元素。如果从列表[-3, -2, -1, 0, 1, 2]开始,去除第零个元素,接着去除第一个、第二个,得到的是[-2, 0, 2],这个秘密被揭开了。 为了解决这个问题,要将list.remove的参数转换成Integer,迫使选择正确的重载方法。或者,你可以调用Integer.valueOf(i),并将结果传递给list.remove。这两种方法都如我们所料,打印出[-3,-2,-1]、[-3,-2,-1]:

for (int i = 0; i < 3; i++) {
    set.remove(i);
    list.remove((Integer) i); // or remove(Integer.valueOf(i))
}


  前一个范例中所示的混乱行为在这里也出现了,因为List接口有两个重载的remove方法:remove(E)和remove(int)。当它在Java 1.5 发行版中被泛型化之前,List接口有一个remove(Object)而不是remove(E),相应的参数类型:Object和int,则根本不用。但是自从有了泛型和自动装箱之后,这两种参数类型就不再根本不同了。换句话说,Java语言中添加了泛型和自动装箱之后,破坏了List接口。幸运的是,Java类库中几乎再没有API受到同样的破坏,但是这种情形清楚地说明了,自动装箱和泛型成了Java语言的一部分之后,谨慎重载显得更加重要了。

  在Java 8中添加lambda和方法引用进一步增加了重载混淆的可能性。例如,考虑这两个片段:

new Thread(System.out::println).start();

ExecutorService exec = Executors.newCachedThreadPool();
exec.submit(System.out::println);


  虽然Thread构造函数的调用和submit方法的调用看起来类似,但前者编译而后者不编译。参数是相同的(System.out :: println),构造函数和方法都有一个带有Runnable的重载。这里发生了什么?答案令人惊讶:submit方法有一个带有Callable 【参数】的重载,然而Thread构造函数并没有。你可能认为这不应该有任何区别,因为println的所有重载都返回void,因此方法引用不可能是Callable。这很有道理,但这不是重载解析算法的工作方式。也许同样令人惊讶的是,如果println方法也没有重载,则submit方法调用将是合法的。它是重载引用方法(println)和调用方法(submit)的组合,它可以防止重载决策算法按照你的预期运行(It is the combination of the overloading of the referenced method (println) and the invoked method (submit) that prevents the overload resolution algorithm from behaving as you’d expect)。

  从技术上讲,问题是System.out :: println是一个不精确的方法引用[JLS,15.13.1],并且“包含隐式类型的lambda表达式或不精确的方法引用的某些参数表达式被适用性测试忽略,因为它们的在选择目标类型之前无法确定含义[JLS,15.12.2]。如果你不理解这段文字的意思,不要担心; 它针对的是编译器的编写者。导致混淆关键是在同一参数位置中具有不同功能接口的重载方法或构造函数。因此,不要让多个重载方法在相同的参数位置接受不同的功能接口。在这个项目的说法中,不同的功能接口从根本上讲并不是完全不同的【也就是有一些相同点】。如果你使用(pass)命令行开关-Xlint:overloads,出现这种有问题的重载时,Java编译器就会警告你。

  数组类型和Object之外的类截然不同。数组类型和Serializable与Cloneable之外的接口也截然不同。如果两类都不是对方的后代,这两个独特的类就是不相关的(unrelated)[JLS, 5.5]。例如,String和Throwable就是不相关的。任何对象都不可能是两个不相关的类的实例,因此不相关的类也是截然不同的。

  还有其他一些“类型对”的例子也是不能相互转换的[JLS, 5.1.12]。但是,一旦超出了上述这些简单的情形,大多数程序猿要想搞清楚“一组实际的参数应用于哪个重载方法上”就会非常困难。确定选择哪个重载方法的规则是非常复杂得,并且每个版本都会变得更加复杂。很少有程序猿能够理解其中的所有微妙之处。

  有时候,尤其是在更新现有类的时候,可能会被迫违反本项中的指导原则。例如,例如,考虑String,它自Java 4以来就有一个contentEquals(StringBuffer)方法。在Java 5中,新增了一个CharSequence接口,用来为StringBuffer,StringBuilder,String,CharBuffer和其他类似的类型提供公共接口。在添加CharSequence接口的同时,String也加(outfitted)了一个接受一个CharSequence类型参数的contentEquals方法。

  虽然产生的重载明显违反了此项中的指导原则,但它不会造成任何损害,因为重载方法在同一对象引用上调用时会执行完全相同的操作【也就是一个对象引用同时作为参数调用这两个方法,执行的操作是一样的】。程序猿可能并不知道哪个重载函数会被调用,但是只要这它们的行为相同,它【知道哪个重载函数会被调用】就没有任何意义。确保这种行为的标准做法是,让更具体化的重载方法把调用转发给更一般化的重载方法:

// Ensuring that 2 methods have identical behavior by forwarding
public boolean contentEquals(StringBuffer sb) {
    return contentEquals((CharSequence) sb);
}


  虽然Java平台类库很大程度上遵循了本项中的建议,但是也有诸多的类违背了。例如:String类导出了两个重载的静态工厂方法:valueOf(char[])和valueOf(Object),当这两个方法被传递了同样的对象引用时,它们所做的事情完全不同。没有正当的理由可以解释这一点,它应该被看作是一种反常行为,有可能会造成真正的混淆。

  简而言之,能够重载方法并不意味着就应该重载方法。一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。在某些情况下,特别是涉及构造函数的时候,要遵循这条建议也许是不可能的。在这种情况下,至少应该避免这样的情形:同一组参数只需要经过类型转换就可以被传递给不用的重载方法。如果不能避免这种情形,例如,因为正在改造一个现有的类来实现新的接口,就应该保证:当传递同样的参数时,所有重载方法的行为必须一致。如果不能做到这一点,程序猿就很难有效地使用被重载的方法或者构造器,它们就不能理解它为什么不能正常地工作。

所有文章无条件开放,顺手点个赞不为过吧!

                                                           

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

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

相关文章

【软考】系统架构设计师-信息系统基础

#信息系统基础核心知识点 信息系统5个基本功能&#xff1a;输入、存储、处理、输出和控制 诺兰模型&#xff1a;信息系统计划的阶段模型&#xff0c;6阶段 初始阶段&#xff0c;传播阶段&#xff0c;控制阶段&#xff0c;集成阶段&#xff0c;数据管理阶段&#xff0c;成熟阶…

【论文笔记】Large Brain Model (LaBraM, ICLR 2024)

Code: https://github.com/935963004/LaBraM Data: 无 目录 AbstractIntroductionMethodNeural tokenizer training&#xff1a;Pre-training LaBraM&#xff1a; ResultsExperimental setup&#xff1a;Pre-training result&#xff1a;Comparison with SOTA&#xff1a;Pre-t…

瀚海微SD NAND之SD 协议(34)1.8V信号的时序

固定数据窗口输出时序(SDR12、SDR25、SDR50) 固定数据窗口插卡输出时序如下图所示&#xff0c;SDR12、SDR25、SDR50的输出时序 有效窗口由输出延迟(topy)的最小值和最大值指定。 无论温度和电压如何变化&#xff0c;与SDCLK同步的有效数据窗口都是可用的。 输出有效窗口由t…

web——sqliabs靶场——第十三关——报错注入+布尔盲注

发现是单引号加括号闭合的 尝试联合注入 发现不太行&#xff0c;那尝试报错注入。 测试报错注入 unameadmin) and updatexml(1,0x7e,3) -- &passwdadmin&submitSubmit 爆数据库 unameadmin) and updatexml(1,concat(0x7e,database(),0x7e),3) -- &passwdadmin&a…

5、AI测试辅助-生成测试用例思维导图

AI测试辅助-生成测试用例思维导图 创建测试用例两种方式1、Plantuml思维导图版本 (不推荐&#xff09;2、Markdown思维导图版本&#xff08;推荐&#xff09; 创建测试用例两种方式 完整的测试用例通常需要包含以下的元素&#xff1a; 1、测试模块 2、测试标题 3、前置条件 4、…

附录2-pytorch yolov5目标检测

项目地址 https://github.com/ultralytics/yolov5 参考 https://zhuanlan.zhihu.com/p/711356735 目录 1 数据集准备 1.1 images 1.2 labels 1.3 yaml文件 2 环境配置 3 python环境配置 3.1 安装torch 3.2 安装opencv 3.3 安装 ultralytics 4 预训练模型…

CDM(码分复用)发送和接受原理

现在假设主机A、B、C。其对应的码片序列为a、b、c。 现在有&#xff1a; 现在假设A发送比特1&#xff0c;对应发送的是。B不发送。C发送比特0&#xff0c;对应发送。 信号叠加的结果为。 基站X将结果与每一个主机的码片序列做内积。 与A&#xff1a; ,因此A发送了1。 与B…

菜鸟驿站二维码/一维码 取件识别功能

特别注意需要引入 库文 ZXing 可跳转&#xff1a; 记录【WinForm】C#学习使用ZXing.Net生成条码过程_c# zxing-CSDN博客 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using static System.Net.…

华为云鸿蒙应用入门级开发者认证考试题库(理论题和实验题)

注意&#xff1a;考试链接地址&#xff1a;华为云鸿蒙应用入门级学习认证_华为云鸿蒙应用入门级开发者认证_华为云开发者学堂-华为云 当前认证打折之后是1元&#xff0c;之后原价700元&#xff0c;大家尽快考试&#xff01;考试题库里面答案不一定全对&#xff0c;但是可以保证…

Spring Boot与MyBatis-Plus的高效集成

Spring Boot与MyBatis-Plus的高效集成 引言 在现代 Java 开发中&#xff0c;MyBatis-Plus 作为 MyBatis 的增强工具&#xff0c;以其简化 CRUD 操作和无需编写 XML 映射文件的特点&#xff0c;受到了开发者的青睐。本篇文章将带你一步步整合 Spring Boot 与 MyBatis-Plus&…

Elasticsearch:如何部署文本嵌入模型并将其用于语义搜索

你可以按照这些说明在 Elasticsearch 中部署文本嵌入模型&#xff0c;测试模型并将其添加到推理提取管道。它使你能够生成文本的向量表示并对生成的向量执行向量相似性搜索。示例中使用的模型在 HuggingFace上公开可用。 该示例使用来自 MS MARCO Passage Ranking Task 的公共…

uniapp 购物弹窗组件 (微信小程序)

效果图&#xff0c;暂时只适应单规格&#xff0c;居中弹出和下方弹出&#xff0c;如需求不满足&#xff0c;请自行修改代码 &#xff08;更新于24/11/15) 居中显示效果 下方弹出效果 html <template><view class"" v-if"show":class"mod…

(Linux)搭建静态网站——基于http/https协议的静态网站

简单了解nginx配置文件 1.下载并开启nginx服务 下载 [rootlocalhost ~]# dnf install nginx -y开启 [rootlocalhost ~]# systemctl restart nginx 1.(1)搭建静态网站——基于http协议的静态网站 实验1&#xff1a;搭建一个web服务器&#xff0c;访问该服务器时显示“hello w…

爬取网易云音乐热歌榜:从入门到实战

爬取网易云音乐热歌榜&#xff1a;从入门到实战 前提声明 爬虫应遵守目标网站的robots.txt协议&#xff0c;尊重版权和用户隐私。本代码仅供学习和研究使用&#xff0c;不得用于商业用途。请确保在合法合规的前提下使用本代码。本代码所爬音乐为公开可选择的音乐 目录 引言…

Quality minus junk论文阅读

Quality minus junk论文阅读 文章目录 Quality minus junk论文阅读 AbstractTheoretical FrameworkEmpirical AnalysisDataQuality scorePortfoliosEx ante quality forecasts fundamentals Results and DiscussionThe price of qualityUnderstanding the price of quality: th…

利用RAGflow和LM Studio建立食品法规问答系统

前言 食品企业在管理标准、法规&#xff0c;特别是食品原料、特殊食品法规时&#xff0c;难以通过速查法规得到准确的结果。随着AI技术的发展&#xff0c;互联网上出现很多AI知识库的解决方案。 经过一轮测试&#xff0c;找到问题抓手、打通业务底层逻辑、对齐行业颗粒度、沉…

类和对象——拷贝构造函数,赋值运算符重载(C++)

1.拷⻉构造函数 如果⼀个构造函数的第⼀个参数是自身类类型的引用&#xff0c;且任何额外的参数都有默认值&#xff0c;则此构造函数也叫做拷贝构造函数&#xff0c;也就是说拷贝构造是⼀个特殊的构造函数。 // 拷贝构造函数//d2(d1) Date(const Date& d) {_year d._yea…

浅谈软件开发中的yield关键字:从餐厅服务理解异步编程之美

在现代软件开发中&#xff0c;处理大量数据流时经常会遇到性能和内存消耗的问题。传统的编程方式往往是一次性获取所有数据&#xff0c;这就像餐厅厨师要把所有菜品做完才上菜一样&#xff0c;既不高效也不够灵活。而yield关键字的出现&#xff0c;为我们提供了一种优雅的解决方…

散户持股增厚工具:智能T0算法交易

最近市场很多都说牛市&#xff0c;但是大多数朋友怎么来的又怎么吐出去了。这会儿我们用T0的智能算法交易又可以增厚我们的持仓收益。简单来说&#xff0c;就是基于用户原有的股票持仓&#xff0c;针对同一标的&#xff0c;配合智能T0算法&#xff0c;每天全自动操作&#xff0…

[ 网络安全介绍 1 ] 什么是网络安全?

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…