Java学习之Varargs机制

news2024/11/26 2:52:05

概述

Varargs,即variable number of arguments,variable arguments。中文一般译为,可变长度参数,或简称可变参数,参数具体来说是形参。
自JDK5引入,借助这一机制,可以定义能和多个实参相匹配的形参。
JDK5之前,无法在Java程序里定义实参个数可变的方法,因为Java要求实参(Arguments)和形参(Parameters)的数量和类型都必须逐一匹配,而形参的数目是在定义方法时已确定。尽管可以通过重载机制,为同一个方法提供带有不同数量的形参的版本,但是这仍然不能达到让实参数量任意变化的目的。

定义实参个数可变的方法
只要在一个形参的类型参数名之间加上,则可以让它和不确定个实参相匹配。而一个带有这样的形参的方法,就是一个实参个数可变的方法。只有最后一个形参才能被定义成能和不确定个实参相匹配。故而,一个方法里最多只能有一个这样的形参,而且是在最后的位置。
编译器会在背地里把这最后一个形参转化为一个数组形参,并在编译出的class文件里作上一个记号(什么记号?),表明这是个实参个数可变的方法。

根据J2SE 1.5的语法,在前面的空白字符可有可无。故而有两种写法:(Object … args)(Object… args)。Java Code Conventions。究竟哪一种写法比较正统?考虑到数组参数也有Object [] argsObject[] args两种书写方式,而推荐的写法是不在[]前添加空白字符。

调用实参个数可变(包括0个)的方法:sumUp(1, 3, 5, 7);,编译器会转化为数组包裹实参的形式:sumUp(newint[]{1, 2, 3, 4});
零个实参也可以通过编译:sumUp();,编译器转化为空数组:sumUp(newint[]{});
处理个数可变的实参的办法,和处理数组实参的办法基本相同。所有的实参,都被保存到一个和形参同名的数组里。

转发个数可变的实参
有时候,在接受一组个数可变的实参之后,还要把它们传递给另一个实参个数可变的方法。因为编码时无法知道接受来的这一组实参的数目,所以“把它们 逐一写到该出现的位置上去”的做法并不可行。不过,这并不意味着这是个不可完成的任务,因为还有另外一种办法,可以用来调用实参个数可变的方法。

在J2SE 1.5编译器中,实参个数可变的方法是最后带了一个数组形参的方法的特例。因此,事先把整组要传递的实参放到一个数组里,然后把这个数组作为最后一个实参,传递给一个实参个数可变的方法,不会造成任何错误。借助这一特性,就可以顺利的完成转发了。

public class PrintfSample {
	public static void main(String[] args) {
		// 打印:Pi:3.141593 E:2.718282
		printOut("Pi:%f E:%f/n", Math.PI, Math.E);
	}
	private static void printOut(String format, Object... args) {
		// J2SE 1.5里PrintStream新增的printf(String format, Object... args)方法
		System.out.printf(format, args);
	}
}

Java里的printfsprintf,C语言里的printf(按一定的格式输出字符串)和sprintf(按一定的格式组合字符串)即是Varargs机制的例子。按一定的格式输出字符串的功能,可以通过调用PrintStream.printf(String format, Object… args)方法来实现。按一定的格式组合字符串的工作,则可以通过调用String.String format(String format, Object… args)静态方法来进行。

尽管在背地里,编译器会把能匹配不确定个实参的形参,转化为数组形参;而且也可以用数组包实参,再传递给实参个数可变的方法;但是,这并不表示能匹配不确定个实参的形参和数组形参完全没有差异。

一个明显的差异是,如果按照调用实参个数可变的方法的形式,来调用一个最后一个形参是数组形参的方法,只会导致一个cannot be applied to的编译错误:

private static void testOverloading(int[] i) {
	System.out.println("A");
}
public static void main(String[] args) {
	// 编译出错
	testOverloading(1, 2, 3);
}

