Подтвердить что ты не робот

Почему ссылка метода использует не конечные переменные?

У меня была некоторая путаница в отношении внутренних классов и выражения лямбда, и я попытался спросить question об этом, но потом возникло еще одно сомнение, и это вероятно лучше отправляя другой вопрос, чем комментируя предыдущий.

Прямо до сути: я знаю (спасибо Jon), что что-то вроде этого не будет компилироваться

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = new F(){      //1
            public void foo(){one.bar();}   //compilation error
        };

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }

из-за того, как Java управляет закрытием, потому что one не является [эффективно] окончательным и т.д.

Но тогда, почему это разрешено?

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = one::bar; //2

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }

Является ли //2 эквивалентным //1? Разве я не во втором случае рискую "работать с устаревшей переменной"?

Я имею в виду, что в последнем случае после one = new One(); выполняется f все еще имеет устаревшую копию one (т.е. ссылается на старый объект). Разве это не такая двусмысленность, которую мы пытаемся избежать?

4b9b3361

Ответ 1

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

Скажем, что у нас есть лямбда-выражение:

Runnable f = () -> one.bar();

Это эквивалент анонимного класса, который реализует Runnable:

Runnable f = new Runnable() {
    public void run() {
       one.bar();
    }
}

Здесь действуют те же правила, что и для анонимного класса (или локального класса метода). Это означает, что one должен эффективно работать для окончательной работы.

С другой стороны, дескриптор метода:

Runnable f = one::bar;

Больше нравится:

Runnable f = new MethodHandle(one, one.getClass().getMethod("bar"));

С MethodHandle будет:

public class MethodHandle implements Runnable {
    private final Object object;
    private final Method method;

    public MethodHandle(Object object, java.lang.reflect.Method method) {
        this.object = Object;
        this.method = method;
    }

    @Override
    public void run() {
        method.invoke(object);
    }
}

В этом случае объект, назначенный one, назначается как часть созданного дескриптора метода, поэтому one сам по себе не должен быть окончательным для этого.

Ответ 2

Второй пример - это просто не лямбда-выражение. Это метод ссылки. В этом конкретном случае он выбирает метод из определенного объекта, на который в настоящее время ссылается переменная one. Но ссылка на объект, а не на переменную.

Это то же самое, что и классический случай Java:

One one = new One();
One two = one;
one = new One();

two.bar();

Так что, если one изменилось? two ссылается на объект, который использовался one, и может получить доступ к его методу.

Ваш первый пример, с другой стороны, является анонимным классом, который является классической структурой Java, которая может ссылаться на локальные переменные вокруг него. Код относится к фактической переменной one, а не к объекту, к которому он относится. Это ограничено по причинам, которые Джон упомянул в ответе, о котором вы говорили. Обратите внимание, что изменение в Java 8 состоит только в том, что переменная должна быть фактически окончательной. То есть, он не может быть изменен после инициализации. Компилятор просто стал достаточно сложным, чтобы определить, какие случаи не будут путать, даже если модификатор final явно не используется.

Ответ 3

Консенсус, по-видимому, заключается в том, что, когда вы это делаете с использованием анонимного класса, one ссылается на переменную, тогда как когда вы делаете это с использованием ссылки на метод, значение one фиксируется, когда метод создается дескриптор. На самом деле, я думаю, что в обоих случаях one - это значение, а не переменная. Давайте рассмотрим анонимные классы, лямбда-выражения и ссылки на методы более подробно.

Анонимные классы

Рассмотрим следующий пример:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new Supplier<String>() {
        @Override
        public String get() {
            return o.toString();
        }
    };
}

public static void main(String[] args) {
    Supplier<String> supplier = getStringSupplier();
    System.out.println(supplier.get());  // Use o after the getStringSupplier method returned.
}

В этом примере мы вызываем toString на o после возврата метода getStringSupplier, поэтому, когда он появляется в методе get, o не может ссылаться на локальную переменную getStringSupplier метод. Фактически это по существу эквивалентно этому:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new StringSupplier(o);
}

private static class StringSupplier implements Supplier<String> {
    private final Object o;

    StringSupplier(Object o) {
        this.o = o;
    }

    @Override
    public String get() {
        return o.toString();
    }
} 

Анонимные классы делают это так, как будто вы используете локальные переменные, когда на самом деле значения этих переменных захватываются.

В отличие от этого, если метод анонимного класса ссылается на поля вмещающего экземпляра, значения этих полей не фиксируются, а экземпляр анонимного класса не содержит ссылок на них; вместо этого анонимный класс содержит ссылку на прилагаемый экземпляр и может получить доступ к своим полям (либо напрямую, либо через синтетические аксессоры, в зависимости от видимости). Одно из преимуществ заключается в том, что требуется дополнительная ссылка только на один объект, а не на несколько.

Лямбда-выражения

Лямбда-выражения также близки к значениям, а не переменным. Причина, данная Brian Goetz здесь, заключается в том, что

идиомы:

int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR

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

Ссылки на методы

Тот факт, что ссылки на методы ссылаются на значение переменной при создании дескриптора метода, легко проверить.

Например, следующий код дважды печатает "a":

String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());

Резюме

Итак, вкратце, лямбда-выражения и ссылки на методы близки к значениям, а не переменным. Анонимные классы также закрывают значения в случае локальных переменных. В случае полей ситуация более сложная, но поведение по существу совпадает с поведением значений, поскольку поля должны быть эффективно окончательными.

В связи с этим возникает вопрос: почему правила, применимые к анонимным классам и лямбда-выражениям, не применяются к ссылкам метода, то есть почему вам разрешено писать o::toString, когда o не является окончательным? Я не знаю ответа на это, но мне кажется, что это несогласованность. Я предполагаю, потому что вы не можете причинить столько вреда с помощью ссылки на метод; примеры, подобные приведенным выше для лямбда-выражений, не применяются.

Ответ 4

Нет. В первом примере вы определяете реализацию F inline и пытаетесь получить доступ к переменной экземпляра.

Во втором примере вы в основном определяете ваше лямбда-выражение как вызов bar() для объекта one.

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

msg -> System.out::println(msg);