I've been trying to get my head around java generics for the last few days. From what I understand, Java generics are not covariant so List<Object>
is not assignment compatible with other generics List
s
But here in the following program, nameAndPhone.collect()
method returns a List of type List<NamePhone>
and when I replace reference variable List<NamePhone> npList
with List<Object> npList
the program still compiles without warnings.
I tried this with a similar method returning List<String>
as well, and using List<Object>
reference variable did not result in any error.
Why is List<Object>
assignment compatible with List<NamePhone>
here?
import java.util.*;
import java.util.stream.*;
class NamePhoneEmail
{
String name;
String phonenum;
String email;
NamePhoneEmail(String n, String p, String e)
{
name = n;
phonenum = p;
email = e;
}
}
class NamePhone
{
String name;
String phonenum;
NamePhone(String n, String p)
{
name = n;
phonenum = p;
}
}
public class CollectDemo
{
public static void main(String[] args)
{
ArrayList<NamePhoneEmail> myList = new ArrayList<>();
myList.add(
new NamePhoneEmail("Larry", "555-5555", "[email protected]"));
myList.add(
new NamePhoneEmail("James", "555-4444", "[email protected]"));
Stream<NamePhone> nameAndPhone =
myList.stream().map((a) -> new NamePhone(a.name, a.phonenum));
List<NamePhone> npList = nameAndPhone.collect(Collectors.toList());
}
}
You are right that the types are not assignment compatible.
In doubt, this can easily be verified:
List<Object> a = null;
List<NamePhone> b = null;
a = b; // Error!
The reason of why it seems to be assignment compatible in this case is the target type inference. The inference process can be complicated - particularly in this case, which involves a Collector
, which has three type parameters.
I'll try to flesh out the relevant parts here:
The signature of the collect
method is as follows:
<R, A> R collect(Collector<? super T, A, R> collector);
This is called on the Stream<T>
instance. In your case, this is a Stream<NamePhone>
. But note that the method itself has additional generic parameters, namely R
and A
. The relevant one here is R
, which is the return type.
The Collector
that is passed in there is the one created by the toList
method, which looks as follows:
public static <T> Collector<T, ?, List<T>> toList()
It is also generic. The type parameter will basically be "substituted", based on the context in which the method is called.
So when you write this:
List<NamePhone> npList = nameAndPhone.collect(Collectors.toList());
then you will have the following type assignments:
- The
T
of theStream
isNamePhone
- The
T
of theCollector
isNamePhone
- The
R
of thecollect
method isList<NamePhone>
But you could also write
List<Object> npList = nameAndPhone.collect(Collectors.toList());
In this case
- The
T
of theStream
isNamePhone
- The
T
of theCollector
isObject
- The
R
of thecollect
method isList<Object>
Note that this is only possible because the collect
method accepts a Collector<? super T, ...>
. It would not work if it expected a Collector<T, ...>
. This basically means that you can use the elements from the Stream
and collect them into a new List
, as long as the type parameter of the desired list is a supertype of the elements in the stream.
Conceptually, this makes sense, because it's in some way analogous to
List<Integer> integers = ...;
List<Number> numbers = ...;
for (Integer i : integers) numbers.add(i); // This should work as well!