由于这一原因,不能在调用只支持用数组包裹实参的方法的时候(例如在不是专门为J2SE 1.5设计第三方类库中遗留的那些),直接采用这种简明的调用方式。
如果不能修改原来的类,为要调用的方法增加参数个数可变的版本,而又想采用这种简明的调用方式,那么可以借助“引入外加函数(Introduce Foreign Method)”和“引入本地扩展(Intoduce Local Extension)”的重构手法来近似的达到目的。

当个数可变的实参遇到泛型
泛型,可以在一定条件下把一个类型参数化。泛型机制不能和个数可变的形参配合使用。如果把一个能和不确定个实参相匹配的形参的类型,用一个标识符来代表,那么编译器会给出一个generic array creation的错误:

private static void testVarargs(T... args) {
	// 编译出错
}

原因:J2SE 1.5中的泛型机制有一个内在约束,不能拿用标识符来代表的类型来创建这一类型的实例。
用数组包裹的做法,并不受这个约束的限制:

private static void testVarargs(T[] args) {
	for (int i = 0; i < args.length; i++) {
		System.out.println(args[i]);
	}
}

重载中的选择问题

Java支持重载,允许在同一个类拥有许多只有形参列表不同的方法,然后由编译器根据调用时的实参来选择到底要执行哪一个方法。传统上的选择,基本是依照特殊者优先的原则来进行。一个方法的特殊程度,取决于为了让它顺利运行而需要满足的条件的数目,需要条件越多的越特殊。
在引入Varargs机制之后,这一原则仍然适用,传统上,一个重载方法的各个版本之中,只有形参数量与实参数量正 好一致的那些有被进一步考虑的资格。但是Varargs机制引入之后,完全可以出现两个版本都能匹配,在其它方面也别无二致,只是一个实参个数固定,而一 个实参个数可变的情况。
遇到这种情况时,所用的判定规则是“实参个数固定的版本优先于实参个数可变的版本”。

@Slf4j
public class OverloadingSampleA {
    public static void main(String[] args) {
        // 打印A           
        testOverloading(1);
        // 打印出B           
        testOverloading(1, 2);
        // 打印出C       
        testOverloading(1, 2, 3);
    }

    private static void testOverloading(int i) {
        log.info("A");
    }

    private static void testOverloading(int i, int j) {
        log.info("B");
    }

    private static void testOverloading(int i, int... more) {
        log.info("C");
    }
}

在编译器看来,同时有多个方法具有相同的优先权,它就会陷入无法就到底调用哪个方法作出一个选择的状态。在这样的时候,它就会产生一个reference to 被调用的方法名 is ambiguous的编译错误。
在引入Varargs机制之后,这种可能导致迷惑的情况,又增加了一些。例如现在可能会有两个版本都能匹配,在其它方面也如出一辙,而且都是实参个数可变的冲突发生。


public class OverloadingSampleB {
    public static void main(String[] args) {
        // 编译出错: ambiguous method call, both a and b match
        testOverloading(1, 2, 3);
    }

    private static void testOverloading(Object... args) {
    }

    private static void testOverloading(Object o, Object... args) {
    }
}

由于自动装箱和自动拆箱机制的存在,所以还可能发生两个版本都能匹配,而且都是实参个数可变,其它方面也一模一样,只是一个能接受的实参是基本类型,而另一个能接受的实参是包裹类的冲突发生。


public class OverloadingSampleC {
    public static void main(String[] args) {
        // 编译出错 
        testOverloading(1, 2);
        //还是编译出错 
        testOverloading(new Integer(1), new Integer(2));
    }

    private static void testOverloading(int... args) {
    }

    private static void testOverloading(Integer... args) {
    }
}

归纳总结
用数组包裹相比,实参个数可变的方法,在调用时传递参数的操作更为简单,含义也更为清楚。不过也有局限。

用数组包裹实参

直接看代码:

