Search code examples
javagenericstype-inferencereturn-typenested-generics

Java nested generics with raw types return type inference


public class Main {
    List<List<Integer>> f0() {
        return List.of(List.of(1L));
    }
    List<List<Integer>> f1() {
        return List.of((List) List.of(1L));
    }
    List<List<Integer>> f2() {
        var r = List.of((List) List.of(1L));
        return r;
    }
    List<List<Integer>> f3() {
        return List.of((List) List.of(1L), List.of(1L));
    }
    List<List<Integer>> f4() {
        return List.of((List) List.of(1L), (List) List.of(1L));
    }
}

In the code above, f1 and f4 compile while f0, f2 and f3 do not.
I can see why f0 doesn't compile: It needs List<List<Integer>> but it sees List<List<Long>>. But I have no idea why others behave the way they do.

f2:

incompatible types: List<List> cannot be converted to List<List<Integer>>

f3:

error: incompatible types: inference variable E has incompatible bounds
                return List.of((List) List.of(1L), List.of(1L));
                              ^
    equality constraints: Integer
    lower bounds: Long
  where E is a type-variable:
    E extends Object declared in method <E>of(E)

In each function, what exactly is the type of the expression being returned? Why does Java behave like this? And what is happening according to the JLS? I'm using Java 17 if it's important.


Solution

  • The difference between f2 and f0 is that List.of is not in an assignment context in f2, §14.4.1:

    If the LocalVariableType is var, then let T be the type of the initializer expression when treated as if it did not appear in an assignment context ...

    Hence, the call is not a poly expression. It doesn't satisfy the requirements for being a poly expression in §15.12:

    A method invocation expression is a poly expression if all of the following are true:

    • The invocation appears in an assignment context or an invocation context
    • ...

    This means that type inference no longer takes into account the target type (§15.12.2.6).

    If the chosen method is generic and the method invocation does not provide explicit type arguments, the invocation type is inferred as specified in §18.5.2.

    In this case, if the method invocation expression is a poly expression, then its compatibility with a target type is as determined by §18.5.2.1.

    The entirety of §18.5.2.1, which would have added some constraint about Integer, is skipped.

    If you put List.of in a return statement, however, it is in an assignment context (§14.17):

    It is a compile-time error if the return target of a return statement with value Expression is a method with declared return type T, and the type of Expression is not assignable compatible (§5.2) with T.

    (§5.2 is the section for assignment contexts)

    More informally, Java doesn't look at all the places where r is used to infer the type parameter of List.of. It only looks at the call List.of((List) List.of(1L)). It is assigned to a var, so type inference results in List<List>. Type inference has no reason to randomly add a <Integer> in there.

    List<List> is not compatible with List<List<Integer>>, because generics are by default invariant. For how exactly this is specified, see §4.10. Try showing that List<List> is not a subtype of List<List<Integer>>.


    For f3, there are conflicting constraints. I'll use E to refer to the type parameter of the outer List.of call, and E1 and E2 to refer to the type parameters of the first and second inner List.of call.

    Because the outer List.of is in a return statement, §18.5.2.1 applies, and E gets a lower bound of List<Integer>.

    E1 is inferred to have a lower bound of Long, then §18.5.2.1 applies, and the target type is raw List, the type you are casting to. Nothing is wrong with that and we carry on.

    The raw List in the first argument gives an upper bound of List for E. That's fine - no conflicts here.

    In f4, the same thing happens with the second argument and E2, so there is no problem.

    In f3, §18.5.2.1 applies when inferring E2, the target type is List<Integer>, because of the constraint on E. This adds an equality constraint to E2. The argument 1L also adds a lower bound of Long to E2.

    That's where the conflict is. There is no type that satisfies both == Integer and >= Long.