Улучшены циклические и лямбда-выражения for

Насколько я понимаю, лямбда-выражения захватывают значения, а не переменные. Например, следующее является ошибкой во время компиляции:

for (int k = 0; k < 10; k++) {
    new Thread(() -> System.out.println(k)).start();
    // Error—cannot capture k
    // Local variable k defined in an enclosing scope must be final or effectively final
   }

Однако, когда я пытаюсь запустить ту же логику с расширенным for-loop все работает нормально:

List<Integer> listOfInt = new Arrays.asList(1, 2, 3);

for (Integer arg : listOfInt) {
    new Thread(() -> System.out.println(arg)).start();
    // OK to capture 'arg'
 }

Почему это работает отлично для расширенных for цикла, а не для нормального цикла, хотя расширение for цикла также где - то внутри приращения переменного, как это делалось при нормальном цикле. **

Ответы

Ответ 1

Лямбда-выражения работают как обратные вызовы. В тот момент, когда они передаются в коде, они "хранят" любые внешние значения (или ссылки), которые им необходимы для работы (как если бы эти значения были переданы в качестве аргументов при вызове функции. Это просто скрыто от разработчика). В первом примере вы можете обойти эту проблему, сохранив k в отдельной переменной, например d:

for (int k = 0; k < 10; k++) {
    final int d = k
    new Thread(() -> System.out.println(d)).start();
}

Фактически final означает, что в приведенном выше примере вы можете опустить ключевое слово 'final', потому что d фактически является конечным, поскольку оно никогда не изменяется в своей области видимости.

For циклы работают по-разному. Это итеративный код (в отличие от обратного вызова). Они работают в своей области видимости и могут использовать все переменные в своем стеке. Это означает, что блок кода цикла for является частью блока внешнего кода.

Что касается вашего выделенного вопроса:

Усовершенствованный цикл for не работает с обычным счетчиком индексов, по крайней мере, напрямую. Улучшено for циклов (над не массивами) создать скрытый итератор. Вы можете проверить это следующим образом:

Collection<String> mySet = new HashSet<>();
mySet.addAll(Arrays.asList("A", "B", "C"));
for (String myString : mySet) {
    if (myString.equals("B")) {
        mySet.remove(myString);
    }
}

Приведенный выше пример вызовет исключение ConcurrentModificationException. Это связано с тем, что итератор замечает, что базовая коллекция изменилась во время выполнения. Однако в вашем примере внешний цикл создает "окончательно" переменную arg которую можно ссылаться в лямбда-выражении, поскольку значение захватывается во время выполнения.

Предотвращение захвата "неэффективно окончательных" значений является более или менее просто предосторожностью в Java, потому что в других языках (например, JavaScript) это работает по-другому.

Таким образом, компилятор может теоретически перевести ваш код, захватить значение и продолжить, но ему придется хранить это значение по-другому, и вы, вероятно, получите неожиданные результаты. Поэтому команда разработчиков лямбд для Java 8 правильно исключила этот сценарий, исключив его за исключением.

Если вам когда-либо понадобится изменить значения внешних переменных в лямбда-выражениях, вы можете объявить одноэлементный массив:

String[] myStringRef = { "before" };
someCallingMethod(() -> myStringRef[0] = "after" );
System.out.println(myStringRef[0]);

Или используйте Atomic <T>, чтобы сделать его потокобезопасным. Однако в вашем примере это, вероятно, вернет "до", так как поток, скорее всего, будет выполнен после выполнения println.

Ответ 2

В расширенном цикле for переменная инициализируется при каждой итерации. Из §14.14.2 Спецификации языка Java (JLS):

...

Когда выполняется расширенный оператор for, локальная переменная инициализируется на каждой итерации цикла для последовательных элементов массива или Iterable созданных выражением. Точное значение расширенного оператора for дается переводом в базовый оператор for следующим образом:

  • Если тип Expression является подтипом Iterable, то перевод выглядит следующим образом.

    Если тип Expression является подтипом Iterable<X> для некоторого аргумента типа X, то пусть I будет типом java.util.Iterator<X>; в противном случае позвольте I быть необработанным типом java.util.Iterator.

    Расширенный оператор for эквивалентен основному for оператора формы:

    for (I #i = Expression.iterator(); #i.hasNext(); ) {
        {VariableModifier} TargetType Identifier =
            (TargetType) #i.next();
        Statement
    }
    

