12

Whilst I was working on a project involving Java 8's new streams, I noticed that when I called Stream#toArray() on a stream, it return an Object[] instead of a T[]. Surprised as I was, I started digging into the source code of Java 8 and couldn't find any reason why they didn't implement Object[] toArray(); as T[] toArray();. Is there any reasoning behind this, or is it just an (in)consistency?

EDIT 1: I noticed in the answers that a lot of people said this would not be possible, but this code snippet compiles and return the expected result?

import java.util.Arrays;

public class Test<R> {

    private Object[] items;

    public Test(R[] items) {
        this.items = items;
    }

    public R[] toArray() {
        return (R[]) items;
    }

    public static void main(String[] args) {
        Test<Integer> integerTest = new Test<>(new Integer[]{
            1, 2, 3, 4
        });

        System.out.println(Arrays.toString(integerTest.toArray()));
    }

}
Martijn
  • 2,146
  • 2
  • 19
  • 49
  • 3
    How would it know `R`? If you want specific types to be returned, pass a generator to the method. For example for a String array, do the following: `streamString.toArray(String[]::new)` – meskobalazs Oct 30 '14 at 21:24
  • `R` is the type parameter of the Stream interface. I used `R` in my question, but it should be `T`. Will update that. – Martijn Oct 30 '14 at 21:26
  • @meskobalazs But still, worst case scenario, you could cast it to the desired type. Why did they choose to go the wildcard path instead of the typed path? I feel sometimes, that it is unknown to the programmer what type stream has so having to case it, doesn't make for the best code. And the stream is still the one that knows exactly what type it contains. – Martijn Oct 30 '14 at 21:29
  • 1
    You wouldn't know to which type you should cast, because at runtime Java doesn't know the actual type, because of the *type erasure* (just as it stands in the answer of @Susei) – meskobalazs Oct 30 '14 at 21:32
  • But it's possible? `R[] toArray() { return (R[]) new Object[0]; }` – Martijn Oct 30 '14 at 21:34
  • 1
    An `Object[]` is not necessarily a `R[]`. That cast is bound to fail at runtime. – Sotirios Delimanolis Oct 30 '14 at 21:35
  • 2
    Object does not extends `R`, so this will throw a `RuntimeException` – meskobalazs Oct 30 '14 at 21:35
  • It is in this case. A stream can only consist of one type, so in this case the type must be `R[]`. There is no exception. – Martijn Oct 30 '14 at 21:36
  • @meskobalazs We are casting objects that most certainly extend `R`. – Martijn Oct 30 '14 at 21:37
  • 1
    The cast to `R[]` should be your warning sign. This is an _unchecked_ cast, and the compiler tells you so (but you probably just ignored this). `items[]` is *not* an array of `R[]`, so the compiler is quite right to say that it can't verify this cast -- and it can't stop a client from casting the returned array to `Object[]`, and then putting non-R values in it, which would subvert the supposed type-safety of saying "this is an array of R". – Brian Goetz Apr 21 '16 at 20:34
  • 1
    Note that there's also a `toArray` method that takes an array factory, which you can call as `String[] arr = stream.toArray(String[]::new)`. – Brian Goetz Apr 21 '16 at 20:36

3 Answers3

21

Try:

String[] = IntStream.range(0, 10).mapToObj(Object::toString).toArray(String[]::new);

The no-arg toArray() method will just return an Object[], but if you pass an array factory (which can be conveniently represented as an array constructor reference), you can get whatever (compatible) type you like.

Brian Goetz
  • 76,505
  • 17
  • 128
  • 138
10

This is the same problem that List#toArray() has. Type erasure prevents us from knowing the type of array we should return. Consider the following

class Custom<T> {
    private T t;
    public Custom (T t) {this.t = t;}
    public T[] toArray() {return (T[]) new Object[] {t};} // if not Object[], what type?
}

Custom<String> custom = new Custom("hey");
String[] arr = custom.toArray(); // fails

An Object[] is not a String[] and therefore cannot be assigned to one, regardless of the cast. The same idea applies to Stream and List. Use the overloaded toArray(..) method.

Sotirios Delimanolis
  • 252,278
  • 54
  • 635
  • 683
  • Look at my edit. Could you explain why this works then? – Martijn Oct 30 '14 at 21:44
  • 1
    Your code is fine because `items` is actually an `Integer[]`, not an `Object[]` that has been cast to something else. – Louis Wasserman Oct 30 '14 at 21:45
  • 2
    @MartijnRiemers In your example, you are returning the array you already created (for which you know the type). A `Stream` (or a `List`) is not guaranteed to be backed by an array. It can be constructed by any other means. This means that a **new** array has to be created, but we can't know which type. – Sotirios Delimanolis Oct 30 '14 at 21:46
  • It still feels a bit weird but your casting example makes a lot of sense. Accepted as the answer. – Martijn Oct 30 '14 at 21:47
  • Digging further in, why does ArrayList return an `Object[]` while the underlying array of items is of type `E[]`? I came across this one whilst I was looking at `Stream#collect(Collectors#toList()).toArray()`. – Martijn Oct 30 '14 at 21:56
  • 2
    @MartijnRiemers The real (runtime) type of the `ArrayList`'s underlying array is `Object[]`. – Sotirios Delimanolis Oct 30 '14 at 22:01
  • @SotiriosDelimanolis Maybe, it's just me, but I feel this is one of the most confusing parts of Java. Wouldn't it make more sense to have the class of the generic type to call on e.g. `R.class.cast()`, making generics a whole lot more useful. – Martijn Oct 30 '14 at 22:04
  • 1
    @MartijnRiemers They made the decision to implement generics with type erasure. There's a lot of discussion online about whether this was a good choice or a bad one. You might want to look into them. It's currently impossible to do things like `R.class` when `R` is a type variable. – Sotirios Delimanolis Oct 30 '14 at 22:06
  • @SotiriosDelimanolis I see multiple packages using the `Class clazz`trick nowadays. These are the parts where .NET wins it over Java. – Martijn Oct 30 '14 at 22:08
5

About the reason why toArray() returns Object[]: it is because of type erasure. Generic types lose their type parameters at runtime so Stream<Integer>, Stream<String> and Stream become the same types. Therefore there is no way to determine component type of array to create. Actually, one could analyze types of array's elements using reflection and then try to find their least upper bound, but this is too complicated and slow.

There is a way to get R[] array by using overloaded toArray(IntFunction<A[]> generator) method. This method gives the caller an opportunity to choose type of the array. See this SO question for code examples: How to Convert a Java 8 Stream to an Array?.

Community
  • 1
  • 1
Stanislav Lukyanov
  • 1,420
  • 7
  • 16