你知道Java方法能定义多少个参数吗?

简介: 一个Java方法,最多能定义多少参数呢?我知道这是一个很无聊的问题,即使能定义一万个,十万个,谁又会真的去这么做呢。但是作为一个Coder,最重要的不就是好奇心吗,没有好奇心,和一条咸鱼又有什么区别呢?

一:为什么研究这么无聊的问题

这两天在读一本老书《Orange'S 一个操作系统的实现》,把丢了很长时间没研究的操作系统又重新拾起来了,在第三章讲解“保护模式”时,作者提到了调用门描述符中的Param Count只有5位,也就是说,最多只支持32个参数,这本来只是一个不是特别重要的细节,但是却勾起了我的思索:在JVM中,一个Java方法,最多能定义多少参数呢?我知道这是一个很无聊的问题,即使能定义一万个,十万个,谁又会真的去这么做呢。但是作为一个Coder,最重要的不就是好奇心吗,没有好奇心,和一条咸鱼又有什么区别呢?

二:实地考察

这种问题,第一步当然就是看看JVM中关于方法的定义,这里以openJDK10中的HotSpot为例。
在ConstMethod中,代表参数数量的字段为_size_of_parameters。

      u2                _size_of_parameters;         // size of the parameter block (receiver + arguments) in words 

_size_of_parameters的类型为u2,在JVM中,u2为2个字节长,那么理论上来说,HotSpot支持的方法最大参数数量为2^16 - 1,即65535。
这个答案究竟是否正确呢?实践出真知!
当然我不会傻到真的去一个个定义65535个参数,那我岂不成了“数一亿粒米”的幼儿园老师了?Coder就得按照Coder的办法:

    public static void main(String[] args) {
        for (int i = 0; i < 65535; i++) {
            System.out.print("int a" + i + ",");
        }
    }

完美解放了生产力 。
生成完参数列表,定义好方法,当我满怀信心的开始编译时,编译器给了我狠狠一刀:

image

居然不是65535?那应该是多少呢?难道是一个字节长?废话不多说,我立即来实验了下255个参数,编译通过,再试了一下256,和65535时一样报错。那么结果很明显了,Java方法最多可以定义255个参数。
我查看了下Javac源码,在生成方法的字节码时,有方法参数数量限制判断:

    if (Code.width(types.erasure(env.enclMethod.sym.type).getParameterTypes()) + extras > ClassFile.MAX_PARAMETERS) {
        log.error(tree.pos(), "limit.parameters");
        nerrs++;
    }

其中 ClassFile.MAX_PARAMETERS = 255。

事情到这里我很不甘心,HotSpot中明明是用两个字节长来定义的方法参数数量,莫非只是Javac在编译过程中做了限制?只要能成功编译出一个有256个参数的java方法,在虚拟机中一试便知,但是怎么才能绕过Javac呢?我觉得主要有以下两种办法:

  1. 修改Javac源码,干掉以上参数限制这一段代码,再重新编译;
  2. 利用字节码修改工具,硬改字节码,加上一个拥有256个参数的方法。