private int sum(int[] nums){
    return 0;
}

Varargs机制


Varargs机制,允许直接定义能和多个实参相匹配的形参。从而,可以用一种更简单的方式,来传递个数可变的实参。

工作原理

可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。

在这里插入图片描述

语法糖

语法糖是一种几乎每种语言或多或少都提供过的一些方便程序员开发代码的语法,编译期间以特定的字节码或者特定的方式对这些语法做一些处理,开发者就可以直接方便地使用。这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高性能、或能提升语法的严谨性、或能减少编码出错的机会。Java提供给用户大量的语法糖,比如泛型、自动装箱、自动拆箱、foreach循环、变长参数、内部类、枚举类、断言(assert)等。
可变长度参数
先讲可变长度参数:

public static void main(String[] args) {
    print("000", "111", "222", "333");
}
     
public static void print(String... strs) {
    for (int i = 0; i < strs.length; i++) {
        System.out.println(strs[i]);
    }
}

print方法的参数的意思是表示传入的String个数是不定的,运行结果:
000
111
222
333
我用数组遍历的方式成功地将输入的参数遍历出来了,这说明两个问题:
1、可以使用遍历数组的方式去遍历可变参数
2、可变参数是利用数组实现的
其实main函数也可以这么写,完全可以:

String[] strs = {"000", "111", "222", "333"};
print(strs);

可变长度参数必须作为方法参数列表中的的最后一个参数且方法参数列表中只能有一个可变长度参数。

foreach循环原理
自己写一个ArrayList,想用foreach循环遍历一下看一下写的效果,结果报空指针异常。代码:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("111");
    list.add("222");
     
    for (String str : list) {
        System.out.println(str);
    }
}

javap反编译一下:javap -verbose TestMain.class
截取一段信息:

public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #16                 // class java/util/ArrayList
         3: dup
         4: invokespecial #18                 // Method java/util/ArrayList."<in
it>":()V
         7: astore_1
         8: aload_1
         9: ldc           #19                 // String 111
        11: invokeinterface #21,  2           // InterfaceMethod java/util/List.
add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: ldc           #27                 // String 222
        20: invokeinterface #21,  2           // InterfaceMethod java/util/List.
add:(Ljava/lang/Object;)Z
        25: pop
        26: aload_1
        27: invokeinterface #29,  1           // InterfaceMethod java/util/List.
iterator:()Ljava/util/Iterator;

new、dup、invokespecial这些本来就是字节码指令表内定义的指令,虚拟机会根据这些指令去执行指定的C++代码,完成每个指令的功能。关键21、22行的iterator,得出结论:在编译的时候编译器会自动将对for这个关键字的使用转化为对目标的迭代器的使用,这就是foreach循环的原理。得出两个结论:
1、ArrayList之所以能使用foreach循环遍历,是因为ArrayList所有的List都是Collection的子接口,而Collection是Iterable的子接口,ArrayList的父类AbstractList正确地实现了Iterable接口的iterator方法。之前我自己写的ArrayList用foreach循环直接报空指针异常是因为我自己写的ArrayList并没有实现Iterable接口
2、任何一个集合,无论是JDK提供的还是自己写的,只要想使用foreach循环遍历,就必须正确地实现Iterable接口。
实际上,这种做法就是23中设计模式中的迭代器模式。

数组并没有实现Iterable接口,为什么可以用foreach?

public static void main(String[] args) {
    int[] ints = {1,2,3,4,5};
    for (int i : ints) {
        System.out.println(i);
    }
}

反编译:

0: iconst_2
1: newarray       int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: astore_1
12: aload_1
13: dup
14: astore        5
16: arraylength
17: istore        4
19: iconst_0
20: istore_3
21: goto          39
24: aload         5
26: iload_3
27: iaload
28: istore_2
29: getstatic     #16         // Field java/lang/System.out:Ljava/io/PrintStream;
32: iload_2
33: invokevirtual #22       // Method java/io/PrintStream.println:(I)V
36: iinc          3, 1
39: iload_3
40: iload         4
42: if_icmplt     24
45: return

