- Although called
Inner
in the example, it's not actually an inner class, but a static nested class. Inner classes are non-static nested classes (see https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html).
- The fact that it's a static nested class instead of a top-level class doesn't matter here.
- Code using class
Inner
can either use the raw type Inner
(in which all type checks are bypassed - not interesting here), or specify an actual type for the type parameter U
. In the latter case the upper bound restricts that type to subtypes of the generic type Outer<T>
where T
can be any type, regardless of whether Inner
is declared as Inner<U extends Outer>
or Inner<U extends Outer<?>>
.
The use of a raw type in the class signature still makes it possible to use strong type checking when declaring variables or parameters. For example, the following will compile (assuming Inner
has a no-args constructor):
Outer.Inner<Outer<String>> x = new Outer.Inner<Outer<String>>();
But replacing Outer<String>
on either side (but not on the other) with Outer
will yield a compiler error. This behavior would be exactly the same in case an unbounded wildcard would be used instead of a raw type, so no difference so far.
The actual difference is in how the the class Inner
is allowed to use variables of type U
. Suppose you are passing in such a variable in the constructor:
public Inner(U u) { this.u = u; }
Suppose also that Outer
has a method that takes an argument of type T
(its own type parameter), e.g.:
void add(T) { ...}
Now, in case of the raw upper bound (U extends Outer
), it would be legal for code in class Inner
to call this method with any object, e.g. a String:
this.u.add("anything")
although a compiler warning would be issued (unless suppressed), and in case the actual runtime type T
would be different from String
, a ClassCastException
would be thrown in code that depends on the object being a different type.
In the case of an unbounded wildcard however (U extends Outer<?>
), since T
is a specific but unknown type, calling the add
method will result in a compiler error regardless of which argument you give it.
Since you mention the code compiles fine in both cases, such method consuming T
either doesn't exist in Outer
, or it is not being called from Inner
. But by adding the unbounded wildcard, you can prove to the users of the class that this is not happening (because otherwise the code wouldn't compile).
In order to allow calling this.u.add(s)
with s
being a String
argument without using a raw type for the upper bound, Inner
would have to be declared as Inner<U extends Outer<? super String>>
, following the PECS principle, since U
is the type of a consumer in this case.