47

I've stumbled upon a piece of code that has me wondering why it compiles successfully:

public class Main {
    public static void main(String[] args) {
        String s =  newList(); // why does this line compile?
        System.out.println(s);
    }

    private static <T extends List<Integer>> T newList() {
        return (T) new ArrayList<Integer>();
    }
}

What is interesting is that if I modify the signature of method newList with <T extends ArrayList<Integer>> it doesn't work anymore.

Update after comments & responses: If I move the generic type from the method to the class the code doesn't compile anymore:

public class SomeClass<T extends List<Integer>> {
    public  void main(String[] args) {
        String s = newList(); // this doesn't compile anymore
        System.out.println(s);
    }

    private T newList() {
        return (T) new ArrayList<Integer>();
    }
}
Denis Rosca
  • 3,409
  • 19
  • 38
  • 3
    Related http://stackoverflow.com/questions/36402646/generic-return-type-upper-bound-interface-vs-class-surprisingly-valid-code – Tunaki Jun 29 '16 at 11:19
  • 1
    The reason for the update making a difference is that the first version can work if there exists any type satisfying the constraints (T extends List & T extends String) [i.e. it is an existential quantification], whereas the second version can only work if all possible types T that match the declaration in the class also extend String [i.e. it is a universal quantification]. – Periata Breatta Jan 16 '17 at 21:46

3 Answers3

39

If you declare a type parameter at a method, you are allowing the caller to pick an actual type for it, as long as that actual type will fulfill the constraints. That type doesn’t have to be an actual concrete type, it might be an abstract type, a type variable or an intersection type, in other, more colloquial words, a hypothetical type. So, as said by Mureinik, there could be a type extending String and implementing List. We can’t manually specify an intersection type for the invocation, but we can use a type variable to demonstrate the logic:

public class Main {
    public static <X extends String&List<Integer>> void main(String[] args) {
        String s = Main.<X>newList();
        System.out.println(s);
    }

    private static <T extends List<Integer>> T newList() {
        return (T) new ArrayList<Integer>();
    }
}

Of course, newList() can’t fulfill the expectation of returning such a type, but that’s the problem of the definition (or implementation) of this method. You should get an “unchecked” warning when casting ArrayList to T. The only possible correct implementation would be returning null here, which renders the method quite useless.

The point, to repeat the initial statement, is that the caller of a generic method chooses the actual types for the type parameters. In contrast, when you declare a generic class like with

public class SomeClass<T extends List<Integer>> {
    public  void main(String[] args) {
        String s = newList(); // this doesn't compile anymore
        System.out.println(s);
    }

    private T newList() {
        return (T) new ArrayList<Integer>();
    }
}

the type parameter is part of the contract of the class, so whoever creates an instance will pick the actual types for that instance. The instance method main is part of that class and has to obey that contract. You can’t pick the T you want; the actual type for T has been set and in Java, you usually can’t even find out what T is.

The key point of generic programming is to write code that works independently of what actual types have been chosen for the type parameters.

But note that you can create another, independent instance with whatever type you like and invoke the method, e.g.

public class SomeClass<T extends List<Integer>> {
    public <X extends String&List<Integer>> void main(String[] args) {
        String s = new SomeClass<X>().newList();
        System.out.println(s);
    }

    private T newList() {
        return (T) new ArrayList<Integer>();
    }
}

Here, the creator of the new instance picks the actual types for that instance. As said, that actual type doesn’t need to be a concrete type.

Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765
22

I'm guessing this is because List is an interface. If we ignore the fact that String is final for a second, you could, in theory, have a class that extends String (meaning you could assign it to s) but implements List<Integer> (meaning it could be returned from newList()). Once you change the return type from an interface (T extends List) to a concrete class (T extends ArrayList) the compiler can deduce they aren't assignable from each other, and produces an error.

This, of course, breaks down since String is, in fact, final, and we could expect the compiler to take this into account. IMHO, it's a bug, although I must admit I'm no compiler-expert and there might be a good reason to ignore the final modifier at this point.

