views:

3027

answers:

6

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

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->Date class objects, which doesn't match Map String -> anything that's Serializable.

One thing to check -- are you sure you want Class<Date> and not Date? A map of String->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.

Scott Stanchfield
In this case, yes I do want a map of classes. The example I gave is contrived to use standard JDK classes rather than my custom classes, but in this case the class is actually instantiated via reflection and used based on the key. (A distributed app where the client doesn't have the server classes available, just the key of which class to use to do the server side work).
Yishai
I guess where my brain is stuck is on why a Map containing classes of type Date doesn't just fit nicely into a type of Maps containing classes of type Serializable. Sure the classes of type Serializable could be other classes as well, but it certainly includes type Date.
Yishai
On the assertThat making sure the cast is performed for you, the matcher.matches() method doesn't care, so since the T is never used, why involve it? (the method return type is void)
Yishai
Ahhh - that's what I get for not reading the def of the assertThat close enough. Looks like it's only to ensure that a fitting Matcher is passed in...
Scott Stanchfield
+1  A: 

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.

erickson
I'm not quite following - when you say "does not mean ... but ...": what is the difference? (like, what would be an example of a "known but nonspecific" class which fits the former definition but not the latter?)
rascher
Yes, that is a bit awkward; not sure how to express it better... does it make more sense to say, "'?' is a type that is unknown, not a type that matches anything?"
erickson
+1  A: 

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).

GreenieMeanie
That certainly helps, but the "may sound confusing" is kind of replaced with a "sounds confusing." As a followup, so why, according to this explanation, does the method with Matcher`<? extends T>` compile?
Yishai
+1  A: 

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).

GreenieMeanie
+1  A: 

what if you use

Map<String, ? extends Class<? extends Serializable>> expected = null;
newacct
Yes, this is kind of what my above answer was driving at.
GreenieMeanie
No, that doesn't help the situation, at least the way I tried.
Yishai
+2  A: 

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>();
Serilizable 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.

Yishai