3

I have trouble understanding following code about two Predicate objects. The first one uses a lower bounded wildcard, the second a upper bounded.

Predicate<? super String> p1 = s -> s.startsWith("a"); // why can I call startsWith()?
Predicate<? extends String> p2 = s -> s.startsWith("a");

p1.test("a"); // works
p2.test("a"); // doesn't work (why?)

What I don't understand about p1 is, why is it possible to call methods from the class String, e.g. startsWith()? Why can I only pass String objects into p1.test(), I expected to be able to call it for Number and Object objects as well.

As p1 behaves I thought p2 would, but this isn't the case. I can't even pass a String object into p2.test(). This doesn't makes sense to me, because we expect an object which inherits from String (including String).

I think it maybe has something to do with the fact, that we specify the reference type rather than the type of the object itself. But what type is then used for the object?

Huntro
  • 322
  • 1
  • 3
  • 16

2 Answers2

5

It's legal for you to call startsWith for p1, even though p1 is typed with a lower bound ? super String, because the type argument is inferred to be String. The lambda expression s -> s.startsWith("a"); is inferred to be a Predicate<String>, which is legal to assign to a variable of type Predicate<? super String>.

This compiles:

Predicate<String> ps = s -> s.startsWith("a");
Predicate<? super String> p1 = ps;

This does not:

// no "startsWith" on Object
Predicate<? super String> p1 = (Object s) -> s.startsWith("a");

The JLS reference is in Section 15.27.3, "Type of a Lambda Expression".

If T is a wildcard-parameterized functional interface type and the lambda expression is implicitly typed, then the ground target type is the non-wildcard parameterization (§9.9) of T.

Here, the ground target type is the type of the lambda expression, and T is the target type, which here is your lower bound variable datatype. This allows the compiler to assign Predicate<String> as the ground target type, which becomes the type of the lambda expression.

Also notice that you can't pass a superclass object to p1.test, because you could (and you already have) assigned a Predicate<String> to p1, which takes a String.

p1.test(new Object()); // Error: can't pass something higher than String

As for why you can't pass a String to p2.test, when you have an upper bounded wildcard such as ? extends String, that means that the type parameter can be any class that is either String or a subtype. (The compiler ignores that String is final here and there can't be any subclasses of String.) The predicate p2 could be assigned a Predicate<SillyString>, assuming SillyString is a subclass of String. But you can't pass a String to a method that could expect a SillyString.

rgettman
  • 176,041
  • 30
  • 275
  • 357
  • Thanks for your answer, why `p1` is working makes totally sense now. But I'm still struggling with `p2`. What is the actual type of the object, why can't it be `Predicate` but only e.g. `Predicate`? – Huntro Oct 08 '19 at 22:44
  • 1
    With `p2`, you declared the type to be `Predicate extends String>`, so it would be legal to assign a `Predicate` to it, which would in turn only accept a `SillyString` as a parameter. It's not that only `SillyString` would be legal, it that `SillyString` could be there. It's similar to [why you can't call `add` on a `List` with an upper bounded type parameter](https://stackoverflow.com/questions/32283197/why-cant-i-add-to-a-list-extends-string-in-java). – rgettman Oct 08 '19 at 22:47
  • 3
    Huntro - the point that you may be missing is that the compiler decides what you can do to a variable, based on the type of the variable, not on the class of the object that it happens to point to. In other words, when you declare `Predicate extends String> p2`, you're saying `p2` might be a `Predicate` (or it might not). But the compiler assumes the worst, and tells you you can't pass `"a"` (which is not a `SillyString`) to `p2`. It ignores the fact that you happen to have made `p2` reference a `Predicate`. – Dawood ibn Kareem Oct 08 '19 at 22:50
  • Thanks guys, I now got it. The explanations as well as the `List` example have helped. – Huntro Oct 08 '19 at 22:54
2

When you have Predicate<? super String> that means the actual implementation of the Predicate is guaranteed to be able to handle an instance of String. Whether the implementation sees the String as a String or CharSequence or even Object is irrelevant, as long as it can handle the type. This provides flexibility in an API that uses the interface. For example:

void addFilter(Predicate<? super String> filter) { ... }

You could call addFilter with a Predicate<CharSequence> or a Predicate<Object> instance, it doesn't matter to the API. For instance:

addFilter((CharSequence cs) -> cs.length() % 2 == 0);

However, when the API actually invokes the filter it has to pass an instance of String to test. If the API were allowed to call filter.test(new Object()) then the Predicate<CharSequence> passed to addFilter above would fail with a ClassCastException.

When you have Predicate<? extends CharSequence> (using CharSequence since String is a final class) you cannot call test because the implementation probably won't be able to handle any type of CharSequence. For instance:

Predicate<? extends CharSequence> predicate = (String s) -> s.startsWith("...");
predicate.test(new StringBuilder());

A ClassCastException would be thrown because the "real type" of predicate is actually Predicate<String>, not Predicate<StringBuilder>. Thus the compiler rejects the call to test because it's not type-safe.

I also recommend reading What is PECS (Producer Extends Consumer Super)?.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Thank you, your explanation why `Predicate extends CharSequence>` isn't allowed makes sense. – Huntro Oct 08 '19 at 22:59