Java "Пустое конечное поле, возможно, не было инициализировано" Анонимный интерфейс vs Lambda Expression

Недавно я столкнулся с сообщением об ошибке "Пустое конечное поле obj, возможно, не было инициализировано".

Обычно это происходит, если вы попытаетесь обратиться к полю, которое еще не присвоено значению. Пример класса:

public class Foo {
    private final Object obj;
    public Foo() {
        obj.toString(); // error           (1)
        obj = new Object();
        obj.toString(); // just fine       (2)
    }
}

Я использую Eclipse. В строке (1) я получаю ошибку, в строке (2) все работает. Пока что имеет смысл.

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

public class Foo {
    private Object obj;
    public Foo() {
        Runnable run = new Runnable() {
            public void run() {
                obj.toString(); // works fine
            }
        };
        obj = new Object();
        obj.toString(); // works too
    }
}

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

Но теперь я сокращаю создание моего экземпляра Runnable до версии гамбургера с помощью выражения лямбда:

public class Foo {
    private final Object obj;
    public Foo() {
        Runnable run = () -> {
            obj.toString(); // error
        };
        obj = new Object();
        obj.toString(); // works again
    }
}

И вот здесь я больше не могу следовать. Здесь я снова получаю предупреждение. Я знаю, что компилятор не обрабатывает лямбда-выражения как обычные инициализации, он не "заменяет его на длинную версию". Однако почему это влияет на то, что я не запускаю часть кода в моем методе run() во время создания объекта Runnable? Я все еще могу выполнить инициализацию, прежде чем я вызову run(). Таким образом, технически можно встретить NullPointerException здесь. (Хотя было бы лучше проверить и на null здесь, но это соглашение - еще одна тема.)

Какую ошибку я делаю? Что такое lambda по-разному, что влияет на мое использование объекта так, как оно есть?

Я благодарю вас за дальнейшие объяснения.

Ответы

Ответ 1

Я не могу воспроизвести ошибку для вашего последнего случая с компилятором Eclipse.

Однако аргументация для компилятора Oracle, которую я могу себе представить, такова: внутри лямбда значение obj должно быть зафиксировано во время объявления. То есть, он должен быть инициализирован, когда он объявлен внутри тела лямбда.

Но в этом случае Java должна отображать значение экземпляра Foo, а не obj. Затем он может получить доступ к obj через ссылку на объект (инициализированный) Foo и вызвать его метод. Вот как компилятор Eclipse компилирует ваш фрагмент кода.

Это указано в спецификации здесь:

Сроки оценки выражения ссылки метода более сложны чем у лямбда-выражений (§15.27.4). Когда ссылка на метод выражение имеет выражение (а не тип), предшествующее:: разделитель, это подвыражение оценивается немедленно. Результат оценка сохраняется до тех пор, пока метод соответствующего функционального тип интерфейса вызывается; в этот момент результат используется как целевая ссылка для вызова. Это означает, что выражение предшествующий разделителю:: оценивается только тогда, когда программа встречает ссылочное выражение метода и не переоценивается на последующие вызовы по типу функционального интерфейса.

Аналогичная ситуация наблюдается и для

Object obj = new Object(); // imagine some local variable
Runnable run = () -> {
    obj.toString(); 
};

Представьте, что obj является локальной переменной, когда выполняется код выражения lambda, obj оценивается и создает ссылку. Эта ссылка хранится в поле в созданном экземпляре Runnable. При run.run() называется, экземпляр использует опорное значение, хранящееся.

Это не может произойти, если obj не инициализирован`. Например

Object obj; // imagine some local variable
Runnable run = () -> {
    obj.toString(); // error
};

Лямбда не может записать значение obj, потому что оно еще не имеет значения. Это эффективно эквивалентно

final Object anonymous = obj; // won't work if obj isn't initialized
Runnable run = new AnonymousRunnable(anonymous);
...
class AnonymousRunnable implements Runnable {
    public AnonymousRunnable(Object val) {
        this.someHiddenRef = val;
    }
    private final Object someHiddenRef;
    public void run() {
        someHiddenRef.toString(); 
    }
}

Вот как компилятор Oracle в настоящее время ведет себя для вашего фрагмента.

Однако компилятор Eclipse вместо этого не записывает значение obj, он фиксирует значение this (экземпляр Foo). Это эффективно эквивалентно

final Foo anonymous = Foo.this; // you're in the Foo constructor so this is valid reference to a Foo instance
Runnable run = new AnonymousRunnable(anonymous);
...
class AnonymousRunnable implements Runnable {
    public AnonymousRunnable(Foo foo) {
        this.someHiddenRef = foo;
    }
    private final Foo someHiddenFoo;
    public void run() {
        someHiddenFoo.obj.toString(); 
    }
}

Это нормально, потому что вы предполагаете, что экземпляр Foo полностью инициализирован к моменту времени run.

Ответ 2

Вы можете обойти проблему с помощью

        Runnable run = () -> {
            (this).obj.toString(); 
        };

Это обсуждалось во время развития лямбда, в основном тело лямбда рассматривается как локальный код при определенном анализе присваивания.

Цитата Дэн Смит, spec tzar, https://bugs.openjdk.java.net/browse/JDK-8024809

Правила вырезают два исключения:... ii) использование из анонимного класса в порядке. Не существует исключения для использования внутри выражения лямбда

Честно говоря, я и некоторые другие люди считали, что решение неверно. Лямбда регистрирует только this, а не obj. Этот случай следует рассматривать так же, как анонимный класс. Нынешнее поведение является проблематичным для многих законных случаев использования. Ну, вы всегда можете обойти его, используя трюк выше - к счастью определенный анализ присваивания не слишком умен, и мы можем обмануть его.