0

I know that the Java Stream<> interface provides several ways to convert a stream to an array. But the Collection.toArray(T[] array) method is a little different. It has a few clever (at the time) requirements, including:

  • If the array you pass is big enough, the array you pass must be used; otherwise a new one must be created.
  • If the array is bigger than necessary, null must be added after the last element.

So if my Collection<T> implementation retrieves its values from some Stream<FooBar> (with a converter strategy that converts to T, how can I convert from my stream to the array required by Collection.toArray(T[] array)?

Without a lot of thought, it would seem I have to do this:

@Override
public <T> T[] toArray(T[] array) {
  try (final Stream<FooBar> stream = getStream()) {
    T[] result = stream.map(converter::toT).toArray(length ->
        (T[])Array.newInstance(array.getClass(), length));
    if(result.length <= array.length) {
      System.arraycopy(result, 0, array, 0, result.length);
      if(result.length < array.length) {
        array[result.length] = null;
      }
      result = array;
    }
  }
  return result;
}

But is there some more concise way to do this? Is there some way I could transfer the stream directly into the given array if possible? And does the Stream<> API already provide for something like this: creating an array as the Collection<>.toArray(T[] array) API expects?

Response to possible duplicates

I know how to convert a stream to an array. This question is very specific: how to follow the contract of Collection.toArray(T[] array). I don't think this is a duplicate, for two reasons: the other answers do not re-use an existing array if it is big enough, and they don't mark an element with null if the existing array is too big.

halfer
  • 19,824
  • 17
  • 99
  • 186
Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • 1
    Here you can find it: https://stackoverflow.com/questions/23079003/how-to-convert-a-java-8-stream-to-an-array – Eduardo Meneses Nov 13 '18 at 16:46
  • @nullpointer, this is not a duplicate. Please explain how this is a duplicate. I know how to convert a stream to an array. I'm asking how to most efficiently create stream to an array with the added restrictions that the `Collections.toArray(T[] array)` provides. Did you read that part of the question? – Garret Wilson Nov 13 '18 at 21:49
  • 2
    The easiest way would be for your collection to extend AbstractCollection, and let the default implementation do its job. – JB Nizet Nov 13 '18 at 22:09
  • 2
    If performance is not a major concern, you can defer to the JDK: `getStream().collect(Collectors.toList()).toArray(array)` – shmosel Nov 13 '18 at 22:13
  • OK so may I ask why you need that? Holger already said in his answer what happens in java-11, you can even read the answer from the method creator [himself](https://stackoverflow.com/questions/51902362/collections-emptylist-singleton-singletonlist-list-set-toarray)... so why would you need that? – Eugene Nov 15 '18 at 08:10
  • "OK so may I ask why you need that?" Because it's the contract of `Collection.toArray()`. I didn't invent it. Every `Collection` must implement that method according to the API contract. – Garret Wilson Nov 15 '18 at 17:48
  • Why don't you extend AbstractCollection? – shmosel Nov 15 '18 at 17:55
  • Everyone, thank you for the suggestion to extend `AbstractCollection`. I am aware of that option. Now does anyone have an answer to the question? – Garret Wilson Nov 16 '18 at 00:51

1 Answers1

2

A very recommended read is the article Arrays of Wisdom of the Ancients.

In short, contrary to intuition, passing a pre-sized array to the Collections.toArray(T[]) method turns out to be less efficient than passing a zero sized array, which only serves to determine the result type but lets the collection allocate the result array.

That’s why Java 11’s new default method <T> T[] toArray​(IntFunction<T[]> generator) does not use the function to allocate an array of the collection’s size but rather to allocate a zero sized array to be passed to <T> T[] toArray​(T[] a).

So it’s worth rethinking whether you really want such a contract for a method or which actual use cases you really want to optimize for (as you can’t serve all at once).

E.g. considering that passing a zero sized array is the most efficient choice anyway, you could optimize for exactly that case

@Override
public <T> T[] toArray(T[] array) {
    T[] template = array.length == 0? array: Arrays.copyOf(array, 0);
    try(Stream<FooBar> stream = getStream()) {
        T[] result = stream.map(converter::toT)
            .toArray(length -> Arrays.copyOf(template, length));
        if(result.length > array.length) return result;
        System.arraycopy(result, 0, array, 0, result.length);
        if(result.length < array.length) array[result.length] = null;
        return array;
    }
}

Note that when you have to implement that method because you’re implementing a Collection, there are plenty of helpful abstract base classes in the JDK which provide an implementation already.

You could even utilize such an implementation when you are not implementing a collection, e.g.

public <T> T[] toArray(T[] array) {
    try(final Stream<FooBar> stream = getStream()) {
        return new AbstractCollection<XYZ>() {
            public Iterator<XYZ> iterator() {
                return stream.map(converter::toT).iterator();
            }
            public int size() { return 0; } // don't know beforehand
        }.toArray(array);
    }
}

You have to replace XYZ with the return type of the converter.toT(FooBar) method.

Which leads to the bigger question, how converter::toT is supposed to convert to the right type without actually knowing what T is.

Holger
  • 285,553
  • 42
  • 434
  • 765