Search code examples
javaeclipse-photonjava-11

How do I provide a generator function to Collection.toArray() using JDK 11?


I have upgraded Eclipse Photon 4.8 (http://download.eclipse.org/eclipse/downloads/drops4/S-4.9M2-201808012000/) to support JDK 11 (https://marketplace.eclipse.org/content/java-11-support-eclipse-photon-49). It seems to be working fine (Version: 4.9 Build id: I20180801-2000).

In JDK 11 there is a new override of method toArray() in Java.util.Collection:

default <T> T[] toArray(IntFunction<T[]> generator) {
    return toArray(generator.apply(0));
}

It is a default method, but it is not overriden. All it does is pass the value returned by the supplied generator function (using a hard-coded argument of zero) to another override of toArray() which then returns the content of the Collection as an array.

As described in the Javadoc for that method, it can be called like this:

String[] y = x.toArray(String[]::new);

That works fine, and an array of String of the appropriate length, corresponding to the Collection<String>, is returned.

The Javadoc also states that "the default implementation calls the generator function with zero and then passes the resulting array to toArray(T[])".

If I provide my own generator function it does get called (as shown by the println() console output), but the return value of its apply() method seems to be ignored. It's as though I had called toArray(String[]::new) regardless of the content of the array returned by my generator function.

Here's the MCVE:

package pkg;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.IntFunction;

public class App {
    public static void main(String[] args) {

        IntFunction<String[]> intFunc = (int sz) -> {
            System.out.println("intFunc: sz: " + sz);
            if (sz == 0) {
                sz = 3;
            }
            String[] array = new String[sz];
            for (int i = 0; i < sz; i++) {
                array[i] = Character.toString('A' + i);

            }
            System.out.println("intFunc: array to be returned: " + Arrays.toString(array));
            return array;
        };

        Collection<String> coll = List.of("This", "is", "a", "list", "of", "strings");

        // Correctly returns the collection as an array, as described in JDK11 Javadoc.
        String[] array1 = coll.toArray(String[]::new);
        System.out.println("array1: " + Arrays.toString(array1) + '\n');

        // Use generator function to return a different collection as an array - doesn't work.      
        String[] array2 = coll.toArray(intFunc);
        System.out.println("array2: " + Arrays.toString(array2) + '\n');

        // Use generator function to return a different collection as an array - doesn't work.
        String[] array3 = coll.toArray(intFunc.apply(coll.size()-2));
        System.out.println("array3: " + Arrays.toString(array3));
    }
}

Here's the console output produced by running the MCVE:

array1: [This, is, a, list, of, strings]

intFunc: sz: 0

intFunc: array to be returned: [A, B, C]

array2: [This, is, a, list, of, strings]

intFunc: sz: 4

intFunc: array to be returned: [A, B, C, D]

array3: [This, is, a, list, of, strings]

The output shows that it doesn't matter what my generator function does - the array it returns is not used.

My question is how do I get this new implementation of toArray() to use the array returned by my generator function, or am I attempting something that is not possible?


Update based on comments and the answer from Nicolai:

The problem with my sample code was not with the generator, but with my test cases. They happened to cause the generator to return an array with fewer elements than the collection, so a new array was allocated instead, to hold exactly the number of elements in the collection.

A test case that returns an array larger than the collection works as expected. For example this code:

    String[] array4 = coll.toArray(intFunc.apply(coll.size() + 3));
    System.out.println("array4: " + Arrays.toString(array4));

gives the following console output:

intFunc: sz: 9

intFunc: array to be returned: [A, B, C, D, E, F, G, H, I]

array4: [This, is, a, list, of, strings, null, H, I]

The SO question Collections emptyList/singleton/singletonList/List/Set toArray explains why there is a null value within the returned array.


Solution

  • As you pointed out, toArray(IntFunction<T[]>) is a default method that simply forwards to toArray(T[]) (after creating an array with the given function). If you take a closer look at that method, you will find the answer to your question - from the JDK 10 Javadoc (emphasis mine):

    Returns an array containing all of the elements in this collection; the runtime type of the returned array is that of the specified array. If the collection fits in the specified array, it is returned therein. Otherwise, a new array is allocated with the runtime type of the specified array and the size of this collection.

    For the array you create to be used, it must be long enough to hold the collection's elements, e.g.:

    public static void main(String[] args) {
        var createdArray = new AtomicReference<String[]>();
        var usedArray = List.of("A", "B", "C").toArray(__ -> {
            createdArray.set(new String[5]);
            return createdArray.get();
        });
    
        var message = String.format(
                "%s (length: %d; identical with created array: %s)",
                Arrays.toString(usedArray), usedArray.length, usedArray == createdArray.get());
        System.out.println(message);
    }