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.
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 letT
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
.