Angelo Alvisi
Angelo Alvisi

Reputation: 479

Filling a Multidimensional Array using a Stream

I'm new to Java 8 and currently failing to grasp Streams fully, is it possible to fill an array using the Stream functional operations? This is an example code of how I would do it with a standard for loop:

public static void testForLoop(){
    String[][] array = new String[3][3];
    for (int x = 0; x < array.length; x++){
        for (int y = 0; y < array[x].length; y++){
            array[x][y] = String.format("%c%c", letter(x), letter(y));
        }
    }               
}

public static char letter(int i){
    return letters.charAt(i);
} 

If it is possible how would I do it using Stream? If it is possible, is it convenient (performance and readability wise)?

Upvotes: 8

Views: 11988

Answers (4)

Lukasz Wiktor
Lukasz Wiktor

Reputation: 20422

Here you have a solution that produces the array instead of modifying a previously defined variable:

String[][] array = 
    IntStream.range(0, 3)
             .mapToObj(x -> IntStream.range(0, 3)
                                     .mapToObj(y -> String.format("%c%c", letter(x), letter(y)))
                                     .toArray(String[]::new))
             .toArray(String[][]::new);

If you want to use parallel streams then it's very important to avoid side effects like modifications of a variable (array or object). It might lead to race conditions or other concurrency issues. You can read more about that in java.util.stream package documentation - see Non-interference, Stateless behaviors and Side-effects sections.

Upvotes: 15

Angelo Alvisi
Angelo Alvisi

Reputation: 479

After working and testing around this is the best option I came with:

IntStream.range(0, array.length).forEach(x -> Arrays.setAll(array[x], y -> builder.build2Dobject(x, y)));

(In the specific case I proposed it would be:

IntStream.range(0, array.length).forEach(x -> Arrays.setAll(array[x], y -> String.format("%c%c", letter(x), letter(y)));

for a 3d array it's simply:

IntStream.range(0, array.length).forEach(x -> IntStream.range(0, array[x].length).forEach(y -> Arrays.setAll(array[x][y], z -> builder.build3Dobject(x, y, z))));

this is the code that lets the program choose the fastest option:

public static void fill2DArray(Object[][] array, Object2DBuilderReturn builder){
    int totalLength = array.length * array[0].length;
    if (totalLength < 200){
        for(int x = 0; x < array.length; x++){
            for (int y = 0; y < array[x].length; y++){
                array[x][y] = builder.build2Dobject(x, y);
            }
        }
    } else if (totalLength >= 200 && totalLength < 1000){
        IntStream.range(0, array.length).forEach(x -> Arrays.setAll(array[x], y -> builder.build2Dobject(x, y))); 
    } else {
        IntStream.range(0, array.length).forEach(x -> Arrays.setAll(array[x], y -> builder.build2Dobject(x, y))); 
    }
}

the functional interface:

@FunctionalInterface
public interface Object2DBuilderReturn<T> {
    public T build2Dobject(int a, int b);
}

Upvotes: 0

Holger
Holger

Reputation: 298153

The best way is a combination of the two approaches of Stuart Marks’ answer.

IntStream.range(0, array.length).forEach(x -> Arrays.setAll(
    array[x], y -> String.format("%c%c", letter(x), letter(y))));

The reasoning leading to the solution is that “filling a multi-dimensional array” in Java means, “iterating over the outer array(s)” followed by “filling a one-dimensional array” as String[][] is just a an array of String[] elements in Java. In order to set their elements you have to iterate over all String[] elements and since you need the index to calculate the final value, you can’t use Arrays.stream(array).forEach(…). So for the outer array iterating over the indices is appropriate.

For the inner arrays the search is for the best solution for modifying an (one-dimensional) array. Here, Arrays.setAll(…,…) is appropriate.

Upvotes: 4

Stuart Marks
Stuart Marks

Reputation: 132380

There are a couple ways to do this.

One way is with a couple nested IntStreams over the row and column indexes:

String[][] testStream() {
    String[][] array = new String[3][3];
    IntStream.range(0, array.length).forEach(x -> 
        IntStream.range(0, array[x].length).forEach(y -> 
            array[x][y] = String.format("%c%c", letter(x), letter(y))));
    return array;
}

Another way which seems promising is to use Array.setAll instead of streams. This is great for generating values for a one-dimensional array: you provide a function that maps from the array index to the value you want assigned in the array. For example, you could do this:

String[] sa = new String[17];
Arrays.setAll(sa, i -> letter(i));

Unfortunately it's less convenient for multidimensional arrays. The setAll method that takes a lambda that returns a value that's assigned to the array location at that index. If you've created a multidimensional array, the higher dimensions are already initialized with lower dimensional arrays. You don't want to assign to them, but you do want the implicit looping behavior of setAll.

With this in mind, you can use setAll to initialize the multidimensional array like this:

static String[][] testArraySetAll() {
    String[][] array = new String[3][3];
    Arrays.setAll(array, x -> {
        Arrays.setAll(array[x], y -> String.format("%c%c", letter(x), letter(y)));
        return array[x];
    });
    return array;
}

The inner setAll is reasonably nice, but the outer one has to have a statement lambda that calls the inner setAll and then returns the current array. Not too pretty.

It's not clear to me that either of these approaches is any better than the typical nested for-loops.

Upvotes: 2

Related Questions