Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generics behavior differs in JDK 8 and 9

The following simple class (repo to reproduce it):

import static org.hamcrest.*;
import static org.junit.Assert.assertThat;
import java.util.*;
import org.junit.Test;

public class TestGenerics {
  @Test
  public void thisShouldCompile() {
    List<String> myList = Arrays.asList("a", "b", "c");
    assertThat("List doesn't contain unexpected elements", myList, not(anyOf(hasItem("d"), hasItem("e"), hasItem("f"))));
  }
}

Behavior depends on the JDK version:

  • Compiles properly in JDK<=8 (tested with 7 and 8)
  • Compilation fails using JDK 9+ (tested with 9, 10 and 11 EA)

With the following error:

[ERROR] /tmp/jdk-issue-generics/src/test/java/org/alostale/issues/generics/TestGenerics.java:[17,17] no suitable method found for assertThat(java.lang.String,java.util.List<java.lang.String>,org.hamcrest.Matcher<java.lang.Iterable<? super java.lang.Object>>)
    method org.junit.Assert.<T>assertThat(java.lang.String,T,org.hamcrest.Matcher<? super T>) is not applicable
      (inference variable T has incompatible bounds
        upper bounds: java.lang.String,java.lang.Object
        lower bounds: capture#1 of ? super T?,capture#2 of ? super java.lang.Object,capture#3 of ? super java.lang.Object,java.lang.Object,java.lang.String,capture#4 of ? super T?)
    method org.junit.Assert.<T>assertThat(T,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))

Is this some expected change in JDK 9 or it's a bug?

I could extract matchers to typed variables in this way, and it would work:

    Matcher<Iterable<? super String>> m1 = hasItem("d");
    Matcher<Iterable<? super String>> m2 = hasItem("e");
    Matcher<Iterable<? super String>> m3 = hasItem("f");
    assertThat(myList, not(anyOf(m1, m2, m3)));

But still the question is: is it correct javac <=8 is being able to infer types, but not in 9+?

like image 296
alostale Avatar asked Jun 19 '18 12:06

alostale


People also ask

What is the difference between T and E in Java generics?

Well there's no difference between the first two - they're just using different names for the type parameter ( E or T ). The third isn't a valid declaration - ? is used as a wildcard which is used when providing a type argument, e.g. List<?>

Which Java version is the collection and generics included?

The generic collections are introduced in Java 5 Version. The generic collections disable the type-casting and there is no use of type-casting when it is used in generics. The generic collections are type-safe and checked at compile-time. These generic collections allow the datatypes to pass as parameters to classes.

Why generics are introduced in Java What problems do they overcome?

To overcome the above problems of collections(type-safety, type casting) generics introduced in java 1.5v . Main objectives of generics are: 1) To provide type safety to the collections. 2) To resolve type casting problems. To hold only string type of objects we can create a generic version of ArrayList as follows.

How do you read Java generics?

Java Generic methods and generic classes enable programmers to specify, with a single method declaration, a set of related methods, or with a single class declaration, a set of related types, respectively. Generics also provide compile-time type safety that allows programmers to catch invalid types at compile time.


2 Answers

After some research I believe we can rule this out as a Junit or hamcrest issue. Indeed, this seems to be a JDK bug. The following code will not compile in JDK > 8:

AnyOf<Iterable<? super String>> matcher = CoreMatchers.anyOf(
    CoreMatchers.hasItem("d"), CoreMatchers.hasItem("e"), CoreMatchers.hasItem("f"));
Error:(23, 63) java: incompatible types: inference variable T has incompatible bounds
equality constraints: java.lang.String
lower bounds: java.lang.Object,java.lang.String

Turing this into a MCVE which uses no libraries:

class Test {
    class A<S> { } class B<S> { } class C<S> { } class D { }

    <T> A<B<? super T>> foo() { return null; }

    <U> C<U> bar(A<U> a1, A<? super U> a2) { return null; }

    C<B<? super D>> c = bar(foo(), foo());
}

A similar effect can be achieved using a single variable in bar which results in upper bounds equality constraint as opposed to a lower:

class Test {
    class A<S> { } class B<S> { } class C<S> { } class D { }

    <T> A<B<? super T>> foo() { return null; }

    <U> C<U> bar(A<? super U> a) { return null; }

    C<B<? super D>> c = bar(foo());
}
Error:(21, 28) java: incompatible types: inference variable U has incompatible bounds
equality constraints: com.Test.B<? super com.Test.D>
upper bounds: com.Test.B<? super capture#1 of ? super com.Test.D>,java.lang.Object

It looks like when the JDK is attempting to rationalize ? super U it fails to find the proper wildcard class to use. Even more interesting, if you fully specify the type for foo, then the compiler will actually succeed. This holds true for both MCVE's and the original post:

// This causes compile to succeed even though an IDE will call it redundant
C<B<? super D>> c = bar(this.<D>foo(), this.<D>foo());

And just like in the case you presented, Breaking up the execution into multiple lines will produce the correct results:

A<B<? super D>> a1 = foo();
A<B<? super D>> a2 = foo();
C<B<? super D>> c = bar(a1, a2);

Because there are multiple ways to write this code that should be functionally equivalent, and given that only some of them compile, my conclusion is that that this is not the intended behavior of the JDK. There is a bug somewhere within the evaluation of wildcards that have a super bound.

My recommendation would be to compile existing code against JDK 8, and for newer code requiring JDK > 8, to fully specify the generic value.

like image 177
flakes Avatar answered Oct 19 '22 16:10

flakes


I created a different MCVE showing a difference in type inference:

import java.util.Arrays;
import java.util.List;


public class Example {

    public class Matcher<T> {
        private T t;
        public Matcher(T t) {
            this.t = t;
        }   
    }

    public <N> Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second) {
        return first;
    }

    public <T> Matcher<List<? super T>> hasItem1(T item) {
        return new Matcher<>(Arrays.asList(item));
    }

    public <T> Matcher<List<? super T>> hasItem2(T item) {
        return new Matcher<>(Arrays.asList(item));
    }

    public void thisShouldCompile() {
        Matcher x = (Matcher<List<? super String>>) anyOf(hasItem1("d"), hasItem2("e"));
    }
}

JDK8 compile passes, JDK10 gives:

Example.java:27: error: incompatible types: Example.Matcher<List<? super Object>> cannot be converted to Example.Matcher<List<? super String>>
        Matcher x = (Matcher<List<? super String>>) anyOf(hasItem1("d"), hasItem2("e"));

So it seems JDK10 has a bug resolving N to List<? super String> in

Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second)

when calling

anyOf(Matcher<List<? super String>>, Matcher<List<? super String>>)

I would recommend reporting this issue to OpenJDK (linking the issue here), and possibly reporting the problem to the hamcrest project.

like image 35
tkruse Avatar answered Oct 19 '22 16:10

tkruse