Усовершенствованные циклические и лямбда-выражения for - PullRequest
0 голосов
/ 24 января 2019

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

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 также находится где-то внутри, увеличивая переменную, как это делает нормальный цикл. **

Ответы [ 4 ]

0 голосов
/ 24 января 2019

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

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

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

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

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

0 голосов
/ 24 января 2019

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

...

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

  • Если тип Выражение являетсяподтип Iterable, то перевод будет следующим:

    Если тип Выражение является подтипом 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, это уменьшает нагрузку на программистов.

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

...

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

0 голосов
/ 24 января 2019

Усовершенствованная петля for определена как эквивалент этому коду:

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

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

0 голосов
/ 24 января 2019

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

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

Фактически final означает, что в приведенном выше примере вы можете оставить 'final 'ключевое слово out, потому что 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 , чтобы сделать его поточно-ориентированным.Однако в вашем примере это, вероятно, вернет «до», поскольку поток, скорее всего, будет выполнен после выполнения println.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...