Java中“for”循环的最佳实践。如何定义集合?

3

定义一个“for-each循环”,最好使用以下约定:

情况1

for (String s : xxx.getList())

这是第二种情况:
List<String> list = xxx.getList();
for (String s : list)

在情况1中,方法getList()会被调用多少次?是每个循环都会调用一次还是只在开始时调用一次?谢谢。

10
在getList()方法中添加System.out.println("hello"),你会发现问题所在。 - JB Nizet
1
如果 getList() 每次返回不同的内容会怎么样? ;) - Rob Audenaerde
1
ForEach循环。 - akash
链接的问题涉及不同的主题(传统的for(;;)而不是增强的for(T item: Iterator<T> iterator))。已投票重新开放。 - slim
6个回答

4
不要花时间思考这种类型的优化。
1)如果你能想到一行代码的更改,看起来更有效率但功能相同,那么编写JIT编译器的人肯定也想到了。他们将编译为相同的字节码。
2)可读性比性能更重要,除非在最关键的循环中。只有在您打算将列表用于超出for循环范围的其他内容,并且您的for循环修改了该列表时,才应使用情况二。在这种情况下,携带已更改的列表比重复调用xxx.getList()在语义上更清晰,因为查看函数的部分的程序员可能不会意识到您已经在同一函数中进一步编辑了xxx中的列表。
3)在此类编程中几乎没有硬性规则,“可读性”代表什么意见也不尽相同。
请记住,编译语言与解释语言不同,您编写的代码不是处理器的文字脚本。编译器可以进行几乎您可以想象的每个小优化,以及许多高复杂度的标准编程模式可以被编译器优化,当您试图自己“优化它”时,编译器只会变得混乱,无法识别这是一个标准模式,并在应该进行优化时未能进行优化。
在评论中,slim指出还有另一种情况可以使用情况2-如果您有几个for循环,并且getList是一个昂贵的操作,它会根据请求构造列表,而不仅仅是返回现有对象。

我基本上同意,但“永不”有点太强了。 最近我遇到了非常慢的代码,它执行了 while(x < file.length()) ...而不是size = file.length(); while(x < file.length())。 这个 getList() 方法可能是一个非常昂贵的操作。 - slim
当然,你是正确的。我假设getList只是返回在xxx类中保存的列表变量,但如果它是按请求构建的,那么它可能非常昂贵。 - phil_20686
当然,你的注释中的while循环在每次循环时都会检查条件,而for循环习语for(String s : xxx.getList())只调用列表一次。我想在这种情况下它不是“功能等效”的,因为编译器通常无法保证while循环中的代码永远不会改变条件的值,所以必须手动进行优化。但是这是一个很好的例子。 - phil_20686

1

你可以这样测试:

public static void main(tring[] args) {
   for (String string : getList()) {
       System.out.println(string);
   }
}

private static List<String> getList() {
   System.out.println("getList");
   List<String> l = new ArrayList<String>();
   l.add("a");
   l.add("b");
   return l;
}

...你会发现getList()只被调用了一次。

增强型for循环的语法如下:

 for(Type item : iterable) ...

在你的例子中,getList() 在运行时返回一个Iterable--在这种情况下是一个List。一旦返回了List,循环就可以使用它所需的唯一Iterable
 for (String string : getList()) ...

并且

 List list = getList();
 for (String string : list) ...

...是等价的。第一种形式的优点是简短明了。第二种形式的优点是,如果需要,您可以在之后再次使用列表。

在Eclipse中,您可以通过自动重构(其他IDE也有类似功能)在两种形式之间切换:

从第一种形式开始,选择getList(),右键单击,选择重构 -> 提取局部变量。Eclipse会提示您输入一个变量名。输入list,它将为您创建第二种形式。

从第二种形式开始,在for()语句中选择list,右键单击,选择重构 -> 内联。它会提示您,然后将其改回旧格式。

重构应该产生功能上相同的代码,因此您可以将其用作这两种形式等效的证据。

但要注意,其他循环形式并不像这么聪明。

  while( size < file.length()) {
       ...
  }

...每次循环迭代时都会执行file.length(),而

 long fileLength = file.length();
 while( size < fileLength ) {
     ...
 }

... 仅执行一次 file.length()。传统的 for(;;) 循环也是如此。

上面描述的 Eclipse 重构转换也会在这两种形式之间切换,但行为不同。


0
强化for循环使用提供的Iterable(或数组)来获取一个Iterator以循环遍历项。Iterator仅被获取一次。
所以
for (String s : list) {
    // loop code
}

等同于:

Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String s = iter.next();

    // loop code
}

提示:您可以编写并使用自己的Iterable实现来增强for循环。

0
for (String s : list)

编译后

for (Iterator<String> i = list.iterator(); i.hasNext(); ){...}

for (String s : xxx.getList()){...}

编译后会是:

for (Iterator<String> i = xxx.getList().iterator(); i.hasNext(); ){...}

这意味着第二种情况下getList只会被调用一次。


0
在情况1中,getList()方法仅被调用一次。您可以通过在调用该方法时打印一些内容来验证这一点。
除了其他人已经提供的答案之外,我们还可以查看两种for循环样式产生的字节码。
对于此代码:
List<String> l = getList();
for (String s : l) {
  processItem(s);
}

字节码是:

0:   invokestatic    #4; //Method getList:()Ljava/util/List;
3:   astore_1
4:   aload_1
5:   invokeinterface #5,  1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
10:  astore_2
11:  aload_2
12:  invokeinterface #6,  1; //InterfaceMethod java/util/Iterator.hasNext:()Z
17:  ifeq    37
20:  aload_2
21:  invokeinterface #7,  1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
26:  checkcast       #8; //class java/lang/String
29:  astore_3
30:  aload_3
31:  invokestatic    #9; //Method processItem:(Ljava/lang/String;)V
34:  goto    11
37:  return

而对于这段代码:

for (String s : getList()) {
  processItem(s);
}

字节码是:

0:   invokestatic    #4; //Method getList:()Ljava/util/List;
3:   invokeinterface #5,  1; //InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
8:   astore_1
9:   aload_1
10:  invokeinterface #6,  1; //InterfaceMethod java/util/Iterator.hasNext:()Z
15:  ifeq    35
18:  aload_1
19:  invokeinterface #7,  1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
24:  checkcast       #8; //class java/lang/String
27:  astore_2
28:  aload_2
29:  invokestatic    #9; //Method processItem:(Ljava/lang/String;)V
32:  goto    9
35:  return

这两个字节码之间唯一的区别在于,在前一个字节码中,我们有:

3:   astore_1
4:   aload_1

这相当于将getList()方法返回的结果存储在一个变量中。换句话说,这两种for循环风格的性能几乎相同(除了用于存储getList()方法返回结果的变量之外)。


0

getList方法在每种情况下都被调用相同的次数 - 只调用一次。

至于采用哪种风格,这取决于情况。如果您需要在for循环之后再次引用列表,请选择Case 2,以避免不必要地多次调用getList。

从调试的角度来看,我也更喜欢Case 2。您可以在for循环上设置断点,并在迭代开始之前检查列表的内容。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接