The Java 14 Language Specification, Section 5.1.10 (PDF) devotes some paragraphs to why one would prefer providing the wildcard method public
ly, while using the generic method private
ly. 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.