...

  • В противном случае выражение обязательно имеет тип массива T[].

    Пусть L1... Lm будет (возможно, пустой) последовательностью меток, непосредственно предшествующих расширенному оператору for.

    Расширенный оператор for эквивалентен основному for оператора формы:

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

...

Другими словами, ваш расширенный цикл for эквивалентен:

ArrayList<Integer> listOfInt = new ArrayList<>();
// add elements...

for (Iterator<Integer> itr = listOfInt.iterator(); itr.hasNext(); ) {
    Integer arg = itr.next();
    new Thread(() -> System.out.println(arg)).start();
}

Поскольку переменная инициализируется на каждой итерации, она фактически является окончательной (если только вы не изменили переменную внутри цикла).

Напротив, переменная в базовом цикле for (k в вашем случае) инициализируется один раз и обновляется на каждой итерации (если присутствует "ForUpdate", например, k++). См. §14.14.1 JLS для получения дополнительной информации. Поскольку переменная обновляется, каждая итерация не является ни окончательной, ни эффективной.

Необходимость окончательной или эффективной окончательной переменной определяется и объясняется в §15.27.2 JLS:

...

Любая локальная переменная, формальный параметр или параметр исключения, используемые, но не объявленные в лямбда-выражении, должны быть либо объявлены final либо фактически окончательными ( §4.12.4 ), либо при попытке использования возникает ошибка времени компиляции.

Любая локальная переменная, используемая, но не объявленная в лямбда-теле, должна быть обязательно назначена ( §16 (Определенное назначение) ) перед лямбда-телом, иначе произойдет ошибка времени компиляции.

Аналогичные правила использования переменных применяются в теле внутреннего класса (§8.1.3).Ограничение на эффективные конечные переменные запрещает доступ к динамически изменяющимся локальным переменным, чей захват может привести к проблемам с параллелизмом.По сравнению с final ограничением, это уменьшает нагрузку на программистов.

Ограничение для эффективно конечных переменных включает стандартные переменные цикла, но не enhanced- for переменных цикла, которые рассматриваются как отдельные для каждой итерации цикла (§14.14.2).

...

В последнем предложении даже явно упоминается разница между базовыми для переменных цикла и улучшенными для переменных цикла.

Ответ 3

Другие ответы полезны, но, похоже, они не решают вопрос напрямую и не дают четких ответов.

В первом примере вы пытаетесь получить доступ k из лямбда-выражения. Проблема здесь в том, что k меняет свое значение во времени (k++ вызывается после каждой итерации цикла). Лямбда-выражения действительно захватывают внешние ссылки, но они должны быть помечены как final или быть "эффективно окончательными" (т.е. Пометить их как final все равно приведет к действительному коду). Это должно предотвратить проблемы параллелизма; к тому моменту, как созданный вами поток будет запущен, k уже может содержать новое значение.

Во втором примере, с другой стороны, переменная, к которой вы обращаетесь, это arg, которая повторно инициализируется при каждой итерации расширенного цикла for (сравните с приведенным выше примером, где k было просто обновлено), поэтому вы создаете совершенно новая переменная с каждой итерацией. Кроме того, вы также можете явно объявить переменную итерации расширенного цикла for как final:

for (final Integer arg : listOfInt) {
    new Thread(() -> System.out.println(arg)).start();
}

Это гарантирует, что значение arg ссылок не изменится к моменту запуска созданного вами потока.

Ответ 4

Расширенный цикл for определен как эквивалент этого кода:

for (Iterator<T> it = iterable.iterator(); it.hasNext(); ) {
    T loopvar = it.next();
    …
}

Этот код подстановки объясняет, почему переменная расширенного цикла for считается фактически конечной.

Ответ 5

Локальная переменная k, определенная во внешней области видимости, должна быть конечной или эффективно конечной.
Решение: собрать значение в final переменную и передать его в lambada. Это должно работать.

   for (int k = 0; k < 10; k++) {
        int finalK = k;
        new Thread(() -> System.out.println(finalK)).start();
        // Error—cannot capture k
    }