第一种方法看似简单,但是其实从openJDK中提取出来的Javac项目不能直接run,需要很多配置,而且源码依赖了很多jdk中的不可见类,操作起来很麻烦。所以这里我采用了第二种方法,工具选用的是老朋友javassist。
其实javassist使用起来很简单,这里我只需要对一个已有的class文件加上一个新方法即可:

        try {
            StringBuilder sb = new StringBuilder();

            sb.append("public static void testMax(");

            for (int i = 0; i < 256; i++) {
                sb.append("int a" + i);
                if(i < 255) {
                    sb.append(",");
                }
            }
            sb.append("){}");

            ClassPool cPool = new ClassPool(true);
            cPool.insertClassPath("/Users/wanginbeijing/Documents/MyProgramings/java/Mine/test/src");
            CtClass cClass = cPool.get("com.wangxiandeng.test.Test");
            CtMethod newMethod = CtNewMethod.make(sb.toString(), cClass);
            cClass.addMethod(newMethod);
            cClass.writeFile("/Users/wanginbeijing/Documents/MyProgramings/java/Mine/test/src");
        } catch (NotFoundException e) {
            e.printStackTrace();
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (
                IOException e) {
            e.printStackTrace();
        }

以上就通过javassist成功的给Test.class 文件加上了一个拥有256个参数的方法testMax()。现在让我们运行下Test.class试试:

    java com.wangxiandeng.test.Test

没想到这次虽然瞒过了编译器,却没有过的了虚拟机这一关,运行直接报错了:
错误: 加载主类 com.wangxiandeng.test.Test 时出现 LinkageError

        java.lang.ClassFormatError: Too many arguments in method signature in class file com/wangxiandeng/test/Test

看样子Java不仅仅在编译期会对方法参数数量做限制,在虚拟机运行期间同样会干这件事。

本着一查到底的精神,我在HotSpot源码中搜索了下上面报的错误,找到了虚拟机检查参数数量的地方:

Method* ClassFileParser::parse_method(const ClassFileStream* const cfs,
                                      bool is_interface,
                                      const ConstantPool* cp,
                                      AccessFlags* const promoted_flags,
                                      TRAPS) {
      ......
      if (_need_verify) {
            args_size = ((flags & JVM_ACC_STATIC) ? 0 : 1) +verify_legal_method_signature(name, signature, CHECK_NULL);

            if (args_size > MAX_ARGS_SIZE) {
                  classfile_parse_error("Too many arguments in method signature in class file %s", CHECK_NULL);
            }
      }
      ......
}

可见虚拟机在解析class文件中的方法时,会判断参数数量args_size是否大于MAX_ARGS_SIZE,如果大于则就会报错了。MAX_ARGS_SIZE为255。

这里有一点需要注意,在计算args_size时,有判断方法是否为static方法,如果不是static方法,则会在方法原有参数数量上再加一,这是因为非static方法会添加一个默认参数到参数列表首位:方法的真正执行者,即方法所属类的实例对象。
事情到这里总算大概明白了,Java static方法的参数最多只能有255个,非static方法最多只能有254个。虽然远不及我刚开始推测的65535个,但是这也完全够用了,毕竟你敢在你的项目里定义一个255个参数的方法而保证不被人打死吗。
有人可能要问,如果我定义的方法参数是变长参数呢?还有这种限制吗?这当然是没有的,因为变成参数的本质其实就是传递一个数组,你传再多的参数,编译后其实都只是一个数组而已。

其实Java不是限制方法的参数个数,而是限制参数的单位数量,我又回头看了下源码,Javac在编译期计算方法的参数个数时,调用的是这个方法:
Code.width(types.erasure(env.enclMethod.sym.type).getParameterTypes())
Code.width()会根据参数类型,计算出以字节为单位的参数数量:

    public static int width(int typecode) {
        switch (typecode) {
        case LONGcode: case DOUBLEcode: return 2;
        case VOIDcode: return 0;
        default: return 1;
        }
    }

可见对于long、doubule,会占用两个单位数,所以说java方法最多只能有255个参数是不准确的,只能说java方法容许的参数总单位数为255。其实想想也对,java方法调用前会将参数进行压栈,虚拟机本身就应该基于方法传参占用的栈深度去进行限制。

另有知友提醒,《Java虚拟机规范》中其实已经做了说明,我翻了一下,在《Java虚拟机限制》这一小节中有提到:
方法的参数最多有255个,它是由方法描述符(§4.3.3)的定义所限制,如果方法调 用是针对实例或接口方法,那么这个限制也包􏰁着占有一个单元的 this。注意对于定义 在方法描述符中的参数长度来说,每个 long 和 double 参数都会占用两个长度单位,所 以如果有这些类型的话,最终的限制的最大值将会变小。
书中自有黄金屋啊!不过纸上得来终觉浅,绝知此事要躬行,你说是不是呢?

三:一切都结束了

嗯,做完实验,写完文章,我总算把这件事搞明白了,女朋友早已在呼呼大睡,好像我确实很无聊,好像我确实还是一条咸鱼。

听说喜欢点关注的同学都长得帅

相关文章
|
3月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
3月前
|
Java
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
java小工具util系列5:java文件相关操作工具,包括读取服务器路径下文件,删除文件及子文件,删除文件夹等方法
109 9
|
21天前
|
存储 Java 索引
Java快速入门之数组、方法
### Java快速入门之数组与方法简介 #### 一、数组 数组是一种容器,用于存储同种数据类型的多个值。定义数组时需指定数据类型,如`int[]`只能存储整数。数组的初始化分为静态和动态两种: - **静态初始化**:直接指定元素,系统自动计算长度,如`int[] arr = {1, 2, 3};` - **动态初始化**:手动指定长度,系统给定默认值,如`int[] arr = new int[3];` 数组访问通过索引完成,索引从0开始,最大索引为`数组.length - 1`。遍历数组常用`for`循环。常见操作包括求和、找最值、统计特定条件元素等。
|
17天前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
13天前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
46 9
|
18天前
|
安全 Java 程序员
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
48 12
|
19天前
|
算法 Java API
Java 方法注释:规范、实用和高质量的写法
本文深入探讨了如何编写高质量的 Java 方法注释
46 11
|
19天前
|
SQL Java 数据库连接
【潜意识Java】Java中JDBC过时方法的替代方案以及JDBC为什么过时详细分析
本文介绍了JDBC中一些常见过时方法及其替代方案。
37 5
|
3月前
|
Java 编译器
Java重复定义变量详解
这段对话讨论了Java中变量作用域和重复定义的问题。学生提问为何不能重复定义变量导致编译错误,老师通过多个示例解释了编译器如何区分不同作用域内的变量,包括局部变量、成员变量和静态变量,并说明了使用`this`关键字和类名来区分变量的方法。最终,学生理解了编译器在逻辑层面检查变量定义的问题。
Java重复定义变量详解
|
3月前
|
安全 Java 开发者
Java中WAIT和NOTIFY方法必须在同步块中调用的原因
在Java多线程编程中,`wait()`和`notify()`方法是实现线程间协作的关键。这两个方法必须在同步块或同步方法中调用,这一要求背后有着深刻的原因。本文将深入探讨为什么`wait()`和`notify()`方法必须在同步块中调用,以及这一机制如何确保线程安全和避免死锁。
69 4