Patrick Peer
Patrick Peer

Reputation: 287

Type checking with generic Suppliers and lambdas

I have two generic methods, which are designed to force the caller to provide parameters that match type wise:

private <T> void compareValues(Supplier<T> supplier, T value) {
    System.out.println(supplier.get() == value);
}

private <T> void setValue(Consumer<T> consumer, T value) {
    consumer.accept(value);
}

However, when calling them, the compiler reasons differently on what is allowed to pass as parameters:

compareValues(this::getString, "Foo"); // Valid, as expected
compareValues(this::getInt, "Foo");    // Valid, but compiler should raise error
compareValues(this::getString, 1);     // Valid, but compiler should raise error

setValue(this::setString, "Foo");      // Valid, as expected
setValue(this::setInt, "Foo");         // Type mismatch, as expected
setValue(this::setString, 1);          // Type mismatch, as expected


private String getString() {
    return  "Foo";
}

private int getInt() {
    return 1;
}

private void setString(String string) {
}

private void setInt(int integer) {
}

How come? Is the compiler just too clumsy to properly reason about types here, or is this a feature of the type system? If so, what are the rules that lead to this behavior? Also, how would I create a "type safe" version of compareValues without adding artificial parameters, if at all possible?

Please note, that the provided methods merely contain a dummy implementation and do not reflect the code in my actual code base. The focus here are solely the method calls.

Upvotes: 6

Views: 936

Answers (3)

Michael
Michael

Reputation: 44150

Others have mentioned why this is happening, so here's a solution to get around the problem.

If you create a generic class, separating the passing of the supplier from the passing of the argument, you do not give the compiler the opportunity to choose an intersection type:

public class Comparer<T>
{
    private final Supplier<T> supplier;

    Comparer(final Supplier<T> supplier)
    {
        this.supplier = supplier;
    }

    void compare(T value)
    {
        System.out.println(supplier.get() == value);
    }
}

new Comparer<>(this::getString).compare("Foo"); // Valid, as expected
new Comparer<>(this::getInt).compare("Foo"); // Invalid, compiler error
new Comparer<>(this::getString).compare(1);  // Invalid, compiler error

By separating out this behaviour, you also allow Comparer to do potentially useful things like caching the result of Supplier.get().

Upvotes: 4

Veselin Davidov
Veselin Davidov

Reputation: 7071

Well here T can be anything. It is a synonym of a type but can be basically any type.

So when you have a compareValues(Supplier<T> supplier, T value) it means a supplier that can give me any type and value that can be of any type. So it doesn't give a compile error and it even works. In your method you can do:

private <T> void compareValues(Supplier<T> supplier, T value) {
    value=supplier.get();  //It is still valid even if you give different types
    System.out.println((supplier.get() == value) +" - "+ value);
}

As for the other method it is different because you say "Give me a consumer that accepts any type" but you give him a consumer that accepts just String.

So here

private void setString(String s) {

    }

won't work but

private <T> void setString(T s) {

}

will work just fine.

It's like if you have a variable of type Object you can assign String to it but not the other way around in a more bizarre situation. A String supplier is a <T> supplier but a String consumer is not a <T> consumer.

See these two methods:

    private <T> void setString(T a) {
        T var=a;
        T var2="Asdf"; //This doesn't compile! cannot convert String to T
    }

    private <String> void setString2(String a) {
        String var=a;
        String var2="asd";
    }

You want consumer of type T which the first method is. But instead you try to give a consumer of type String which cannot work because it consumes just Strings and you want a method that can consume everything

Upvotes: 1

Eugene
Eugene

Reputation: 120848

You can tell that the compiler choose an intersection type, by using

javac -XDverboseResolution=deferred-inference

output in one of the cases is:

 instantiated signature: (Supplier<INT#1>,INT#1)void
 target-type: <none>

 where T is a type-variable:
 T extends Object declared in method <T>compareValues(Supplier<T>,T)

 where INT#1,INT#2 are intersection types:
 INT#1 extends Object,Serializable,Comparable<? extends INT#2>
 INT#2 extends Object,Serializable,Comparable<?>

Upvotes: 2

Related Questions