When do Java generics require <? extends T> ins

2019-01-01 05:20发布

问题:

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());
    }
}

回答1:

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.



回答2:

Thanks to everyone who answered the question, it really helped clarify things for me. In the end Scott Stanchfield\'s answer got the closest to how I ended up understanding it, but since I didn\'t understand him when he first wrote it, I am trying to restate the problem so that hopefully someone else will benefit.

I\'m going to restate the question in terms of List, since it has only one generic parameter and that will make it easier to understand.

The purpose of the parametrized class (such as List<Date> or Map<K, V> as in the example) is to force a downcast and to have the compiler guarantee that this is safe (no runtime exceptions).

Consider the case of List. The essence of my question is why a method that takes a type T and a List won\'t accept a List of something further down the chain of inheritance than T. Consider this contrived example:

List<java.util.Date> dateList = new ArrayList<java.util.Date>();
Serializable s = new String();
addGeneric(s, dateList);

....
private <T> void addGeneric(T element, List<T> list) {
    list.add(element);
}

This will not compile, because the list parameter is a list of dates, not a list of strings. Generics would not be very useful if this did compile.

The same thing applies to a Map<String, Class<? extends Serializable>> It is not the same thing as a Map<String, Class<java.util.Date>>. They are not covariant, so if I wanted to take a value from the map containing date classes and put it into the map containing serializable elements, that is fine, but a method signature that says:

private <T> void genericAdd(T value, List<T> list)

Wants to be able to do both:

T x = list.get(0);

and

list.add(value);

In this case, even though the junit method doesn\'t actually care about these things, the method signature requires the covariance, which it is not getting, therefore it does not compile.

On the second question,

Matcher<? extends T>

Would have the downside of really accepting anything when T is an Object, which is not the APIs intent. The intent is to statically ensure that the matcher matches the actual object, and there is no way to exclude Object from that calculation.

The answer to the third question is that nothing would be lost, in terms of unchecked functionality (there would be no unsafe typecasting within the JUnit API if this method was not genericized), but they are trying to accomplish something else - statically ensure that the two parameters are likely to match.

EDIT (after further contemplation and experience):

One of the big issues with the assertThat method signature is attempts to equate a variable T with a generic parameter of T. That doesn\'t work, because they are not covariant. So for example you may have a T which is a List<String> but then pass a match that the compiler works out to Matcher<ArrayList<T>>. Now if it wasn\'t a type parameter, things would be fine, because List and ArrayList are covariant, but since Generics, as far as the compiler is concerned require ArrayList, it can\'t tolerate a List for reasons that I hope are clear from the above.



回答3:

It boils down to:

Class<? extends Serializable> c1 = null;
Class<java.util.Date> d1 = null;
c1 = d1; // compiles
d1 = c1; // wont compile - would require cast to Date

You can see the Class reference c1 could contain a Long instance (since the underlying object at a given time could have been List<Long>), but obviously cannot be cast to a Date since there is no guarantee that the \"unknown\" class was Date. It is not typsesafe, so the compiler disallows it.

However, if we introduce some other object, say List (in your example this object is Matcher), then the following becomes true:

List<Class<? extends Serializable>> l1 = null;
List<Class<java.util.Date>> l2 = null;
l1 = l2; // wont compile
l2 = l1; // wont compile

...However, if the type of the List becomes ? extends T instead of T....

List<? extends Class<? extends Serializable>> l1 = null;
List<? extends Class<java.util.Date>> l2 = null;
l1 = l2; // compiles
l2 = l1; // won\'t compile

I think by changing Matcher<T> to Matcher<? extends T>, you are basically introducing the scenario similar to assigning l1 = l2;

It\'s still very confusing having nested wildcards, but hopefully that makes sense as to why it helps to understand generics by looking at how you can assign generic references to each other. It\'s also further confusing since the compiler is inferring the type of T when you make the function call (you are not explicitly telling it was T is).



回答4:

The reason your original code doesn\'t compile is that <? extends Serializable> does not mean, \"any class that extends Serializable,\" but \"some unknown but specific class that extends Serializable.\"

For example, given the code as written, it is perfectly valid to assign new TreeMap<String, Long.class>()> to expected. If the compiler allowed the code to compile, the assertThat() would presumably break because it would expect Date objects instead of the Long objects it finds in the map.



回答5:

One way for me to understand wildcards is to think that the wildcard isn\'t specifying the type of the possible objects that given generic reference can \"have\", but the type of other generic references that it is is compatible with (this may sound confusing...) As such, the first answer is very misleading in it\'s wording.

In other words, List<? extends Serializable> means you can assign that reference to other Lists where the type is some unknown type which is or a subclass of Serializable. DO NOT think of it in terms of A SINGLE LIST being able to hold subclasses of Serializable (because that is incorrect semantics and leads to a misunderstanding of Generics).



回答6:

I know this is an old question but I want to share an example that I think explains bounded wildcards pretty well. java.util.Collections offers this method:

public static <T> void sort(List<T> list, Comparator<? super T> c) {
    list.sort(c);
}

If we have a List of T, the List can, of course, contain instances of types that extend T. If the List contains Animals, the List can contain both Dogs and Cats (both Animals). Dogs have a property \"woofVolume\" and Cats have a property \"meowVolume.\" While we might like to sort based upon these properties particular to subclasses of T, how can we expect this method to do that? A limitation of Comparator is that it can compare only two things of only one type (T). So, requiring simply a Comparator<T> would make this method usable. But, the creator of this method recognized that if something is a T, then it is also an instance of the superclasses of T. Therefore, he allows us to use a Comparator of T or any superclass of T, i.e. ? super T.



回答7:

what if you use

Map<String, ? extends Class<? extends Serializable>> expected = null;