可变元参数
在 Java 5.0 之前,每个 Java 方法都有固定数量的形参。Java 5.0 开始,可以将方法的最后一个参数声明为 Type... params
形式,表示可以接收 0 个或多个 Type
类型的参数。这种形参称为 "可变元参数",含这种形参方法称为 "可变元方法",不含这种形参的方法称为 "固定元方法"(Reference 1)。
下面的代码中,我们定义了一个可变元方法,它首先输出接收到的字符串个数,然后再一个一个输出它们。在 main 方法中,我们调用了该方法:
public static void main(String[] args) {
print("one", "two", "three");
}
public static void print(String... strs) {
System.out.println(strs.length);
for (String s : strs) {
System.out.println(s);
}
}
复制代码
输出结果:
3
one
two
three
复制代码
让我们来研究一下Java 是怎么做的。反编译上面那段代码,得到(print方法只列出了部分指令):
public static void main(java.lang.String[]);
Code:
0: iconst_3
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String one
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String two
13: aastore
14: dup
15: iconst_2
16: ldc #5 // String three
18: aastore
19: invokestatic #6 // Method print:([Ljava/lang/String;)V
22: return
public static void print(java.lang.String...);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: arraylength
5: invokevirtual #8 // Method java/io/PrintStream.println:(I)V
...
复制代码
可以看到,在 main 方法内部,首先创建了一个长度为 3 的 String 类型的数组。然后依次将字符串 "one"、"two"、"three" 存入,然后在调用 print 方法时将该数组传递过去。在 print 方法内部,也是按照数组处理。
除了使用反编译,我们也可以使用反射的方式来分析可变元方法。执行如下代码(Test 是上面的 print 方法所属的类):
Method[] methods = Test.class.getMethods();
for (Method method : methods) {
if ("print".equals(method.getName())) {
for (Class cl : method.getParameterTypes()) {
System.out.printf("%s ", cl.getName());
}
System.out.println();
}
}
复制代码
会得到输出:
[Ljava.lang.String;
复制代码
由以上可见,可变元参数本质上是一个数组。在调用可变元方法时,Java 先创建一个长度等于要传递的参数个数的数组,并依次将要传递的参数存入,然后将该数组传递给被调用方法。
最后,额外说明三点:
- 可以传递 0 个参数给可变元形参,这其实是传递了一个长度为 0 的数组。相比前面的例子,也就是少了数据放入的过程。这使得传参具有了更大的灵活性,而不需要引入额外的重载。
- 一个方法最多只能有一个可变元参数,且必须是最后一个形参。
- 可以将一个已存在且最后一个形参是数组类型的方法安全地重定义为可变元方法,而不会引起破坏。
foreach 循环
foreach 循环又称为 "增强 for 循环" 或 "增强 for 语句",是 Java 5.0 引入的另一个很有用的特性,通过它可以很方便地完成数组及集合元素的遍历。有资料称,这个特性是受到了 C# 的启发(Reference 2)。
举两个例子,Java 5.0 之前,遍历数组只能通过一个步进的游标顺序取出元素:
int[] ints = {0, 1, 2, 3};
for (int i = 0; i < ints.length; i++) {
System.out.println(ints[i]);
}
复制代码
遍历列表也是类似的方式,或使用迭代器:
List strings = ...;
Iterator iter = strings.iterator();
while (iter.hasNext()) {
System.out.println(iter.next());
}
复制代码
foreach 循环的语法大致如下:
for ({VariableModifier} Type e : elements) {
...
}
复制代码
其中,elements 是数组类型或实现了 Iterable 接口的类型,Type 类型的变量 e 用来存储每个元素的值。
使用 foreach 循环,实现同样的遍历可以更加简练,下面是实现上述遍历的两个方法:
public static void forEachArray(int[] ints) {
for (int i : ints) {
System.out.println(i);
}
}
public static void forEachIterable(Iterable<String> strings) {
for (String s : strings) {
System.out.println(s);
}
}
复制代码
为了一探究竟,我们像以往一样对上述两个方法进行反编译。
首先看使用 foreach 迭代数组:
public static void forEachArray(int[]);
Code:
0: aload_0
1: astore_1
2: aload_1
3: arraylength
4: istore_2
5: iconst_0
6: istore_3
7: iload_3
8: iload_2
9: if_icmpge 31
12: aload_1
13: iload_3
14: iaload
15: istore 4
17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
20: iload 4
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: iinc 3, 1
28: goto 7
31: return
复制代码
上述代码中,首先用两个变量分别将数组对象的引用、数组长度存下来。然后通过指令 5 和 6 定义了一个计数器。通过 7、8、9 比较计数器的值与数组长度决定语句跳转,如果计数器大于等于数组长度则跳至指令 31;否则继续,指令 12 ~ 22 则完成了输出功能,并在指令 25 完成计数器加 1 的功能,之后在指令 28 跳转到指令 7,重新比较计数器和数组长度。换句话说,上面的代码可以看做下面这样:
int[] var1 = var0;
int var2 = var0.length;
for(int var3 = 0; var3 < var2; ++var3) {
int var4 = var1[var3];
System.out.println(var4);
}
复制代码
再看使用 foreach 迭代 Iterable 对象:
public static void forEachIterable(java.lang.Iterable<java.lang.String>);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod java/lang/Iterable.iterator:()Ljava/util/Iterator;
6: astore_1
7: aload_1
8: invokeinterface #5, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
13: ifeq 36
16: aload_1
17: invokeinterface #6, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
22: checkcast #7 // class java/lang/String
25: astore_2
26: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_2
30: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: goto 7
36: return
复制代码
上述代码中,首先调用 Iterable 的 iterator 方法得到其迭代器,然后不断通过迭代器的 hasNext 方法,如果结果非 0 (0 即 false) 则继续调用迭代器的 next 方法获取元素,并完成打印输出,之后跳转至指令 7 重新计算 hasNext;如果结果为 0 则跳转至指令 36。用等价的代码表示,就像下面这样:
Iterator var1 = var0.iterator();
while(var1.hasNext()) {
String var2 = (String)var1.next();
System.out.println(var2);
}
复制代码
总结一下,foreach 循环可用于迭代数组或实现了 Iterable 接口的对象的遍历。如果是遍历数组,在编译阶段会将其还原为根据数组下标的遍历;而如果是遍历 Iterable 的对象,则会转化为通过迭代器遍历。另外,可以参考 Java 语言规范关于增强 for 语句的解释,那里有对增强 for 语句的精确语法定义及更详细的说明。
关于 foreach 的额外说明
为什么 java 要用冒号而不像很多语言那样使用 in 作为变量和被迭代对象之间的分隔
相比于 C# 中的 foreach(Type var in xxx)
、python 中的 for var in xxx
等,Java 的 foreach 语法似乎不那么直观。这是因为 Java 在设计之初并没有将 in
作为关键字保留,以致于 in
可以出现在正常的程序中。Java 本身也使用了这个词作为某些变量,如 System.in
。
foreach 所不能做的事
使用 foreach 只能对整个数组或 Iterable 对象做遍历,而无法做部分遍历。
RandomAccess
Java 1.4 引入了 RandomAccess 接口,用以表示一个 List 实现支持快速随机访问,java.util.ArrayList
就实现了此接口。根据 JDK 文档的描述,似乎对于 ArrayList 这样的类使用游标的方式做遍历更快速。但根据测试,Java 并没有这么做,仍然是将 foreach 转为迭代器遍历的方式。其中的一个原因可能是二者的差距并不明显。
References
- Java语言规范(Java SE 8)
- Java 核心技术(第10版) 卷I ch5.5、ch1.4、ch3.10.1、ch9.1.3
- Java doc(Java SE 8)