Для цикла работают разные в 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