Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JDK 11 Generics Issue when using Set.of

I am unable to understand the below issue for type safety when using JDK 11. Can anyone explain the reason for not getting a compilation error when I am directly passing the Set.of in the argument:

public static void main(String[] args) {
    var intSet1 = Set.of(123, 1234, 101);
    var strValue = "123";
    isValid(strValue, intSet1);// Compilation error (Expected behaviour)
    **isValid(strValue, Set.of(123, 1234, 101));// No Compilation error**
}

static <T> boolean isValid(T value, Set<T> range) {
    return range.contains(value);
}

You can run this code live at IdeOne.com.

like image 844
Alok Dubey Avatar asked Jun 02 '21 07:06

Alok Dubey


People also ask

How do I restrict a generic type in Java?

Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class.

Are there any situations where generic type information is available at runtime?

It's important to realize that generic type information is only available to the compiler, not the JVM. In other words, type erasure means that generic type information is not available to the JVM at runtime, only compile time.

Why does Java not allow generic exception classes?

It's essentially because it was designed in a bad way. The fact that a catch clause would fail for generics are not reified is no excuse for that. The compiler could simply disallow concrete generic types that extend Throwable or disallow generics inside catch clauses.


3 Answers

To put it simply, the compiler is stuck with your declared types on the first call, but has some latitude to infer a compatible type on the second one.

With isValid(strValue, intSet1);, you're calling isValid(String, Set<Integer>), and the compiler does not resolve T to the same type for the two arguments. This is why it's failing. The compiler simply can't change your declared types.

With isValid(strValue, Set.of(123, 1234, 101)), though, Set.of(123, 1234, 101) is a poly expression, whose type is established in the invocation context. So the compiler works at inferring T that is applicable in context. As Eran points out, this is Serializable.

Why does the first one work and the second one doesn't? It's simply because the compiler has some flexibility around the type of the expression given as the second argument. intSet1 is a standalone expression, and Set.of(123, 1234, 101) is a poly expression (see the JLS and this description about poly expression) . In the second case, the context allows the compiler to compute a type that works to a concrete T that is compatible with String, the first argument's type.

like image 67
ernest_k Avatar answered Oct 26 '22 09:10

ernest_k


isValid(strValue, Set.of(123, 1234, 101));

When I hover with my mouse over this isValid() call on Eclipse, I see that it's going to execute the following method:

<Serializable> boolean com.codebroker.dea.test.StringTest.isValid(Serializable value, Set<Serializable> range)

When the compiler tries to resolve the possible type to use for the generic type parameter T of isValid, it needs to find a common super type of String (the type of strValue) and Integer (the element type of Set.of(123, 1234, 101)), and finds Serializable.

Therefore Set.of(123, 1234, 101) is resolved to Set<Serializable> instead of Set<Integer>, so the compiler can pass a Serializable and aSet<Serialiable> to isValid(), which is valid.

isValid(strValue, intSet1);

On the other hand, when you first assign Set.of(123,1234,101) to a variable, it is resolved to a Set<Integer>. And in that case, a String and a Set<Integer> cannot be passed to your isValid() method.

If you change

var intSet1 = Set.of(123, 1234, 101);

to

Set<Serializable> intSet1 = Set.of(123,1234,101);

Then

isValid(strValue, intSet1);

will pass compilation too.

like image 44
Eran Avatar answered Oct 26 '22 09:10

Eran


When you (as a human) look at the second isValid that compiles, probably think - how is that possible? The type T is inferred by the compiler to either String or Integer, so the call must absolutely fail.

The compiler when it looks at the method call thinks in a very different way. It looks at the method arguments, at the types provided and tries to infer an entirely different and un-expected type(s) for you. Sometimes, these types are "non-denotable", meaning types that can exist for the compiler, but you as a user, can not declare such types.

There is a special (un-documented) flag that you can compile your class with and have glimpse into how the compiler "thinks":

 javac --debug=verboseResolution=all YourClass.java

The output is going to be long, but the main part that we care about is:

  instantiated signature: (INT#1,Set<INT#1>)boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>isValid(T,Set<T>)
  where INT#1,INT#2 are intersection types:
    INT#1 extends Object,Serializable,Comparable<? extends INT#2>,Constable,ConstantDesc
    INT#2 extends Object,Serializable,Comparable<?>,Constable,ConstantDesc

You can see that the types that are inferred and used are not String and Integer.

like image 34
Eugene Avatar answered Oct 26 '22 09:10

Eugene