Для цикла работают разные в Groovy и Java
Обратите внимание на следующий фрагмент кода в groovy:
def static void main(String... args) {
def arr = [1, 2, 3, 4, 5]
for (int f in arr) {
Thread.start { print f + ', '}
}
}
Out: 2, 3, 5, 5, 5,
Я был удивлен этим выходом. Почему "5" было напечатано несколько раз?
Кроме того, все выглядит хорошо, используя эквивалентный код в Java:
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3, 4, 5};
for (int f : arr) {
new Thread(() -> { System.out.print(f + ", "); }).start();
}
}
Out: 1, 5, 4, 3, 2,
Может кто-нибудь объяснить, почему это так? Похоже, проблема с groovy заключается в реализации Closure. Однако это поведение довольно странно. Это какая-то ошибка, или я просто не понимаю, как работает groovy?
Спасибо!
Ответы
Ответ 1
Закрытие Java закрывается по неизменяемому значению в f
при его создании, а закрытие Groovy закрывается над изменяемой переменной f
.
Итак, как только цикл Groovy завершается, f
содержит 5
, а потоки, которые выполняются после этого, будут печатать 5
.
Закрытие Java может закрывать ссылку на переменную, которая является окончательной или "фактически окончательной", что означает ее окончание во всех, кроме имени. См. Java 8: Lambdas, часть 1. То, что могут делать внутренние классы, а также полезное удобство.
Groovy закрытия - очень разные объекты, и они предшествуют закрытию Java. См. Groovy Закрытия, где пример { ++item }
изменяет переменную из охватывающей области.
Groovy определяет замыкания как экземпляры класса Closure. Это очень сильно отличается от лямбда-выражений в Java 8. Делегирование является ключевым понятием в закрытии Groovy, которое не имеет эквивалента в лямбдах. Возможность изменить делегата или изменить стратегию делегирования закрытия позволяет создавать красивые доменные языки (DSL) в Groovy.
Нижняя строка Groovy направлена на динамический язык с лучшим "совпадением импеданса" с Java, но теперь, когда Java имеет lambdas, эти два языка продолжают расходиться. Программист Caveat.
Ответ 2
Это не проблема с реализацией Closure в Groovy.
Это с вашим непониманием того, что такое Закрытие.
Прежде всего, это не то же самое, что анонимный метод (класс) или Lambda (Java 8+).
Это то же самое, что и закрытие JavaScript.
Закрытие имеет полный доступ для чтения/записи к локальным переменным, которые находятся в области видимости, что означает переменные, определенные в методе размещения, но вне закрытия. Эти переменные существуют и могут быть обновлены любым кодом с доступом к ним, и они продолжают существовать после того, как метод, определяющий их завершение (возврат).
Вы действительно должны прочитать гораздо больше о закрытии, либо в Groovy, либо в документации и примерах JavaScript. JavaScript изобилует использованием закрытий, поэтому вы найдете много документации по этому вопросу.
Здесь короткое введение:
def a() {
def myval = 0
return { x -> myval += x } // <-- Returns a closure
}
def f = a()
print f(5)
print f(7)
Это будет печатать 5
и 12
, потому что переменная myval
существует до тех пор, пока закрытие, назначенное f
, останется в живых.
Или вот версия JavaScript: https://jsfiddle.net/Lguk9qgw/
Напротив, Java не может этого сделать, потому что Java не имеет закрытий, даже с новыми Lambdas. Java-анонимные классы и их эквивалент Лямбды требуют, чтобы все внешние переменные были инвариантными, т.е. final
, явно ли они определены таким образом или выведены компилятором (новым в Java 8).
Это связано с тем, что Java в действительности копирует значение и требует, чтобы значение final
гарантировало, что вы не заметите, если вы не разобрали сгенерированный байт-код.
Чтобы показать это, эти 5 примеров Java все делают одно и то же, функционально, например. вызов test1().applyAsInt(5)
вернет 12
:
// Using Lambda Expression
public static IntUnaryOperator test1() {
final int f = 7;
return x -> x + f;
}
// Using Lambda Block
public static IntUnaryOperator test2() {
final int f = 7;
return x -> { return x + f; };
}
// Using Anonymous Class
public static IntUnaryOperator test3() {
final int f = 7;
return new IntUnaryOperator() {
@Override public int applyAsInt(int operand) { return operand + f; }
};
}
// Using Local Class
public static IntUnaryOperator test4() {
final int f = 7;
class Test4 implements IntUnaryOperator {
@Override public int applyAsInt(int operand) { return operand + f; }
}
return new Test4();
}
// Using Nested Class
private static final class Test5 implements IntUnaryOperator {
private final int f;
Test5(int f) { this.f = f; }
@Override public int applyAsInt(int operand) { return operand + this.f; }
}
public static IntUnaryOperator test5() {
final int f = 7;
return new Test5(f);
}
Ответ 3
Я действительно не знаю причины для цикла, указанного в вопросе, но следующий фрагмент кода работает как шарм:
["one","two","three","four"].each { tid ->
Thread.start {
println "Thread $tid says Hello World!"
}
}
Ответ 4
Это результат декомпиляции кода:
CallSite[] arrayOfCallSite = $getCallSiteArray();
Object arr = ScriptBytecodeAdapter.createList(new Object[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5) });
Reference f = new Reference(Integer.valueOf(0));
for (Iterator i = (Iterator)ScriptBytecodeAdapter.castToType(arrayOfCallSite[0].call(arr), Iterator.class); i.hasNext();) {
((Reference)f).set(Integer.valueOf(DefaultTypeTransformation.intUnbox(i.next())));
arrayOfCallSite[1].call(Thread.class, new _main_closure1(Test.class, Test.class, f));
}
Из этого фрагмента видно, что каждый поток получает тот же экземпляр f