Hay
Hay

Reputation: 2281

Java constructor chaining with method reference

I've encountered a situation that looked more or less like this while refactoring code; rewritten here for the sake of the example:

public class UrlProbe {
    private final OkHttpClient http;
    private final String url;
    private final Function<Response, Object> transform;
    private Object cachedValue;

    public UrlProbe(OkHttpClient http, String url) {
        this(http, url, this::debuggingStringTransform);
    }

    public UrlProbe(OkHttpClient http, String url, Function<Response, Object> transform) {
        this.http = http;
        this.url = url;
        this.transform = transform;
    }

    private Object debuggingStringTransform(Response response) {
        String newValue = response.body().toString();
        System.out.println("Debugging new value from url " + url + ": " + newValue);
        return newValue;
    }

    public synchronized Object probe() {
        if (cachedValue != null) {
            return cachedValue;
        }

        try (Response response = http.newCall(new Request.Builder().url(url).get().build()).execute()) {
            cachedValue = transform.apply(response);
            return cachedValue;

        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

This code will not compile because we cannot reference this before supertype constructor has been called:

public UrlProbe(OkHttpClient http, String url) {
    this(http, url, this::debuggingStringTransform);
}

The following will not compile either:

public UrlProbe(OkHttpClient http, String url) {
    this(http, url, response -> debuggingStringTransform(response));
}

The only way I've found around this is to explicitly use a non-chaining constructor:

public UrlProbe(OkHttpClient http, String url) {
    this.http = http;
    this.url = url;
    this.transform = this::debuggingStringTransform;
}

While it makes sense to restrict the use of this in constructors chaining arguments, I find it surprising in this particular context as there doesn't appear to be any kind of evaluation of the object being constructed caused by the use of this when it comes to method references and the contents of a lambda expression.

Is there a rationale behind this restriction other than it is because JLS §8.8.7.1 says so?

Upvotes: 2

Views: 224

Answers (2)

jontro
jontro

Reputation: 10628

Allowing referencing this scope that early would break code that looks like this

public class UrlProbe {
    final String url;
    final String param2;
    public UrlProbe(String url) {
        this(url, this::debuggingStringTransform);
    }

    public UrlProbe(String url, Function<String, String> transform) {
        this(url, transform.apply("")); // <-- What should happen when url is referenced here?

    }
    public UrlProbe(String url, String param2) {
        this.url = url;
        this.param2 = param2;
    }

    private String debuggingStringTransform(String response) {
        System.out.println("Debugging new value from url " + url + ": " + response);
        return response;
    }
}

That's at least one way of violating the rules.

Upvotes: 2

Makoto
Makoto

Reputation: 106450

IntelliJ shows this on the tooltip for why this code is "bad":

enter image description here

Cannot reference 'this' before supertype constructor has been called

This makes sense. You're in the middle of constructing your object and the method reference as defined only exists after the class is instantiated. It can't realistically exist before it's fully instantiated, since from a semantic standpoint, you can't actually do anything with it until it's "ready".

If you wanted to get around this, you could change that function to a static one, since there's no state required for it:

public UrlProbe(OkHttpClient http, String url) {
    this(http, url, UrlProbe::debuggingStringTransform);
}

private static Object debuggingStringTransform(Response response) {
    String newValue = response.body().toString();
    System.out.println("Debugging new value from url " + url + ": " + newValue);
    return newValue;
}

...although admittedly it looks weird to see a private static method.

Alternatively, have this function exist elsewhere in the same package, like in a static class at the bottom of this one:

public UrlProbe(OkHttpClient http, String url) {
    this(http, url, Functions::debuggingStringTransform);
}

static class Functions {
    static Object debuggingStringTransform(Response response) {
        String newValue = response.body().toString();
        System.out.println("Debugging new value from url " + url + ": " + newValue);
        return newValue;
    }
}

Upvotes: 1

Related Questions