【问题标题】:What's happening under the hood of an enhanced for loop?在增强的 for 循环的底层发生了什么?
【发布时间】:2018-06-11 20:43:56
【问题描述】:

我在探索 Java 的增强 for 循环时编写了以下测试:

class test
{
    int number = 0;

    public static void main(String args[]) {
        new test();
    }

    public test() {

        int[] numbers = getNumbers();

        for(int number : numbers) {
            System.out.println("number    : " + number);
            System.out.println("numbers[0]: " + numbers[0]);

            numbers = getNumbers();
        }
    }

    public int[] getNumbers() {

        number++;

        int[] numbers = new int[5];

        for(int i = 0; i < numbers.length; i++)
            numbers[i] = number;

        return numbers;
    }
}

我惊讶地发现我的测试被输出了:

number    : 1
numbers[0]: 1
number    : 1
numbers[0]: 2
number    : 1
numbers[0]: 3
number    : 1
numbers[0]: 4
number    : 1
numbers[0]: 5

然后看起来numbers 的一个单独实例是在第一次执行循环时创建的,并且是不可变的。从那里开始,对numbers 所做的任何更改都只会对存在于循环条件之外的版本进行。

我已确认行为至少与此类似,方法是将循环替换为:

for(int number : numbers) {
    System.out.println("number    : " + number);
    numbers = null;
}

给了:

number    : 1
number    : 1
number    : 1
number    : 1
number    : 1

我的问题是:这里隐藏了哪些行为?是否复制了numbers 的第二个版本?它实际上是不可变的,还是我没有足够努力地戳它?换句话说:

增强的 for 循环背后发生了什么?

【问题讨论】:

    标签: java foreach


    【解决方案1】:

    增强的for 循环被JLS, Section 14.14.2 覆盖:

    • 对于Iterable 对象:

    增强的for 语句等效于基本for 语句的形式:

    for (I #i = Expression.iterator(); #i.hasNext(); ) {
        {VariableModifier} TargetType Identifier =
            (TargetType) #i.next();
        Statement
    }
    
    • 对于数组:

    增强的for 语句等效于基本for 语句的形式:

    T[] #a = Expression;
    L1: L2: ... Lm:
    for (int #i = 0; #i < #a.length; #i++) {
        {VariableModifier} TargetType Identifier = #a[#i];
        Statement
    }
    

    它会创建一个对数组的新引用,与您的原始引用分开。你可以随心所欲地修改对数组的引用,但它仍然会不断迭代这个对原始数组的隐式引用。

    Iterables 上,仅保留对Iterator 的引用,它仍然引用原始Iterable 对象,即使您在循环期间重新分配原始变量。

    无论您对数组使用增强的for 循环还是Iterable,您都可以重新分配用作目标的引用而不影响迭代的结果——您仍将迭代由目标参考。

    【讨论】:

      【解决方案2】:

      在你的增强 for 循环的行中:

          for(int number : numbers) {
      

      : 之后的任何表达式都只计算一次:当 for 循环开始时。即使您在循环运行时为 numbers 变量设置了不同的值,Java 已经缓存了 numbers 的原始值并正在迭代该值。如果您在循环后打印 numbers 中的值,您会看到 numbers 确实发生了变化,但 for 循环正在迭代的实际数组没有变化。

      ...
      public test() {
      
          int[] numbers = getNumbers();
      
          System.out.println("Array before loop starts:");
          System.out.println(Arrays.toString(numbers) + "\n");
      
          for (int number : numbers) {
              System.out.println("number    : " + number);
              System.out.println("numbers[0]: " + numbers[0]);
      
              numbers = getNumbers();
          }
      
          System.out.println("\nArray after loop finishes:");
          System.out.println(Arrays.toString(numbers));
      
      }
      ...
      

      以上打印:

      Array before loop starts:
      [1, 1, 1, 1, 1]
      
      number    : 1
      numbers[0]: 1
      number    : 1
      numbers[0]: 2
      number    : 1
      numbers[0]: 3
      number    : 1
      numbers[0]: 4
      number    : 1
      numbers[0]: 5
      
      Array after loop finishes:
      [6, 6, 6, 6, 6]
      

      【讨论】:

        【解决方案3】:

        引用JLS

        增强的for 语句等效于基本for 语句的形式:

        T[] #a = Expression;
        L1: L2: ... Lm:
        for (int #i = 0; #i < #a.length; #i++) {
            {VariableModifier} TargetType Identifier = #a[#i];
            Statement
        }
        

        注意Expression 只被评估一次。

        【讨论】:

          【解决方案4】:

          来自 java 文档

          "例如这段代码:

          List<? extends Integer> l = ...
          for (float i : l) ...
          

          将被翻译成:

          for (Iterator<Integer> #i = l.iterator(); #i.hasNext(); ) {
              float #i0 = (Integer)#i.next();
              ...
          

          https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14.2

          所以是的,你的观察是正确的!

          【讨论】:

          • 问题是关于数组的。
          • 同样适用,不是吗?
          • 粗略地说。实际上没有迭代器。
          【解决方案5】:

          然后似乎创建了一个单独的数字实例......

          嗯……是的。您通过运行new int[5] 6 次创建了五个新实例。而 for 循环继续使用第一个。第一个在 for 循环结束之前不会被垃圾收集,即使你不能再引用它。

          不建议在循环时更改集合。

          【讨论】:

          • 唯一正确的部分是for循环继续使用第一个。其他一切都无关紧要或具有误导性。
          猜你喜欢
          • 2016-07-24
          • 2019-05-23
          • 1970-01-01
          • 1970-01-01
          • 2014-02-23
          • 2012-07-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多