完整的这段main函数对应的45个字节码指令,涉及一些压栈、出栈、推送等。简单对照字节码指令表之后,我个人对于这45个字节码的理解是Java将对于数组的foreach循环转换为对于这个数组每一个的循环引用。

【强制】相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object。
说明: 可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)
正例:public User getUsers(String type, Integer... ids)
白话:
用处不大,可以用重载方法或者数组参数代替。
一般应用在日志的 API 定义上,用于传不定的日志参数。

参考

可变长度参数以及foreach循环原理

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

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

相关文章

基于Java+SpringBoot+Vue的流浪动物救助平台设计与实现

博主介绍&#xff1a;✌擅长Java、微信小程序、Python、Android等&#xff0c;专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不然下次找不到哟 Java项目精品实战案…

【前端|CSS系列第2篇】CSS零基础入门之常用样式属性

欢迎来到CSS零基础入门系列的第二篇博客&#xff01;作为前端开发的关键技术之一&#xff0c;CSS&#xff08;层叠样式表&#xff09;能够为网页添加各种样式和布局效果。对于前端零基础的小白来说&#xff0c;了解和掌握CSS的常用样式属性是入门的关键。本篇博客将带你深入了解…

基本的组合门电路、以及二极管、三极管(详细讲解)

基本门电路、二极管、三极管 1.基本的组合门电路1.1 与门&#xff08;AND Gate&#xff09;&#xff1a;2.2 或门&#xff08;OR Gate&#xff09;&#xff1a;1.3 非门&#xff08;NOT Gate&#xff09;&#xff1a;1.4 异或门&#xff08;XOR Gate&#xff09;&#xff1a; 2…

chatgpt赋能python:Python面向对象和面向过程编程

Python面向对象和面向过程编程 Python是一种高级编程语言&#xff0c;可以使用不同的编程风格进行开发。本篇文章将讨论Python面向对象和面向过程编程风格的概念、差异和优缺点。 什么是面向对象编程&#xff1f; 面向对象编程是一种编程范式&#xff0c;它将现实世界的概念…

TP6在composer包里写控制器

前提&#xff1a;首先要了解下如何自建composer包。 1.先建一个空包&#xff0c;加一个文件&#xff1a;composer.json {"name": "test/ctrs","type": "library","license": "MIT","autoload": {&quo…

Python一行命令搭建HTTP服务器并外网访问+-+内网穿透

文章目录 1.前言2.本地http服务器搭建2.1.Python的安装和设置2.2.Python服务器设置和测试 3.cpolar的安装和注册3.1 Cpolar云端设置3.2 Cpolar本地设置 4.公网访问测试5.结语 转载自远程内网穿透的文章&#xff1a;【Python】快速简单搭建HTTP服务器并公网访问「cpolar内网穿透…

PS扣签名

打开Photoshop CS6&#xff0c;依次点击“文件”-“打开”&#xff0c;把签名照导入进来。 在“选择”菜单下点击“色彩范围”。 此时鼠标形状变成了一支笔&#xff0c;点击签名上黑色的地方&#xff0c;适当调整颜色容差&#xff0c;点击“确定”完成选择。 按住CtrlJ组…

基于Java园区停车管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

【SWAT水文模型】率定参数选择及校准技巧

SWAT模型率定参数选择及校准技巧 水量平衡与径流&#xff08;Water Balance And Stream Flow&#xff09;1 基本水量平衡和总径流校准&#xff08;Basic Water Balance & Total Flow Calibration&#xff09;1.1 校准地表径流&#xff1a;1.2 校准地下径流&#xff1a; 2 流…

如何查看 Facebook 公共主页的广告数量上限?

