0

I've recently encountered another NullPointerException in Java. It took me quite some time to figure it out, but in the end I found the problem.

Here's the thing: it's a Java 8 method reference that is causing the exception.

When I convert that exact same piece of code to a lambda abstraction, everything works just fine.

I've managed to break it down to this 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;
    }

}

This results in a "foo yay!", followed by the exception.

That means the class Bar works just fine. Baz however fails.

I've read but not fully understood the specs at Run-Time Evaluation of Method References and I feel like the solution should be somewhere in there.

Does anyone know what is the difference between the lambda and the method reference regarding null values etc?

I hope there's some expert out there who can help me. The bug was too annoying not to find out the exact behaviour of Java 8. Thank you in advance!

PS: After reading the specs I actually thought that none of Bar, Baz should work. So why does one..?

Nagaraj Tantri
  • 5,172
  • 12
  • 54
  • 78
KnorpelSenf
  • 390
  • 1
  • 3
  • 10
  • There's another subtlety here, with initialization order: the call to `createConsumer` occurs before `bazConsumer` is assigned, because it happens in the superclass constructor, so `bazConsumer` is `null` at the point that you do `bazConsumer::accept`. After that, it is the same as the answer at the linked question. (I think your code example is better than the other question's, by the way, but eh, that's the site.) – Radiodef Mar 18 '18 at 01:37
  • Answers to that question explain a lot, thank you! I didn't find it myself. One point still required clarification though: shouldn't I get an exception when executing Bar upon runtime? I mean, the lambda also takes a null reference and stores it. See https://stackoverflow.com/q/30360824 for that. Still doesn't make sense in my head. – KnorpelSenf Mar 18 '18 at 01:41
  • The lambda actually captures a reference to the enclosing `Bar` instance, so the unqualified `barConsumer` is the same as `Bar.this.barConsumer`. There's some discussion of this [here](https://docs.oracle.com/javase/specs/jls/se9/html/jls-15.html#jls-15.27.2) in the specification. On the other hand, the method reference captures the `bazConsumer` instance only at the time the method reference expression is evaluated. – Radiodef Mar 18 '18 at 01:52
  • to be honest this example of yours (without taking initialization order into account) can be further simplified: `static class Mapper { public int map(int i) { return i; } }` `private static Mapper getMapper() { return null; }` and usage `Stream.of(1).map(getMapper()::map);` Notice that there is no terminal operation and that means nothing gets done; this still throws a `NullPointerException`, this on the other hand `Stream.of(1).map(x -> getMapper().map(x));` will not – Eugene Mar 18 '18 at 09:35

0 Answers0