it-swarm.com.ru

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

Недавно я столкнулся с сообщением об ошибке «Пустое окончательное поле 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. Но это соглашение - другая тема.)

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

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

20
Steffen T

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

10
Sotirios Delimanolis

Вы можете обойти проблему, 

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

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

Цитирую Дана Смита, спец-царя, https://bugs.openjdk.Java.net/browse/JDK-8024809

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

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

15
ZhongYu

Вы можете использовать служебный метод для принудительного захвата только this. Это работает и с Java 9.

public static <T> T r(T object) {
    return object;
}

Теперь вы можете переписать свою лямбду следующим образом:

Runnable run = () -> r(this).obj.toString();
0
Dávid Horváth