Java generics and wildcards: How to make this code compile?
I'm writing some matchers using the Hamcrest 1.2 library, but I'm having a hard time with Java wildcards. When I try to compile the following code
public class GenericsTest {
public void doesNotCompile() {
Container<String> container = new Container<String>();
// this is the desired assertion syntax
assertThat(container, hasSomethingWhich(is("foo")));
}
// these two are a custom made class and matcher; they can be changed
public static class Container<T> {
public boolean hasSomethingMatching(Matcher<T> matcher) {
T something = null; // here is some application logic
return matcher.matches(something);
}
}
public static <T> Matcher<Container<T>> hasSomethingWhich(final Matcher<T> matcher) {
return new TypeSafeMatcher<Container<T>>() {
@Override
protected boolean matchesSafely(Container<T> container) {
return container.hasSomethingMatching(matcher);
}
};
}
// the following signatures are from the Hamcrest 1.2 library; they cannot be changed
public static <T> void assertThat(T actual, Matcher<? super T> matcher) {
}
public static <T> Matcher<? super T> is(T value) {
return null;
}
public interface Matcher<T> {
boolean matches(Object item);
}
public static abstract class TypeSafeMatcher<T> implements Matcher<T> {
@SuppressWarnings({"unchecked"})
@Override
public final boolean matches(Object item) {
return matchesSafely((T) item);
}
protected abstract boolean matchesSafely(T item);
开发者_Python百科}
}
it produces the compile error
$ javac GenericsTest.java
GenericsTest.java:7: <T>assertThat(T,GenericsTest.Matcher<? super T>) in GenericsTest cannot be applied to (GenericsTest
.Container<java.lang.String>,GenericsTest.Matcher<GenericsTest.Container<capture#928 of ? super java.lang.String>>)
assertThat(container, hasSomethingWhich(is("foo")));
^
1 error
How to modify the code so that it will compile? I've tried different combinations of ? super
and ? extends
in the signatures of the Container class and the hasSomethingWhich method, but have not been able to make it compile (without the use of explicit method type parameters, but that produces ugly code: GenericsTest.<String>hasSomethingWhich
).
Also alternative approaches for creating a succinct and readable assertion syntax are welcome. Whatever the syntax, it should accept as parameters a Container and a Matcher for matching the elements inside the Container.
The is(T)
matcher returns a Matcher whose signature is Matcher<? super T>
.
So if you deconstruct the line assertThat(container, hasSomethingWhich(is("foo")))
what you really have is:
Matcher<? super String> matcher = is("foo");
assertThat(container, hasSomethingWhich(matcher));
The second line has a compilation error because the signature of your hasSomethingWhich
method requires a parameter of Matcher<T>
. To match the return type of hamcrest's is(T)
, your signature should instead be:
public static <T> Matcher<Container<T>> hasSomethingWhich(final Matcher<? super T> matcher)
(the difference is changing the parameter from Matcher<T>
to Matcher<? super T>
.
This will then force you to change the signature of hasSomethingWhich()
to also accept a Matcher<? super T>
like so:
public boolean hasSomethingMatching(Matcher<? super T> matcher)
Here is the fully modified version of the original code you posted which compiles successfully for me.
matt is right about <? super T>
in hasSomethingMatching()/hasSomethingWhich()
to make it work:
Matcher<Container<String>> tmp = hasSomethingWhich(is("foo"));
assertThat(container, tmp);
the tmp
variable is necessary, javac will only infer T==String in that assignment, not in arbitrary expressions. (or you can explicitly specify T as String when invoking the method).
If eclipse relaxes the inference rules, that is against the language spec. Let' see in this example why it is inappropriate:
public static <T> Matcher<Container<T>>
hasSomethingWhich(final Matcher<? super T> matcher)
This method is inherently dangerous. given a Match<Object>
, it can return a Matcher<Container<Foo>>
where Foo
can be anything. There is no way to know what the heck T
is, unless the caller supplies T
explicitly, or the compiler has to infer T
from the context.
The language spec defines the inference rule in the above assignment statement, because the developer intention is absolutely clear that T should be exactly String
Advocates of more inference rules must supply the exact set of rules they want, prove that the rules are safe and robust, and comprehensible to mortals.
I was able to create a couple of workarounds to achieve the desired syntax.
Option 1
One workaround is to create a replacement for the assertThat
method, so that it takes a Container<T>
as parameter. The replacement assert method should even be able to have the same name, when the methods are in different classes.
This requires weird additions of ? super
for example in the return type of hasSomethingWhich
and the type parameter of hasSomethingMatching
had to be relaxed. So the code becomes hard to understand.
public class GenericsTest {
public void doesNotCompile() {
Container<String> container = new Container<String>();
// this is the desired assertion syntax
assertThat2(container, hasSomethingWhich(is("foo")));
}
public static <T> void assertThat2(Container<T> events, Matcher<? super Container<T>> matcher) {
assertThat(events, matcher);
}
// these two are a custom made class and matcher; they can be changed
public static class Container<T> {
public boolean hasSomethingMatching(Matcher<?> matcher) {
T something = null; // here is some application logic
return matcher.matches(something);
}
}
public static <T> Matcher<Container<? super T>> hasSomethingWhich(final Matcher<? super T> matcher) {
return new TypeSafeMatcher<Container<? super T>>() {
@Override
protected boolean matchesSafely(Container<? super T> container) {
return container.hasSomethingMatching(matcher);
}
};
}
// the following signatures are from the Hamcrest 1.2 library; they cannot be changed
public static <T> void assertThat(T actual, Matcher<? super T> matcher) {
}
public static <T> Matcher<? super T> is(T value) {
return null;
}
public interface Matcher<T> {
boolean matches(Object item);
}
public static abstract class TypeSafeMatcher<T> implements Matcher<T> {
@SuppressWarnings({"unchecked"})
@Override
public final boolean matches(Object item) {
return matchesSafely((T) item);
}
protected abstract boolean matchesSafely(T item);
}
}
Option 2
The other solution, which is much simpler, is to give up on the type parameters and just use <?>
. The tests will anyways find out at runtime if there is a type mismatch, so compile time type safety is of little use.
public class GenericsTest {
public void doesNotCompile() {
Container<String> container = new Container<String>();
// this is the desired assertion syntax
assertThat(container, hasSomethingWhich(is("foo")));
}
// these two are a custom made class and matcher; they can be changed
public static class Container<T> {
public boolean hasSomethingMatching(Matcher<?> matcher) {
T something = null; // here is some application logic
return matcher.matches(something);
}
}
public static Matcher<Container<?>> hasSomethingWhich(final Matcher<?> matcher) {
return new TypeSafeMatcher<Container<?>>() {
@Override
protected boolean matchesSafely(Container<?> container) {
return container.hasSomethingMatching(matcher);
}
};
}
// the following signatures are from the Hamcrest 1.2 library; they cannot be changed
public static <T> void assertThat(T actual, Matcher<? super T> matcher) {
}
public static <T> Matcher<? super T> is(T value) {
return null;
}
public interface Matcher<T> {
boolean matches(Object item);
}
public static abstract class TypeSafeMatcher<T> implements Matcher<T> {
@SuppressWarnings({"unchecked"})
@Override
public final boolean matches(Object item) {
return matchesSafely((T) item);
}
protected abstract boolean matchesSafely(T item);
}
}
精彩评论