Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does this java 8 stream operation evaluate to Object instead of List<Object> or just List?

Tags:

java

java-8

I'm working with a 3d party library, and they return Collections that lack type specifications (e.g. public List getFoo();) , and I'm trying to convert their return types and return a list with a proper type.

I have created a simple example that demonstrates the problem. e.g.

edit The original question declared l2 as an ArrayList rather than a List, that is corrected now.

import java.util.List; import java.util.ArrayList; import java.util.stream.Collectors;  public class Foo {     public static void main(String[] args) {         ArrayList l = new ArrayList();         l.add(1);         l.add(2);         List<String> l2 = l.stream().map(Object::toString).collect(Collectors.toList());     } } 

This fails to compile.

$ javac Foo.java Foo.java:10: error: incompatible types: Object cannot be converted to List<String>         List<String> l2 = l.stream().map(Object::toString).collect(Collectors.toList());                                                                   ^ 1 error 

If I modify the program slightly so it compiles and I can check the return type of the stream/collect operation. It "works", although I'd have to cast the result.

e.g.

import java.util.ArrayList; import java.util.stream.Collectors;  public class Foo {     public static void main(String[] args) {         ArrayList l = new ArrayList();         l.add(1);         l.add(2);         Object l2 = l.stream().map(Object::toString).collect(Collectors.toList());         System.out.println(l2.getClass());     } } 

Running this shows...

$ javac Foo.java Note: Foo.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. $ java Foo class java.util.ArrayList 

As expected.

So, Collectors.listCollector() does the right thing at runtime, but is this compile time behavior expected? If so, is there a "proper" way around it?

like image 428
Bill Avatar asked May 13 '16 16:05

Bill


People also ask

What is the purpose of each method of stream in Java 8?

With Java 8, Collection interface has two methods to generate a Stream. stream() − Returns a sequential stream considering collection as its source. parallelStream() − Returns a parallel Stream considering collection as its source.

Why is Java 8 stream lazy?

Streams are lazy because intermediate operations are not evaluated unless terminal operation is invoked. Each intermediate operation creates a new stream, stores the provided operation/function and return the new stream.

How do you convert a List of objects into a stream of objects?

Converting a list to stream is very simple. As List extends the Collection interface, we can use the Collection. stream() method that returns a sequential stream of elements in the list.

When should I use Java 8 streams?

Java 8 offers the possibility to create streams out of three primitive types: int, long and double. As Stream<T> is a generic interface, and there is no way to use primitives as a type parameter with generics, three new special interfaces were created: IntStream, LongStream, DoubleStream.


2 Answers

The stream(), map(), collect(), and all the other stream methods rely on generic typing for things to work well. If you use a raw type as input, then all the generics are erased, and everything turns into a raw type. For example, if you have a List<String>, calling the stream() method gives you an object of type Stream<String>.

But if you start out with a raw type List, then calling stream() gives you an object of the raw type Stream instead.

The map() method has this declaration:

    <R> Stream<R> map(Function<? super T,? extends R> mapper) 

but since it's called on a raw Stream, it's as if the declaration is

    Stream map(Function mapper) 

so the result of calling map() is simply a Stream. The signature of collect() is filled with generics:

    <R> R collect(Supplier<R> supplier,                   BiConsumer<R,? super T> accumulator,                   BiConsumer<R,R> combiner) 

When called with a raw type, it gets erased to

    Object collect(Supplier supplier,                    BiConsumer accumulator,                    BiConsumer combiner) 

so the return type of your stream pipeline is Object when its source is a raw type. Obviously this isn't compatible with ArrayList<String> and you get a compilation error.

To fix this, bring your types into the generic world as soon as possible. This will probably involve unchecked casts, and you should probably suppress the warning. Of course, do this only if you're sure the returned collection contains objects only of the expected type.

In addition, as Hank D pointed out, the toList() collector returns a List, not an ArrayList. You can either assign the return value to a variable of type List, or you can use toCollection() to create an instance of ArrayList explicitly.

Here's the code with these modifications included:

    @SuppressWarnings("unchecked")     ArrayList<Integer> l = (ArrayList<Integer>)getRawArrayList();     l.add(1);     l.add(2);     ArrayList<String> l2 = l.stream()                             .map(Object::toString)                             .collect(Collectors.toCollection(ArrayList::new)); 
like image 171
Stuart Marks Avatar answered Sep 28 '22 10:09

Stuart Marks


Collectors.toList() returns a List, not an ArrayList. This will compile:

public static void main(String[] args) {     ArrayList<?> l = getRawArrayList();     List<String> l2 = l.stream().map(Object::toString).collect(Collectors.toList());  } 
like image 37
Hank D Avatar answered Sep 28 '22 11:09

Hank D