Java 中的变长参数是如何实现的
结论
javac编译器会将变长参数放置在一个数组中,然后调用对应的方法- 在
class文件中,接受变长参数的方法,其ACC_VARARGSflag 会被置位(其值为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
对比后发现
f1和f2的 descriptor 相同,都是([Ljava/lang/String;)I(说明它们的参数都是String[],返回值都是int)f1和f2的flags不同(前者是0x0000,后者是0x0080)f1和f2的Code属性几乎完全相同(只有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.VARARGS 是 0x00000080。
综上,如果一个方法接受变长参数,那么在对应的 class 文件中,这个方法的 VARARGS flag 会被置位(其值为 0x0080)。除此之外,这个方法与接受数组参数的方法没有差异。
在 Java Virtual Machine Specification 的 4.6. Methods 中,可以看到关于各个 flag 含义的表格
相关的描述(就在这个表格下方不远处)如下 ⬇️
The
ACC_VARARGSflag 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 theACC_VARARGSflag set to 1. All other methods must be compiled with theACC_VARARGSflag set to 0.