Why use a wild card capture helper method?

1.8k views Asked by At

Referring to : Wildcard Capture Helper Methods

It says to create a helper method to capture the wild card.

public void foo(List<?> i) {
    fooHelper(i);
}        
private <T> void fooHelper(List<T> l) {
    l.set(0, l.get(0));
}

Just using this function below alone doesn't produce any compilation errors, and seems to work the same way. What I don't understand is: why wouldn't you just use this and avoid using a helper?

public <T> void foo(List<T> l) {
    l.set(0, l.get(0));
}

I thought that this question would really boil down to: what's the difference between wildcard and generics? So, I went to this: difference between wildcard and generics. It says to use type parameters:

1) If you want to enforce some relationship on the different types of method arguments, you can't do that with wildcards, you have to use type parameters.

But, isn't that exactly what the wildcard with helper function is actually doing? Is it not enforcing a relationship on different types of method arguments with its setting and getting of unknown values?

My question is: If you have to define something that requires a relationship on different types of method args, then why use wildcards in the first place and then use a helper function for it?

It seems like a hacky way to incorporate wildcards.

4

There are 4 answers

4
dkatzel On BEST ANSWER

In this particular case it's because the List.set(int, E) method requires the type to be the same as the type in the list.

If you don't have the helper method, the compiler doesn't know if ? is the same for List<?> and the return from get(int) so you get a compiler error:

The method set(int, capture#1-of ?) in the type List<capture#1-of ?> is not applicable for the arguments (int, capture#2-of ?)

With the helper method, you are telling the compiler, the type is the same, I just don't know what the type is.

So why have the non-helper method?

Generics weren't introduced until Java 5 so there is a lot of code out there that predates generics. A pre-Java 5 List is now a List<?> so if you were trying to compile old code in a generic aware compiler, you would have to add these helper methods if you couldn't change the method signatures.

0
Bohemian On

I agree: Delete the helper method and type the public API. There's no reason not to, and every reason to.

Just to summarise the need for the helper with the wildcard version: Although it's obvious to us as humans, the compiler doesn't know that the unknown type returned from l.get(0) is the same unknown type of the list itself. ie it doesn't factor in that the parameter of the set() call comes from the same list object as the target, so it must be a safe operation. It only notices that the type returned from get() is unknown and the type of the target list is unknown, and two unknowns are not guaranteed to be the same type.

2
ZhongYu On

You are correct that we don't have to use the wildcard version.

It comes down to which API looks/feels "better", which is subjective

    void foo(List<?> i) 
<T> void foo(List<T> i)

I'll say the 1st version is better.

If there are bounds

    void foo(List<? extends Number> i) 
<T extends Number> void foo(List<T> i)

The 1st version looks even more compact; the type information are all in one place.

At this point of time, the wildcard version is the idiomatic way, and it's more familiar to programmers.

There are a lot of wildcards in JDK method definitions, particularly after java8's introduction of lambda/Stream. They are very ugly, admittedly, because we don't have variance types. But think how much uglier it'll be if we expand all wildcards to type vars.

0
Twisol On

The Java 14 Language Specification, Section 5.1.10 (PDF) devotes some paragraphs to why one would prefer providing the wildcard method publicly, while using the generic method privately. Specifically, they say (of the public generic method):

This is undesirable, as it exposes implementation information to the caller.

What do they mean by this? What exactly is getting exposed in one and not the other?

Did you know you can pass type parameters directly to a method? If you have a static method <T> Foo<T> create() on a Foo class -- yes, this has been most useful to me for static factory methods -- then you can invoke it as Foo.<String>create(). You normally don't need -- or want -- to do this, since Java can sometimes infer those types from any provided arguments. But the fact remains that you can provide those types explicitly.

So the generic <T> void foo(List<T> i) really takes two parameters at the language level: the element type of the list, and the list itself. We've modified the method contract just to save ourselves some time on the implementation side!

It's easy to think that <?> is just shorthand for the more explicit generic syntax, but I think Java's notation actually obscures what's really going on here. Let's translate into the language of type theory for a moment:

/*                 Java *//* Type theory      */
             List<?>     ~~   ∃T. List<T>
    void foo(List<?> l)  ~~  (∃T. List<T>) -> ()
<T> void foo(List<T> l)  ~~   ∀T.(List<T>  -> ()

A type like List<?> is called an existential type. The ? means that there is some type that goes there, but we don't know what it is. On the type theory side, ∃T. means "there exists some T", which is essentially what I said in the previous sentence -- we've just given that type a name, even though we still don't know what it is.

In type theory, functions have type A -> B, where A is the input type and B is the return type. (We write void as () for silly reasons.) Notice that on the second line, our input type is the same existential list we've been discussing.

Something strange happens on the third line! On the Java side, it looks like we've simply named the wildcard (which isn't a bad intuition for it). On the type theory side we've said something _superficially very similar to the previous line: for any type of the caller's choice, we will accept a list of that type. (∀T. is, indeed, read as "for all T".) But the scope of T is now totally different -- the brackets have moved to include the output type! That's critical: we couldn't write something like <T> List<T> reverse(List<T> l) without that wider scope.

But if we don't need that wider scope to describe the function's contract, then reducing the scope of our variables (yes, even type-level variables) makes it easier to reason about those variables. The existential form of the method makes it abundantly clear to the caller that the relevance of the list's element type extends no further than the list itself.