Java Lambda:迭代二维数组并保持当前索引

4

我是Java 8的Lambda表达式新手,我想阐述以下内容: 我有一个二维数组,我想在我的应用程序代码中多次迭代该数组并对其中的项目进行处理。在使用Lambda表达式之前,我会执行以下操作:

    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);
        }

然后

    BlaBla[][] blabla = ...

    new BlaBlaIterator(blabla) {

        @Override
        public void iterateAction(final int x, final int y, final BlaBla bla, final boolean isNull) {
            //...
        }
    }.iterate();

关键是:我需要访问当前的x/y并获取像isNull这样计算出来的东西。

现在我想要做的是将其转换为lambda表达式。我想写出类似于以下内容:

    BlaBla[] blabla = ...
    blabla.stream().forEach((x, y, blabla, isNull) -> ... );

我可以通过以下方式从二维数组中获取流

    Arrays.stream(field).flatMap(x -> Arrays.stream(x))

但是,如果我这样做就会失去x/y信息,无法传递诸如isNull之类的计算结果。我该怎么做呢?

即使有人给出了一个答案:传统形式上的二维数组并不是一个流。请注意,你要求x和y成为最终处理调用的一部分 - 从blabla[0][0]到最后一个元素的序列无法与索引对分离。 - laune
在我看来,这样更易读且易于维护。当你手握一把锤子时,所有东西都看起来像钉子 :)仅仅因为我们有流并不意味着我们应该在所有地方都使用它们 :) - akhil_mittal
2个回答

8
坦白说,我会选择传统的嵌套循环,因为在我看来这种方法更加简洁。流并不能替代所有的“旧式”Java代码。但是,我还是发了一些可能的做法。

第一种方法

以下是一种可能的方法(面向对象)。创建一个类ArrayElement用于保存索引:

class ArrayElement<V> {
    public final int row;
    public final int col;
    public final V elem;
    ...
}

然后,您需要创建一个方法,该方法可以从单个数组创建元素流(我们将为其调用flatMap),并且iterateAction仅打印出当前实例。

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);
}

最终的主函数应该是这样的:
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);

并输出:

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]

这种解决方案的缺点是它为数组中的每个对象创建了一个新的对象。

第二种方法

第二种方法更直接,与第一种方法相同,但您不需要为数组中的每个元素创建新的ArrayElement实例。同样,这可以在一行代码中完成,但是使用Lambda表达式会变得丑陋,因此我将其拆分为方法(就像第一种方法中一样):

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);
    }    
}

然后它会输出:

0, 0, One, false
0, 1, Two, false
1, 0, null, true
1, 1, Four, false

第三种方法

使用两个 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));
});

这将会产生与上述代码相同的输出。

我的结论

使用流(Streams)是可行的,但在您的用例中我更喜欢嵌套循环,因为您需要x和y的值。


2
我完全同意建议只保留现有的嵌套for循环。流在许多情况下非常好用,但并不适用于所有情况。尽管你可以将你的用例强行使用流,但这并不会使代码更易读或更易维护,并且几乎肯定性能更差。 - Brett Okken
谢谢您详细的回答!我会用它们来更熟悉lambda表达式。但是解决方案看起来不像我预期的那么优雅,我的“旧方法”更容易阅读,正如您已经说过的那样。所以最终我会坚持使用嵌套循环。 - bruegae
1
@user2849355 如果你需要索引,嵌套循环显然是最好的选择。当然,如果你只需要处理元素,Arrays.stream(arr).flatMap(Arrays::stream).forEach(System.out::println); 是一个很酷的一行代码方法。但是,虽然Java 8中的新功能非常酷,我们也不应该忘记基础知识 :-) - Alexis C.

1
这是一个问题,类似于不同形式的“for”循环。如果您不关心索引,可以简单地说:
for(BlaBla[] array: blabla) for(BlaBla element: array) action(element);

但是如果您对索引感兴趣,则无法使用for-each循环,而必须在循环体中迭代索引并获取数组元素。同样地,使用Stream并需要索引时,您必须对索引进行流式处理:

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);
             })
         );

这是一种1:1的翻译,它具有不需要额外类的优点,但它由两个不同的Stream操作组成,而不是一个融合的操作,这不是最佳选择。
一个单一的、融合的操作可能看起来像这样:
帮助的类:
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;
    }
}

实际操作:
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));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接