Mureinik
  • 297,002
  • 52
  • 306
  • 350
  • 2
    Hmm, this is actually a really good point. I didn't think about the possibility of having a class that could in theory extends both String and implement List. As you said this doesn't really apply since String is final, but is an interesting idea. – Denis Rosca Jun 29 '16 at 07:44
  • 9
    That fact that a class is `final` at compile-time is never taken into account in such situations as there is no guaranty that the class will still be `final` at runtime. – Holger Jun 29 '16 at 08:05
  • @errantlinguist I'm not sure I follow what you're saying. The problem is that the presented code compiles when I assign a value of type T extends List to a value of type String. – Denis Rosca Jun 29 '16 at 08:05
  • 2
    Why should this be a "bug"?-- a type is an instance of itself: `String s = "blah"; System.out.println((s instanceof String));` prints out `true`. Therefore, it would seem strange that the set of all classes `all SomeClass where (SomeClass instanceof SomeParentClass) == true` would exclude `SomeParentClass` itself. For that reason, a concrete class `SomeClass` matches a generic specifying all subtypes thereof, e.g. `List – errantlinguist Jun 29 '16 at 08:07
  • 2
    @DenisRosca, Holger just explained above why there is no good reason for preventing `T extends FinalClass`. – errantlinguist Jun 29 '16 at 08:08
  • 1
    @DenisRosca, in regards to my previous comment, I attempted to justify why `T extends String` should also accept `String`. – errantlinguist Jun 29 '16 at 08:09
  • 1
    @Holger do you know where in the specification I could find a more detailed description of this behavior? Shouldn't the compiler apply the same logic to `class MyClass extends String` then? – Denis Rosca Jun 29 '16 at 08:23
  • @Holger what do you think of the second code example? If I move the generic type to the class instead of the method, the code doesn't compile anymore. IMO this is related to how type inference works for generic methods. – Denis Rosca Jun 29 '16 at 08:40
  • 1
    It’s a trade off between complexity of the rules and safety. If you were allowed to subclass a `final` class at compile-time, you very likely create a class which will break at runtime. Not being allowed to subclass `final` classes is a simple rule. If the class is not `final` at runtime, the fact that you didn’t subclass it creates no harm. In contrast, the rules of Generic are quite complex. – Holger Jun 29 '16 at 09:16
  • 1
    E.g., if you invoke `getClass()` on a reference of compile-time type `X`, you’ll get a `Class extends X>` which might represent a subclass of `X`. This also applies when `X` is `String`, when you say `"".getClass()`, you’ll get a `Class extends String>`. Allowing to convert a `Class extends String>` to `Class` just because `String` is `final` at compile-time would undermine the type system, if `String` is not `final` at runtime. – Holger Jun 29 '16 at 09:18
6

I don't know why this compile. On the other hand, I can explain how you can fully leverage the compile-time checks.

So, newList() is a generic method, it has one type parameter. If you specify this parameter, then the compiler will check that for you:

Fails to compile:

String s =  Main.<String>newList(); // this doesn't compile anymore
System.out.println(s);

Passes the compile step:

List<Integer> l =  Main.<ArrayList<Integer>>newList(); // this compiles and works well
System.out.println(l);

Specifying thetype parameter

The type parameters provide compile-time checking only. This is by design, java uses type erasure for generic types. In order to make the compiler work for you, you have to specify those types in the code.

Type parameter at instance-creation

The most common case is to specify the patterns for an object instance. I.e. for lists:

List<String> list = new ArrayList<>();

Here we can see that List<String> specifies the type for the list items. On the other hand, new ArrayList<>() doesn't. It uses the diamond operator instead. I.e. the java compiler infers the type based on the declaration.

Implicit type parameter at method invocation

When you invoke a static method, then you have to specify the type in another way. Sometimes you can specify it as a parameter:

public static <T extends Number> T max(T n1, T n2) {
    if (n1.doubleValue() < n2.doubleValue()) {
        return n2;
    }
    return n1;
}

The you can use it like this:

int max = max(3, 4); // implicit param type: Integer

Or like this:

double max2 = max(3.0, 4.0); // implicit param type: Double

Explicit type parameters at method invocation:

Say for instance, this is how you can create a type-safe empty list:

List<Integer> noIntegers = Collections.<Integer>emptyList();

The type parameter <Integer> is passed to the method emptyList(). The only constraint is that you have to specify the class too. I.e. you cannot do this:

import static java.util.Collections.emptyList;
...
List<Integer> noIntegers = <Integer>emptyList(); // this won't compile

Runtime type token

If none of these tricks can help you, then you can specify a runtime type token. I.e. you provide a class as a parameter. A common example is the EnumMap:

private static enum Letters {A, B, C}; // dummy enum
...
public static void main(String[] args) {
    Map<Letters, Integer> map = new EnumMap<>(Letters.class);
}
Tamas Rev
  • 7,008
  • 5
  • 32
  • 49