开发者

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