I'm new to Java 8's Lambda Expressions and I want to formulate the following: I have a 2-dimensional array which I want to iterate over several times in my application code and do stuff with the items in the array. Before i'd do the following:
public static abstract class BlaBlaIterator {
private final BlaBla[][] blabla;
public BlaBlaIterator(final BlaBla[][] blabla) {
this.blabla = blabla;
}
public void iterate() {
final int size = blabla.length;
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
final BlaBla bla = blabla[x][y];
iterateAction(x, y, bla, bla == null);
}
}
}
public abstract void iterateAction(int x, int y, BlaBla bla, boolean isNull);
}
and then
BlaBla[][] blabla = ...
new BlaBlaIterator(blabla) {
@Override
public void iterateAction(final int x, final int y, final BlaBla bla, final boolean isNull) {
//...
}
}.iterate();
Crucial thing: I need access to the current x/y and I need to get calculated things like the isNull.
What I want to do now is to convert this to lambda. I want to write something like this:
BlaBla[] blabla = ...
blabla.stream().forEach((x, y, blabla, isNull) -> ... );
To get a stream from the 2-dimensional Array I can do
Arrays.stream(field).flatMap(x -> Arrays.stream(x))
But then I loose the x/y info and cannot pass calculated stuff like isNull. How can i do this?
To be honest I would keep the traditionnal nested loop, IMO this is a much cleaner approach. Streams are not a substition for all the "old" Java code. Nevertheless, I posted some possible approaches.
Here's a first possible approach (Object-oriented). Create a class ArrayElement
to hold the indices:
class ArrayElement<V> {
public final int row;
public final int col;
public final V elem;
...
}
Then you'll need to create a method that creates a Stream of elements from a single array (the one that we will call for flatMap
), and iterateAction
just print out the current instance
private static <T> Stream<ArrayElement<T>> createStream(int row, T[] arr) {
OfInt columns = IntStream.range(0, arr.length).iterator();
return Arrays.stream(arr).map(elem -> new ArrayElement<>(row, columns.nextInt(), elem));
}
private static <V> void iterateAction(ArrayElement<V> elem) {
System.out.println(elem);
}
Finally the main looks like this:
String[][] arr = {{"One", "Two"}, {"Three", "Four"}};
OfInt rows = IntStream.range(0, arr.length).iterator();
Arrays.stream(arr)
.flatMap(subArr -> createStream(rows.nextInt(), subArr))
.forEach(Main::iterateAction);
and outputs:
ArrayElement [row=0, col=0, elem=One]
ArrayElement [row=0, col=1, elem=Two]
ArrayElement [row=1, col=0, elem=Three]
ArrayElement [row=1, col=1, elem=Four]
This solution has the disadvantage that it creates a new Object for each Object in the array.
The second approach is more direct, it's the same idea but you don't create a new ArrayElement instance for each elem in the array. Again this could be done in a one liner but the lamdba would become ugly so I splitted those up in methods (like in the first approach):
public class Main {
public static void main(String[] args) {
String[][] arr = { {"One", "Two"}, {null, "Four"}};
OfInt rows = IntStream.range(0, arr.length).iterator();
Arrays.stream(arr).forEach(subArr -> iterate(subArr, rows.nextInt()));
}
static <T> void iterate(T[] arr, int row) {
OfInt columns = IntStream.range(0, arr.length).iterator();
Arrays.stream(arr).forEach(elem -> iterateAction(row, columns.nextInt(), elem, elem == null));
}
static <T> void iterateAction(int x, int y, T elem, boolean isNull) {
System.out.println(x+", "+y+", "+elem+", "+isNull);
}
}
and it outputs:
0, 0, One, false
0, 1, Two, false
1, 0, null, true
1, 1, Four, false
Using two instances of AtomicInteger
String[][] arr = {{"One", "Two"}, {null, "Four"}};
AtomicInteger rows = new AtomicInteger();
Arrays.stream(arr).forEach(subArr -> {
int row = rows.getAndIncrement();
AtomicInteger colums = new AtomicInteger();
Arrays.stream(subArr).forEach(e -> iterateAction(row, colums.getAndIncrement(), e, e == null));
});
which produces the same output as above.
It's duable using Streams but I really prefer the nested loop in your use-case since you need both the x and y values.
This is an issue, similar to the different forms of for
-loop. If you are not interested in the indices, you can imply say:
for(BlaBla[] array: blabla) for(BlaBla element: array) action(element);
But if you are interested in the indices, you can’t use the for-each loop but have to iterate over the indices and get the array element in the loop body. Similarly, you have to stream the indices when using Stream
and need the indices:
IntStream.range(0, blabla.length)
.forEach(x -> IntStream.range(0, blabla[x].length)
.forEach(y -> {
final BlaBla bla = blabla[x][y];
iterateAction(x, y, bla, bla == null);
})
);
This is a 1:1 translation which has the advantage of not requiring additional classes, but it consists of two distinct Stream
operations rather than one fused operation, as would be preferred.
A single, fused operation might look like this:
helped class:
class ArrayElement {
final int x, y;
BlaBla element;
final boolean isNull;
ArrayElement(int x, int y, BlaBla obj) {
this.x=x; this.y=y;
element=obj; isNull=obj==null;
}
}
actual operation:
IntStream.range(0, blabla.length).boxed()
.flatMap(x -> IntStream.range(0, blabla[x].length)
.mapToObj(y->new ArrayElement(x, y, blabla[x][y])))
.forEach(e -> iterateAction(e.x, e.y, e.element, e.isNull));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With