Serenic
Serenic

Reputation: 243

"incompatible types" compiler error with lambda / method referencing and generics

I stumbled upon an issue when working through some old code, replacing several anonymous classes with either lambda expressions or method references. The problem is a bit hard to explain with words, but I'll do my best, and I've also added a short example illustrating my problem to the best of my abilities below.

My example consists of...

  1. A functional interface, GenericListener, which takes a type parameter V and has a single method "genericCallback(V genericValue)".

  2. A class, CallbackProducer, which takes a type parameter T. This class also has a method to add a GenericListener with type Integer.

  3. A Main class which creates CallbackProducers and adds GenericListeners to them.

When I run CallbackProducer's addIntegerListener method from Main's constructor, I get the compiler error: "incompatible types" whenever i avoid specifying the type of CallbackProducer's T.

The method addIntegerListener only uses GenericListener's V. As far as I know, it doesn't use CallbackProducer's T in any way.

I've put several calls to addIntegerListener + comments in Main's constructor, 3 of which cause compiler errors. But as far as I can see (and according to IntelliJ) all of them should be legal. If you comment out the 3 first calls to addIntegerListener the application will compile and run just fine.

Also, if CallbackProducer didn't use generics, and we removed the type parameter T completely, the 3 first calls to addIntegerListener would compile.

Is there a reason for this behavior? Am I misunderstanding something, or is this a weakness or bug in the java compiler? (I'm currently using java 1.8_51)

Thanks in advance for any clarification!

import javax.swing.*;

public class Main {

    public static void main(final String[] args) {
        SwingUtilities.invokeLater(Main::new);
    }

    public Main() {

        // Compiler error, type of CallbackProducer's "T" not specified
        CallbackProducer producer1 = new CallbackProducer();
        producer1.addIntegerListener(this::integerReceived);

        // Compiler error, no diamond brackets for CallbackProducer
        new CallbackProducer().addIntegerListener(this::integerReceived);

        // Also compiler error for lambdas with no diamond brackets on CallbackProducer
        new CallbackProducer().addIntegerListener(intValue -> integerReceived(intValue));

        // Works because a (any) type for CallbackProducer's "T" is specified
        CallbackProducer<Object> producer2 = new CallbackProducer<>();
        producer2.addIntegerListener(this::integerReceived);

        // Works because of the diamond brackets
        new CallbackProducer<>().addIntegerListener(this::integerReceived);

        // Lambda also works with diamond brackets
        new CallbackProducer<>().addIntegerListener(intValue -> integerReceived(intValue));

        // This variant also works without specifying CallbackProducer's "T"
        // ... but it is a workaround I'd prefer to avoid if possible :-P
        GenericListener<Integer> integerListener = this::integerReceived;
        new CallbackProducer().addIntegerListener(integerListener);
    }

    private void integerReceived(Integer intValue) {
        System.out.println("Integer callback received: " + intValue);
    }

    // A callback producer taking generic listeners
    // Has a type parameter "T" which is completely unrelated to
    // GenericListener's "V" and not used for anything in this
    // example really, except help provoking the compiler error
    public class CallbackProducer<T> {
        // Adds a listener which specifically takes an Integer type as argument
        public void addIntegerListener(GenericListener<Integer> integerListener) {
            // Just a dummy callback to receive some output
            integerListener.genericCallback(100);
        }
    }

    // A simple, generic listener interface that can take a value of any type
    // Has a type parameter "V" which is used to specify the value type of the callback
    // "V" is completely unrelated to CallbackProducer's "T"
    @FunctionalInterface
    public interface GenericListener<V> {
        void genericCallback(V genericValue);
    }
}

Here's a shortened down version without all the comment clutter and with only two calls to "addIntegerListener", one of which causes compiler error.

import javax.swing.*;

public class Main {

    public static void main(final String[] args) {
        SwingUtilities.invokeLater(Main::new);
    }

    public Main() {

        CallbackProducer producer1 = new CallbackProducer();
        producer1.addIntegerListener(this::integerReceived);    // Compiler error

        CallbackProducer<Object> producer2 = new CallbackProducer<>();
        producer2.addIntegerListener(this::integerReceived);    // Compiles OK      
    }

    private void integerReceived(Integer intValue) {
        System.out.println("Integer callback received: " + intValue);
    }

    public class CallbackProducer<T> {
        public void addIntegerListener(GenericListener<Integer> integerListener) {
            integerListener.genericCallback(100);
        }
    }

    @FunctionalInterface
    public interface GenericListener<V> {
        void genericCallback(V genericValue);
    }
}

Upvotes: 5

Views: 4855

Answers (1)

rgettman
rgettman

Reputation: 178333

All 3 compiler errors are due to the fact that you are using a raw CallbackProducer. When you use a raw CallbackProducer, all type arguments undergo type erasure, such that any T, such as yours, without any upper bound, becomes Object.

Because of this, the addIntegerListener method expects a raw GenericListener as a parameter, something that integerReceived no longer fits. The integerReceived method takes an Integer, not an Object, as a raw GenericListener would supply.

You must supply the angle brackets <> on CallbackProducer to avoid using raw types, as you've done on your subsequent examples.

Upvotes: 1

Related Questions