[Java] 变长参数是如何实现的

104 阅读3分钟

Java 中的变长参数是如何实现的

结论

  1. javac 编译器会将变长参数放置在一个数组中,然后调用对应的方法
  2. class 文件中,接受变长参数的方法,其 ACC_VARARGS flag 会被置位(其值为 0x0080

代码

我们用以下代码来进行探索(请将代码保存为 Main.java

import java.util.Arrays;

public class Main {
  public static void main(String[] args) {
    Arrays.asList("S1", "S2", "S3");
  }
}

如下命令可以编译 Main.java

javac -g -parameters Main.java

编译后会生成 Main.class 文件。

如下命令可以查看 Main.class 文件的内容。

javap -v -p Main

我把和 main(...) 方法相关的部分粘贴在下方。

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=1, args_size=1
         0: iconst_3
         1: anewarray     #7                  // class java/lang/String
         4: dup
         5: iconst_0
         6: ldc           #9                  // String S1
         8: aastore
         9: dup
        10: iconst_1
        11: ldc           #11                 // String S2
        13: aastore
        14: dup
        15: iconst_2
        16: ldc           #13                 // String S3
        18: aastore
        19: invokestatic  #15                 // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
        22: pop
        23: return
      LineNumberTable:
        line 5: 0
        line 6: 23
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      24     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args

利用以上结果,反推出对应的 java 代码应该是这样的 ⬇️

public static void main(String[] args) {
  Arrays.asList(new String[3] {"S1", "S2", "S3"});
}

由此可见,javac 编译器在遇到变长参数时,会自动将这些参数拼装成一个数组。 这么说来,如果一个方法接受变长参数,那么它和接受数组参数的方法似乎没什么区别(但是编译器会特殊处理前者)。 我们用如下的代码来检验一下(请将代码保存为 Main2.java)。 在 Main2 中,f1 方法和 f2 方法都会将参数 strings 中的元素个数作为返回值。

public class Main2 {
  int f1(String[] strings) {
    return strings.length;
  }

  int f2(String... strings) {
    return strings.length;
  }
}

如下命令可以编译 Main2.java

javac -g -parameters Main2.java

编译后,会得到 Main2.class 文件。

如下命令可以查看 Main2.class 的内容。

javap -v -p Main2

我把与 f1 方法以及与 f2 方法相关的部分粘贴如下 ⬇️

  int f1(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)I
    flags: (0x0000)
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: arraylength
         2: ireturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   LMain2;
            0       3     1 strings   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      strings

  int f2(java.lang.String...);
    descriptor: ([Ljava/lang/String;)I
    flags: (0x0080) ACC_VARARGS
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: arraylength
         2: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   LMain2;
            0       3     1 strings   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      strings

对比后发现

  1. f1f2 的 descriptor 相同,都是 ([Ljava/lang/String;)I (说明它们的参数都是 String[],返回值都是 int
  2. f1f2flags 不同(前者是 0x0000,后者是 0x0080
  3. f1f2Code 属性几乎完全相同(只有 LineNumberTable 有差异,那是因为它们在源码中的行号不同)

关于第 2 点,我们在 Method 类的 isVarArgs() 方法 可以看到如下代码

    /**
     * {@inheritDoc}
     * @since 1.5
     * @jls 8.4.1 Formal Parameters
     */
    @Override
    public boolean isVarArgs() {
        return super.isVarArgs();
    }

这里会调用父类的 isVarArgs() 方法,父类是 Executable,在父类中可以找到 对应的代码 ⬇️

    /**
     * {@return {@code true} if this executable was declared to take a
     * variable number of arguments; returns {@code false} otherwise}
     */
    public boolean isVarArgs()  {
        return (getModifiers() & Modifier.VARARGS) != 0;
    }

这里 可以看到 Modifier.VARARGS0x00000080。 综上,如果一个方法接受变长参数,那么在对应的 class 文件中,这个方法的 VARARGS flag 会被置位(其值为 0x0080)。除此之外,这个方法与接受数组参数的方法没有差异。

Java Virtual Machine Specification 的 4.6. Methods 中,可以看到关于各个 flag 含义的表格

image.png

相关的描述(就在这个表格下方不远处)如下 ⬇️

The ACC_VARARGS flag indicates that this method takes a variable number of arguments at the source code level. A method declared to take a variable number of arguments must be compiled with the ACC_VARARGS flag set to 1. All other methods must be compiled with the ACC_VARARGS flag set to 0.