Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 395
skomisa Avatar asked Aug 28 '18 06:08

skomisa


People also ask

What is the use of toArray () in Java?

The toArray() method of ArrayList is used to return an array containing all the elements in ArrayList in the correct order.

What does the toArray () method do when called on a list?

toArray. Returns an array containing all of the elements in this list in proper sequence (from first to last element); the runtime type of the returned array is that of the specified array. If the list fits in the specified array, it is returned therein.


2 Answers

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);
}
like image 63
Nicolai Parlog Avatar answered Nov 14 '22 07:11

Nicolai Parlog


The array returned by the generator function isn't being ignored. The component type of the returned array is used. Suppose you have a collection of strings, as in your example:

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

And your generator function is this:

jshell> IntFunction<CharSequence[]> g = x -> {
   ...>   var a = new CharSequence[x];
   ...>   System.out.println(a);
   ...>   return a;
   ...> }
g ==> $Lambda$28/0x00000008000d8040@17d677df

(Note that this prints out the default representation of the array, which includes its identity hash code, not the contents of the array. The array's contents are as expected.)

Then you can copy the strings from the collection into an array of CharSequence:

jshell> System.out.println(coll.toArray(g))
[Ljava.lang.CharSequence;@7d70d1b1
[Ljava.lang.CharSequence;@2a742aa2

Now suppose the source is an empty collection:

jshell> System.out.println(List.of().toArray(g))
[Ljava.lang.CharSequence;@3dfc5fb8
[Ljava.lang.CharSequence;@3dfc5fb8

You can see that the zero-size array returned by the generator is of sufficient size to contain zero elements, so it's simply returned.

like image 21
Stuart Marks Avatar answered Nov 14 '22 06:11

Stuart Marks