Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java Lambda: Iterate over 2 dim-array keeping the current index

Tags:

java

java-8

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?

like image 873
bruegae Avatar asked Jan 10 '23 04:01

bruegae


2 Answers

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.

First approach

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.

Second approach

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

Third approach

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.

My conclusion

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.

like image 171
Alexis C. Avatar answered Jan 25 '23 23:01

Alexis C.


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));
like image 42
Holger Avatar answered Jan 25 '23 23:01

Holger