Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When do Java generics require <? extends T> instead of <T> and is there any downside of switching?

Given the following example (using JUnit with Hamcrest matchers):

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));  

This does not compile with the JUnit assertThat method signature of:

public static <T> void assertThat(T actual, Matcher<T> matcher)

The compiler error message is:

Error:Error:line (102)cannot find symbol method
assertThat(java.util.Map<java.lang.String,java.lang.Class<java.util.Date>>,
org.hamcrest.Matcher<java.util.Map<java.lang.String,java.lang.Class
    <? extends java.io.Serializable>>>)

However, if I change the assertThat method signature to:

public static <T> void assertThat(T result, Matcher<? extends T> matcher)

Then the compilation works.

So three questions:

  1. Why exactly doesn't the current version compile? Although I vaguely understand the covariance issues here, I certainly couldn't explain it if I had to.
  2. Is there any downside in changing the assertThat method to Matcher<? extends T>? Are there other cases that would break if you did that?
  3. Is there any point to the genericizing of the assertThat method in JUnit? The Matcher class doesn't seem to require it, since JUnit calls the matches method, which is not typed with any generic, and just looks like an attempt to force a type safety which doesn't do anything, as the Matcher will just not in fact match, and the test will fail regardless. No unsafe operations involved (or so it seems).

For reference, here is the JUnit implementation of assertThat:

public static <T> void assertThat(T actual, Matcher<T> matcher) {
    assertThat("", actual, matcher);
}

public static <T> void assertThat(String reason, T actual, Matcher<T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason);
        description.appendText("\nExpected: ");
        matcher.describeTo(description);
        description
            .appendText("\n     got: ")
            .appendValue(actual)
            .appendText("\n");

        throw new java.lang.AssertionError(description.toString());
    }
}
like image 704
Yishai Avatar asked Oct 06 '22 22:10

Yishai


1 Answers

First - I have to direct you to http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html -- she does an amazing job.

The basic idea is that you use

<T extends SomeClass>

when the actual parameter can be SomeClass or any subtype of it.

In your example,

Map<String, Class<? extends Serializable>> expected = null;
Map<String, Class<java.util.Date>> result = null;
assertThat(result, is(expected));

You're saying that expected can contain Class objects that represent any class that implements Serializable. Your result map says it can only hold Date class objects.

When you pass in result, you're setting T to exactly Map of String to Date class objects, which doesn't match Map of String to anything that's Serializable.

One thing to check -- are you sure you want Class<Date> and not Date? A map of String to Class<Date> doesn't sound terribly useful in general (all it can hold is Date.class as values rather than instances of Date)

As for genericizing assertThat, the idea is that the method can ensure that a Matcher that fits the result type is passed in.

like image 76
Scott Stanchfield Avatar answered Oct 23 '22 10:10

Scott Stanchfield