Улучшены циклические и лямбда-выражения 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
}