Filling a Multidimensional Array using a Stream

10,435

Solution 1

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.

Solution 2

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.

Solution 3

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.

Share:
10,435
Angelo Alvisi
Author by

Angelo Alvisi

Self taught Java developer. Bachelor in Sociology. Undergoing Major in Enterprise Marketing and Communication. I've been fiddling with computers since 1991, in 2011 I started learning Java on my own to develop Computer games.

Updated on July 28, 2022

Comments

  • Angelo Alvisi
    Angelo Alvisi over 1 year

    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)?

  • Angelo Alvisi
    Angelo Alvisi over 9 years
    Actually this seems a bit more convolute than the final solution I found myself, I should test which one is faster though.
  • Lukasz Wiktor
    Lukasz Wiktor over 9 years
    Right - it's less readable than other solutions but it has the advantage that it doesn't modify an outside variable what is generally very important when you want to use parallel streams - think of race conditions.
  • Lukasz Wiktor
    Lukasz Wiktor over 9 years
    You can read more about that in the java.util.stream package documentation (Non-interference, Stateless behaviors and Side-effects sections).
  • Angelo Alvisi
    Angelo Alvisi over 9 years
    I'm trying to understand what I'm missing here. Isn't Arrays.setAll simply a shortcut for .mapToObj(...).toArray(...).toArray(...)?
  • Lukasz Wiktor
    Lukasz Wiktor over 9 years
    @AngeloAlvisi No, Arrays.setAll is iterating over the passed array and setting values produced by given function. Look at the source code: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/shar‌​e/…
  • Angelo Alvisi
    Angelo Alvisi over 9 years
    After some testing (benchmarks made with JMH) for the non parallel version the Arrays.setAll is slightly faster (about 5%). The parallel version of .mapToObj is MUCH faster though (about 33%), probably due to the need of changing .forEach to .forEachOrdered