Search code examples
junitjava-8java-6hamcrest

Testing type matching differences in Java 6 and Java 8


I updated an application JDK from 1.6 to 1.8 but I kept the language level 1.6 in IntelliJ.

After updating I got compilation error in some tests in assertThat statements with checking type of an object. The code is like this:

assertThat((Class) myList.get(0).getMyClassType(), is(equalTo(MySubclass.class)));

myList is looking like this:

  List<MyClassDefinition> myList = myClassDefinition.getMyClassDefinitions(reader);

Where definition of MyClassDefinition and getMyClassType() are like this:

public class MyClassDefinition {
    private Class<? extends MyClass> classType;

   public Class<? extends MyClass> getMyClassType() {
        return classType;
    }
}

and MySubclass is Myclass's subclass:

  public class MySubclass extends MyClass {
        @Override
        public void initialise() {
            // some action here!
        }
    }

The definition of MyClass is

public abstract class MyClass extends AnotherClass<AnotherType>{
  //Definitions
}

The import for Assert is and equals library is this:

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

and the hamcrest version is hamcrest-all-1-3

After changing project SDK to jdk 1.8 I am getting this error message:

Error:(136, 9) java: no suitable method found for assertThat(java.lang.Class,org.hamcrest.Matcher<java.lang.Class<MySubClass>>)
    method org.hamcrest.MatcherAssert.assertThat(java.lang.String,boolean) is not applicable
      (argument mismatch; java.lang.Class cannot be converted to java.lang.String)
    method org.hamcrest.MatcherAssert.<T>assertThat(java.lang.String,T,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))
    method org.hamcrest.MatcherAssert.<T>assertThat(T,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (argument mismatch; org.hamcrest.Matcher<java.lang.Class<MySubClass>> cannot be converted to org.hamcrest.Matcher<? super java.lang.Class>))

application is build with Ant and we have added hamcrest jar file to class file, so there is no change when we are altering JDK in project. So my question is why this code is working with JDK 1.6 but not working with 1.8 while I am using same level of language (1.6) for compilation? Is it depend to different libraries some how?


Solution

  • Apparently, you had a problem with the generic type signatures which you worked around by inserting a type cast to the raw type Class, which causes the compiler to treat the entire statement as an unchecked operation using raw types only.

    As you can see from the compiler message, org.hamcrest.Matcher<java.lang.Class<MySubClass>> cannot be converted to org.hamcrest.Matcher<? super java.lang.Class>, it now did a generic type check which (correctly) failed, but imho, it shouldn’t do that type check under Java 6 language rules. The problem stems from the fact that JDK8 doesn’t include the original Java 6 compiler but rather let the new compiler attempt to compile using the old rules, which may not be that precise.

    But regardless of whether the behavior is correct or not, I wouldn’t expect a fix for it, as emulating the old Java 6 language rules is not of a high priority.

    Note that with source level 1.8, the code compiles without problems, even without the raw type cast. The original problem stems from the restrictive signature of CoreMatchers.equalTo(…). The standalone expression equalTo(InstanceOfFoo) returns a Matcher<Foo>, which being passed to assertThat allows to only test instances of Foo (it’s even worse when using isA(Foo.class) which also can only test instances of Foo, which makes the test pointless).

    With Java 8, there is target type inference, which allows, e.g.
    Matcher<Object> m = equalTo(InstanceOfFoo);, which will infer Object for <T> and passes, as InstanceOfFoo is also assignable to Object. This also works in conjunction with the compound assertThat statement of your question, inferring a suitable type.

    This guides us to the general solution that works with all language levels. Just use

    assertThat(myList.get(0).getMyClassType(), is(equalTo((Object)MySubclass.class)));
    

    Class<MySubclass> is, of course, assignable to Object which makes the cast a no-op. But then, getting a Matcher<Object>, you can check every object you like, including the result of myList.get(0).getMyClassType(), which is, of course, also assignable to Object. Unlike your original workaround, this doesn’t bear any raw type usage nor unchecked operation.

    You could also use an explicit type instead of a cast, CoreMatchers.<Object>equalTo(MySubclass.class), but this eliminates the benefit of import static.

    By the way, when matching Class objects, you may use sameInstance instead of equalTo.