Reputation: 479
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
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
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
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
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