作为Facebook的资深人员&#xff0c;了解如何查看公共主页的广告数量上限对于有效管理和优化广告策略至关重要。本文将详细介绍如何轻松查看Facebook公共主页的广告数量上限&#xff0c;以帮助您更好地掌握广告投放策略。 一、什么是Facebook公共主页的广告数量上限&#xff1f…

如何使用《水经注地图服务》快速发布MBTiles数据

《水经注地图服务》的快速发布功能是一个能够帮助用户实现快速发布地图服务的功能&#xff0c;并且提供常规情况下大多数用户所需的默认配置&#xff0c;让用户在发布地图时更加便捷。 今天为大家分享如何利用《水经注地图服务》快速发布MBTiles地图数据。 准备工作 离线示例…

C语言小项目之扫雷(进阶版)

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:C语言学习分享⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5; 带你学习更多C语言知识   &#x1f51d;&#x1f51d; 扫雷小项目 1. 前期准备2. 初始化…

phpRedis扩展安装以及session redis存储

1.下载redis扩展&#xff08;redis扩展各个版本下载地址&#xff1a;https://pecl.php.net/package/redis&#xff09; wget https://pecl.php.net/get/redis-3.1.6.tgz 2.解压下载的redis扩展 tar zxvf redis-3.1.6.tgz 3.用phpize生成configure配置文件 phpize 4.查找p…

账号安全总结-业务安全测试实操(27)

电子邮件账号泄露事件 电子邮箱业务基于计算机和通信网的信息传递业务,利用电信号传递和存储信息,为用户传送电子信函、文件数字传真、图像和数字化语音等各类型的信息。电子邮件最大的特点是,人们可以在任何地方、任何时间收、发信件,解决了时空的限制,大大提高了工作效…

深度学习编译器汇总

深度学习的发展对个科学领域产生了深远的影响。它不仅在自然语言处理&#xff08;NLP&#xff09;和计算机视觉&#xff08;CV&#xff09;等人工智能领域显示出显著的价值&#xff0c;而且在电子商务、智慧城市和药物发现等更广泛的应用领域也取得了巨大的成功。随着卷积神经网…

Django学习笔记-VS Code本地运行项目

截止到上一章节&#xff1a;Django 学习笔记-Web 端授权 AcWing 一键登录&#xff0c;我们的项目一直是部署在云服务器上&#xff0c;包括编写代码以及调试运行也是在云服务器上&#xff0c;现在我们尝试将其放回本地运行。 CONTENTS 1. 将项目传到本地2. 虚拟环境配置3. 修改项…

如何录音转文字?这份录音转文字教程你必须知道

在现代快节奏的工作环境中&#xff0c;电脑会议录音转文字成为了一项非常重要的任务。但是很多人不知道电脑会议录音转文字怎么转&#xff1f;如果你也正有这样的疑问&#xff0c;那么你就来对地方了&#xff01;在本篇文章中&#xff0c;我们将为你介绍几款备受推崇的录音转文…

联想校招雇品年轻化:硬科技「校招+雇品」的创新打法

联想计划3年内增加12,000名研发人才&#xff0c;并明确20%的New Hire将来自校园招聘。 人才梯队的构成预示着企业未来的发展方向与加速度。联想对年轻人才关注与吸引从未放慢脚步&#xff0c;始终相信年轻即代表未来。更多年轻科技人才加入&#xff0c;会为企业注入创新活力&a…

LeetCode 75 —— 62. 不同路径

LeetCode 75 —— 62. 不同路径 一、题目描述&#xff1a; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &…

Appium之xpath定位详解

目录 一、基础定位 二、contains模糊定位 三、组合定位 四、层级定位 前面也说过appium也是以webdriver为基的&#xff0c;对于元素的定位也基本一致&#xff0c;只是增加一些更适合移动平台的独特方式&#xff0c;下面将着重介绍xpath方法&#xff0c;这应该是UI层元素定位…