How does the JLS specify that wildcards cannot be formally used within methods?
Frage
I've always been wondering about some weird aspect of Java generics and the use of wildcards. Let's say, I have the following API:
public interface X<E> {
E get();
E set(E e);
}
And then, let's say we declare the following methods:
public class Foo {
public void foo(X<?> x) {
// This does not compile:
x.set(x.get());
}
public <T> void bar(X<T> x) {
// This compiles:
x.set(x.get());
}
}
From my "intuitive" understanding, X<?>
is in fact the same as X<T>
, except that the unknown <T>
is formally unknown to client code. But inside of foo()
, I would guess that the compiler could infer (pseudo JLS code) <T0> := <?>[0], <T1> := <?>[1], etc...
. This is what most programmers do explicitly and intuitively. They delegate to a private helper method, which leads to a lot of useless boiler-plate code:
public class Foo {
public void foo(X<?> x) {
foo0(x);
}
private <T> void foo0(X<T> x) {
x.set(x.get());
}
}
Another example:
public class Foo {
public void foo() {
// Assuming I could instanciate X
X<?> x = new X<Object>();
// Here, I cannot simply add a generic type to foo():
// I have no choice but to introduce a useless foo0() helper method
x.set(x.get());
}
}
In other words, the compiler knows that the wildcard in x.set()
is formally the same as the wildcard in x.get()
. Why can't it use that information? Is there a formal aspect in the JLS, that explains this lacking compiler "feature"?
Lösung
You wonder why isn't it supported? An equally valid question is, why should it be supported?
The way wildcards work, given the declaration X<? extends Y> x
, the type of the expression x.get()
is not ? extends Y
or some type inferred by the compiler, but Y
. Now, Y
isn't assignable to ? extends Y
, so you can't pass x.get()
to x.set
which has the effective signature of void set(? extends Y e)
. The only thing you could actually pass to it is null
, which is assignable to every reference type.
I follow the JDK developer mailing lists fairly closely, and the general spirit there is that every feature the JLS specifies should pull its own weight - the cost/benefit ratio should be heavy on the benefit side. Now, keeping track of the origin of a value would be quite possible, but it would only solve one special case, which has several other ways to solve it. Consider these classes:
class ClientOfX {
X<?> member;
void a(X<?> param) {
param.set(param.get());
}
void b() {
X<?> local = new XImpl<Object>();
local.set(local.get());
}
void c() {
member.set(member.get());
}
void d() {
ClassWeCantChange.x.set(ClassWeCantChange.x.get());
}
}
class ClassWeCantChange {
public static X<?> x;
}
This obviously doesn't compile, but with your proposed enhancement, it does. However, we can get three of the four methods compiling without changing the JLS:
- For
a
, when we receive the generic object as a parameter, we can make the method generic and receive anX<T>
instead ofX<?>
. - For
b
andc
, when using a locally declared reference, we don't need to declare the reference with a wildcard (Eric Lippert of Microsoft's C# compiler team calls this situation "if it hurts when you do that, then don't do that!"). - For
d
, when using a reference whose declaration we can't control, we have no option but to write a helper method.
So the smarter type system would really only help in the case where we can't control the declaration of a variable. And in that situation, the case for doing what you want to do is kind of sketchy - the very act of wildcarding a generic type normally means that the object is either intended to work only as a producer (? extends Something
) or a consumer (? super Something
) of objects, not both.
The TL;DR version is that it would bring little benefit (and require wildcards to be something else than what they are today!) while adding complexity to the JLS. If someone comes up with a very compelling case to add it, the spec could be extended - but once it's there, it's there forever, and it may cause unforeseen problems down the road, so leave it out until you really need it.
Edit: The comments contain more discussion about what wildcards are and aren't.
Andere Tipps
I've just seen this peculiar implementation of Collections.swap()
:
public static void swap(List<?> list, int i, int j) {
final List l = list;
l.set(i, l.set(j, l.get(i)));
}
The JDK guys resort to a raw type, in order to handle this, locally. To me, this is a strong statement indicating that this probably should be supported by the compiler, but for some reason (e.g. lack of time to formally specify this) just wasn't done
Not having been involved in writing the JLS, my best guess is simplicity. Imagine silly, yet (syntactically) valid expressions such as (x = new X<Integer>()).set(x.get());
. Tracking captures of wildcards can get tricky.
I think of capturing wildcards not as boilerplate, but as having done something right. It's a sign that your API is free of type parameters which client code does not need.
Edit: The current behavior is specified in JLS §4.5.2. The x
gets capture-converted for each member access -- once for x.set(...)
and once for x.get()
. The captures are distinct and thus not known to represent the same type.