Нулевая ссылка в ссылке на функцию Java 8, но не в лямбда-абстракции

Недавно я столкнулся с еще одним NullPointerException в Java. Мне потребовалось довольно много времени, чтобы понять это, но в конце концов я нашел проблему.

Вот в чем дело: это ссылка на метод Java 8, которая вызывает исключение.

Когда я преобразовываю тот же самый фрагмент кода в лямбда-абстракцию, все работает просто отлично.

Мне удалось разбить его на этот SSCCE:

import java.util.function.Consumer;

public class Main{
    public static void main(String[] args) {
        Bar bar = new Bar(System.out::println);
        Baz baz = new Baz(System.out::println);
        bar.fooYou();
        baz.fooYou();
    }
}

abstract class Foo {

    Consumer<String> fooConsumer;

    Foo() {
        fooConsumer = createConsumer();
    }

    void fooYou() {
        fooConsumer.accept("foo yay!");
    }

    abstract Consumer<String> createConsumer();
}

class Bar extends Foo {

    final Consumer<String> barConsumer;

    Bar(Consumer<String> c) {
        barConsumer = c;
    }

    @Override
    Consumer<String> createConsumer() {
        return t -> barConsumer.accept(t);
    }

}

class Baz extends Foo {

    final Consumer<String> bazConsumer;

    Baz(Consumer<String> c) {
        bazConsumer = c;
    }

    @Override
    Consumer<String> createConsumer() {
        return bazConsumer::accept;
    }

}

Это приводит к "foo yay!", за которым следует исключение.

Это означает, что класс Bar работает нормально. Однако Баз терпит неудачу.

Я прочитал, но не до конца понял спецификации по адресу Оценка ссылок на методы во время выполнения, и я чувствую, что решение должно быть где-то там.

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

Надеюсь найдутся специалисты, которые смогут мне помочь. Ошибка была слишком раздражающей, чтобы не узнать точное поведение Java 8. Заранее спасибо!

PS: После прочтения спецификаций я действительно подумал, что ни один из Bar, Baz не должен работать. Так почему один..?


person Steffen T    schedule 18.03.2018    source источник
comment
Здесь есть еще одна тонкость, связанная с порядком инициализации: вызов createConsumer происходит до того, как будет назначено bazConsumer, потому что это происходит в конструкторе суперкласса, поэтому bazConsumer становится null в тот момент, когда вы выполняете bazConsumer::accept. После этого это то же самое, что и ответ на связанный вопрос. (Кстати, я думаю, что ваш пример кода лучше, чем другой вопрос, но да, это сайт.)   -  person Radiodef    schedule 18.03.2018
comment
Ответы на этот вопрос многое объясняют, спасибо! Я сам не нашел. Однако один момент все еще требует уточнения: не должен ли я получить исключение при выполнении Bar во время выполнения? Я имею в виду, что лямбда также принимает нулевую ссылку и сохраняет ее. Для этого см. stackoverflow.com/q/30360824. Все еще не имеет смысла в моей голове.   -  person Steffen T    schedule 18.03.2018
comment
Лямбда-выражение фактически захватывает ссылку на вложенный экземпляр Bar, поэтому неквалифицированный barConsumer совпадает с Bar.this.barConsumer. Здесь обсуждается в спецификации. С другой стороны, ссылка на метод фиксирует экземпляр bazConsumer только во время оценки выражения ссылки на метод.   -  person Radiodef    schedule 18.03.2018
comment
честно говоря, этот ваш пример (без учета порядка инициализации) можно упростить еще больше: static class Mapper { public int map(int i) { return i; } } private static Mapper getMapper() { return null; } и использование Stream.of(1).map(getMapper()::map); Обратите внимание, что нет терминальной операции, а это означает, что ничего не делается; это по-прежнему выдает NullPointerException, а это, с другой стороны, Stream.of(1).map(x -> getMapper().map(x)); не будет   -  person Eugene    schedule 18.03.2018