5

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); } } 

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks for the reply. What you say may for all i know very well be the reason the compiler complains, but CallbackProducer's <T> is never used. "addIntegerListener" already specifically takes a GenericListener<Integer> as argument. Also, the following lines compiles and runs just fine: CallbackProducer<Double> producer2 = new CallbackProducer<>(); producer2.addIntegerListener(this::integerReceived); Now I'm saying T is a Double, when I'm working with integer listeners. It still works, since T is unrelated to the listener.
It has nothing to do with what the type argument to CallProducer is. It has everything to do with the fact that you've used a raw CallProducer.
Just to make sure I understand: What you are saying is that if I use a raw CallbackProducer (or any other class that takes a type parameter), all other type arguments in that producer get type erased, even if they are completely unrelated to the CallbackProducer's "T"? (like in my case) I don't doubt you're right, but out of curiosity, is there a reason for this behavior? I can't see why this should be a problem. The types are guaranteed to match. Do you know if this is a weakness in the compiler that may be addressed in the future or if this is by design?
Yes, all other type arguments are erased, even unrelated ones such as GenericListener. This was created like this for Java 5.0, when Generics was introduced, to provide backwards compatibility with pre-generics code.
Isn't it a bit strange, then, that the last example runs just fine? GenericListener<Integer> integerListener = this::integerReceived; new CallbackProducer().addIntegerListener(integerListener); Also, using the old method of an anonymous class works fine even though the listener's type parameter is specified an Integer.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.