将for循环转换为Java 8流

5

我在尝试使用Java 8进行开发。我尝试将这个for循环转换为Java 8流操作时遇到了一些困难。

for (int y = 0; y < 5; y ++) {
    for (int x = y; x < 10; x += 2) {
        System.out.println(x+y);
    }
}

Please help!


欢迎来到Stackoverflow!在这个网站上,我们喜欢互相帮助,但前提是你已经尝试过一些事情(并告诉我们了)并进行了一些先前的研究。 - erdekhayser
感谢您的评论,erdekhayser。是的,在我来这里询问之前,我确实做了一些研究。外部for循环很容易。IntStream.range(0, 5).forEach()就可以了。但是步长为2的内部循环让我感到困惑...... - user3575416
4个回答

12

将嵌套循环转换的规范方式是在流上使用flatMap,例如:

IntStream.range(0, 5).flatMap(i->IntStream.range(i, 10))
  .forEach(System.out::println);

对于您的任务而言,最棘手的部分是增量加2,因为在流API中没有直接的等价物。有两种可能性:

  1. 使用 IntStream.iterate(y, x->x+2) 定义起始值和增量。然后通过限制元素数量来修改无限流:.limit((11-y)/2)

    因此,您的循环的最终代码如下:

  2. IntStream.range(0, 5)
        .flatMap(y->IntStream.iterate(y, x->x+2).limit((11-y)/2)
        .map(x -> x+y)).forEach(System.out::println);
    
    使用IntStream.range(0, (11-y)/2)创建一个升序的int流,并使用.map(t->y+t*2)修改它以产生所需的内部for循环的值。 然后,你的循环的结果代码将如下所示:
    IntStream.range(0, 5)
        .flatMap(y->IntStream.range(0, (11-y)/2).map(t->y+t*2).map(x -> x+y))
        .forEach(System.out::println);
    

你也可以使用 IntStream.range(a, b),然后使用 filter(n -> n % 2 == 0) 过滤掉奇数,从而生成一个 +2 变体。 - skiwi

7
正如Holger在此处指出,这当然是可以做到的。但问题是:为什么要这样做呢?
我怀疑这是一个XY问题,流并不是解决所有问题的方法,这是一个重要的事实需要记住。
看看你的代码正在做什么,并创建一个尽可能具有可变性的方法来完成你想要做的事情,你需要:
  • 拥有一个 y 的最大值(考虑到你总是从0开始)
  • 拥有一个 x 的最大值(考虑到你总是从0开始)
  • 拥有一个 y 的增量操作。(假设+1没有固定)
  • 拥有一个 x 的增量操作。
  • 然后进行 xy 的操作。
  • 然后你消耗操作的结果。
因此,我提出以下建议,如果这不是你想要的,可变性可能会减少,换句话说:如果有必要,您可以更加硬编码。
private void doMyDoubleLoop(
        final int yEnd, final IntUnaryOperator yIncrementOperator, 
        final int xEnd, final IntUnaryOperator xIncrementOperator,
        final IntBinaryOperator combiner, final IntConsumer consumer
) {
    for (int y = 0; y < yEnd; y = yIncrementOperator.applyAsInt(y)) {
        for (int x = y; x < xEnd; x = xIncrementOperator.applyAsInt(x)) {
            consumer.accept(combiner.applyAsInt(x, y));
        }
    }
}

用途:

doMyDoubleLoop(5, y -> y + 1, 10, x -> x + 2, (x, y) -> x + y, System.out::println);

就像我所说的,这可能有点过度,所以假设所有与for循环相关的问题都已经解决,那么它会突然变得更加美好:

private void doMyInternalDoubleLoop(final IntBinaryOperator combiner, final IntConsumer consumer) {
    for (int y = 0; y < 5; y++) {
        for (int x = y; x < 10; x += 2) {
            consumer.accept(combiner.applyAsInt(x, y));
        }
    }
}

用途:

doMyInternalDoubleLoop((x, y) -> x + y, System.out::println);

如果您有一个类,其中有很多使用这个双循环的操作,但不想复制循环,那么我建议使用这种模式,因为这符合DRY(不要重复自己)原则。


重点是流可以轻松并行化;而for循环则不行。也许在这个简单的例子中,这样做还可以。然而,一旦我们处理更大的数据集,不要被认为你可以使用for循环只是因为它看起来更简单。关于流的另一个有用的事实是,它们并不总是需要大量的内存来操作。 - YoYo
2
只要“像循环一样的流”编码起来如此困难,从维护的角度来看,最好避免使用它们并使用常规循环,正如我所指出的那样。不要让事情变得比必要的更加复杂。 - skiwi
如果你有一个运行了10个小时的工作,而你知道你有机会将其缩短到1个小时,那么你还会坚持使用循环吗?记住,许多人为了发布问题而简化他们的示例。我不认为有人会使用流来构建一个带有输入字段的5x6矩阵 - 我的意思是出于你提到的同样的原因,我希望不会这样做。 - YoYo

4
我会使用内部 IntStream 上的filter方法来完成此操作:
IntStream
.range(0, 5)
.flatMap(
        y ->
        IntStream
        .range(y, 10)
        .filter(x -> (x-y) % 2 == 0)
        .map(x -> x+y))
.forEach(System.out::println);

我一直在使用这种分段数据的模式:例如。
IntStream
.range(first, last)
.filter(index -> (index-first) % segment == 0)
.forEach(
        System.out.printf(
                "Doing segment %s to %s%n", 
                index, 
                Math.min(index+seg-1, last)));

1

我还在努力理解这个问题。以下代码不太优美:

IntStream.range(0, 5).forEach(y -> 
//<kludge> 
//I would appreciate it if someone would replace this with something smarter.
    IntStream.iterate(y, x -> x + 2)
        .limit(100)
        .filter(x -> x < 10)
//</kludge>
        .forEach( x -> System.out.println(y+x)));

“x += 2”是复杂的部分,如果range方法有一个“增量”参数就会变得简单。我使用“iterate”来自行增加值,但“iterate”会产生一个无限的流。我用“filter”将其限制在所需范围内,并放置了一个任意的“limit”,以便它不会溢出并开始输出小于10的大负数。


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