6

I've been doing some code archeology in some odd code and I came across something similar to this:

public abstract class Outer<T>
{
    protected Outer(Inner<?> inner)
    {
        // ...
    }

    public static abstract class Inner<U extends Outer>
    {
        // ...
    }
}

The thing that struck me was that there wasn't an unbounded wildcard type on Inner's usage of the Outer type (the <U extends Outer> bit).

What is the implication of using Inner<U extends Outer<?>> vs. Inner<U extends Outer>?

I can compile and run tests successfully with both type versions, but am stumped on what is going on under the hood.

entpnerd
  • 10,049
  • 8
  • 47
  • 68
Alex Moore
  • 3,415
  • 1
  • 23
  • 39
  • Is there a compile time warning? – Thilo Jan 27 '16 at 04:57
  • Nope, no warnings for either method. – Alex Moore Jan 27 '16 at 04:58
  • 1
    After type erasure, both signatures should be reduced to `Inner` by my calculation. I think the correct answer will need to show some edge cases. – Tim Biegeleisen Jan 27 '16 at 04:59
  • If you had an assignment within `Inner` such as `U x = new OuterSubclass();` I believe you'd get a warning about an unchecked assignment. Where if U were a subclass of unbounded `Outer>`, you wouldn't. AFAIK that's the only difference. – Gene Jan 27 '16 at 05:08
  • 1
    @TimBiegeleisen Yeah, that's what I'm thinking too. I can't think of any edge cases that would show it other than the one @gene mentions. The inner classes are a weird Builder "pattern", but it only uses a statically typed private field of `Outer` in a copy method: `final Inner copyFrom(Outer> other) {inner.field = outer.field}`. – Alex Moore Jan 27 '16 at 05:19
  • Well the code you actually have uses `Inner` without the unbounded wildcard, so at least you don't have to dwell on the alternative scenario. – Tim Biegeleisen Jan 27 '16 at 05:28
  • @TimBiegeleisen it's not really an edge case. See my answer. TL;DR is using the raw type bypasses type checks related to the PECS principle. – herman Jan 27 '16 at 13:15
  • @AlexMoore, let me know if anything in my answer is unclear. – herman Jan 27 '16 at 13:23

1 Answers1

2
  1. 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).
  2. The fact that it's a static nested class instead of a top-level class doesn't matter here.
  3. 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.

Community
  • 1
  • 1
herman
  • 11,740
  • 5
  • 47
  • 58
  • Thanks for the very detailed explanation! I mix "inner" and "static nested" often, I came from C# :) For our purposes, I don't think you could ever have/use a raw class object, so I'm going to add the unbounded wildcard where it's missing. I don't need to know the type parameter, but where we use it we don't care about it - we just know that it has one. – Alex Moore Jan 27 '16 at 16:45