使用流重写复杂的Java 8 for循环

3

是否可能使用Java 8流重写像这样复杂的for循环?任何我想出来的都似乎更加臃肿,比起下面使用普通for循环留下的代码。

public static  boolean isBalanced(String text) {
    int count = 0;
    for(int i = 0; i < text.length(); i++ ) {
        if (text.charAt(i) == ')') {
            count--;
        } else if (text.charAt(i) == '(') {
            count++;
        }
        if (count < 0) {
            return false;
        }
    }
    return count == 0;
}


使用流

public static boolean isBalanced2(String text) {
    AtomicInteger count = new AtomicInteger(0);

    text.chars()
        .forEachOrdered(x -> {
             if (x == ')') {
                 count.getAndDecrement();
             } else if (x == '(') {
                 count.getAndIncrement();
             }
        });

    return count.get() == 0;
}

它的工作正常,但是它会遍历整个字符串,有时这可能是浪费计算资源的,例如在字符串 ")......" 的情况下。

当计数小于0时似乎无法立即退出流?(我不想抛出异常!)

谢谢


我会使用 for (char c : test.toCharArray()) {(或者等价的代码点). - Tom Hawtin - tackline
你可以给人们一些时间来回复... - Pat
public static boolean isBalanced2(String text) { AtomicInteger count = new AtomicInteger(0); text.chars(). forEachOrdered(x -> { if (x == ')') { count.getAndDecrement(); } else if (x == '(') { count.getAndIncrement(); } }); return count.get() == 0; }它的功能很好,只是它遍历整个字符串,有时候可能会浪费计算资源,例如在字符串“)......”的情况下。 - Pat
1
我建议解释使用情况,也就是说你需要什么?为什么要返回 if (count < 0)return count == 0; 更好的做法是解释你想要什么。@Pat - Ryuzaki L
这篇文章中的“Stream”方法对于像“)(”这样的“String”会失败,不是为了性能调整而是为了算法本身。括号的顺序很重要。 - Dorian Gray
6个回答

2
首先,我想提到涉及副作用的代码通常与流不兼容,仅因此原因,我建议采用命令式方法进行操作:
1. 代码短路 2. 可以按照编写方式阅读
至于:
“它可以正常工作,但它遍历整个字符串,有时可能会浪费计算时间,例如在字符串的情况下。”
“任何试图短路您展示的流解决方案的尝试都将涉及副作用,一般情况下是不鼓励的。”
“行为参数中的副作用通常是不鼓励的,因为它们经常会导致无意识的违反无状态要求以及其他线程安全隐患。如果行为参数具有副作用,则除非明确说明,否则不能保证这些副作用对其他线程的可见性,也不能保证在同一流管道中对“相同”元素执行的不同操作在同一线程中执行。”
结论是,流并不总是解决所有问题的答案,而是针对特定情况,而这种情况显然不适合使用流。

我同意这不是一个适合流式处理的任务,但是可以在不使用副作用的情况下完成,而且不需要短路。 - Andreas
@Andreas 确定,但是我说过,“任何试图绕过你展示的流解决方案的尝试都会涉及副作用,一般情况下,这是不被鼓励的。” - Ousmane D.
True,但是短路只会在不平衡的“)”上发生,而不是在不平衡的“(”上发生,因此短路更像是一个良好的加速方式,而不是必需品。因此,如果结果正确,则可以接受不进行短路的流解决方案。 - Andreas
@Andreas 对,我只是试图回答帖子中的“它可以正常工作,但有时候在字符串的情况下可能会浪费计算资源,因为它遍历整个字符串”的部分,这听起来像是他们仍然想要保持短路,但是我同意你的观点,“一个不短路的流解决方案可能是可接受的,只要结果是正确的。” - Ousmane D.

2

你不应该这样做。

Lambda和Stream不能替代所有复杂的for循环。虽然你可以像你所做的那样使用Stream,但这并不意味着它更易于理解(哪个更容易理解?)和更高效(你肯定会因为AtomicInteger与基于int的操作而失去一些东西,但你可能可以使用int[]数组来代替)。

  • 你不能尽早退出循环,除非你使用异常,但你可以缩小测试范围(并且你应该对其进行测试)。你可能考虑在map操作后使用filter,但这并不会使其更易于阅读。
  • 你应该坚持纯函数,例如:你应该不会产生副作用(对AtomicInteger)。

2
以下代码可以实现您想要的功能,并且比原始代码更小,但它很复杂并且总是处理所有字符,即如果检测到不平衡的),它不会提前停止。
然而,与这里的其他答案不同,它不通过在流外部维护状态来违反流规则。
private static boolean isBalanced(String text) {
    return 0 == text.chars()
            .reduce(0, (n, c) -> n < 0 ? n : c == '(' ? n + 1 : c == ')' ? n - 1 : n);
}

逻辑如下:
  • 保持一个表示嵌套级别的累加器,即当找到 ( 时将其值加一,当找到 ) 时将其值减一。

  • 如果总数低于0,则停止更新它,即当发现不平衡的 ) 时,将最终总数保持在-1。

reduce 操作的结果为:
  • 0:所有的 ( 都有匹配的 )

  • -1:发现不平衡的 )

  • >0:发现不平衡的 (

使用 if 语句而不是条件三元运算符的同样代码的长版本。
private static boolean isBalanced(String text) {
    int finalLevel = text.chars().reduce(0, (lvl, ch) -> {
        if (lvl < 0)
            return lvl; // Keep result of -1 for unbalanced ')'
        if (ch == '(')
            return lvl + 1;
        if (ch == ')')
            return lvl - 1;
        return lvl;
    });
    return (finalLevel == 0);
}

1
请注意,这里描述的缩减函数不是可结合的。本答案中的场景可以工作,但它依赖于顺序的从左到右执行。如果缩减函数被修改或重新用途,特别是在并行运行时,它将无法给出正确的结果。 - Stuart Marks

2

这里有一个使用Java 8的类似解决方案。

首先将'('')'和其他字符映射为1-10。然后计算累积总和,并检查每个部分总和ps >= 0以及最终总和s == 0。通过在部分总和检查中使用allMatch,该过程是短路的。

public static boolean isBalanced(String text) {
    AtomicInteger s = new AtomicInteger();
    return text.chars()
            .map(ch -> (ch == '(') ? 1 : (ch == ')') ? -1 : 0)
            .map(s::addAndGet)
            .allMatch(ps -> ps >= 0) && s.get() == 0;
}

这里有一个支持多种不同括号的解决方案(需要一些IntStack实现):

IntStack stack = ...;
return text.chars()
        .map("(){}[]"::indexOf)
        .filter(i -> i >= 0)
        .allMatch(i -> {
            int form = i / 2; // 0 = (), 1 = {}, 2 = []
            int side = i % 2; // 0 = left, 1 = right
            if (side == 0) {
                stack.push(form);
                return true;
            } else {
                return stack.size() != 0 && stack.pop() == form;
            }
        }) && stack.size() == 0;

这是我能看到的最好的答案。OP,请接受这个答案。 - Kunal Puri
2
@KunalPuri:把流强行塞进一个不易于维护的目的中,不能让它们利用流的主要优势,并且在长期维护方面会更加困难。我强烈不同意你认为这是“最佳”答案的想法。 - Makoto
@Makoto 通过最佳答案,我想说这是使用流的最佳答案。我完全同意你的看法。 - Kunal Puri
@Makoto 还有一件事。我觉得即使是基于计数的方法也不太易于维护。当出现不同的可能括号,如花括号、方括号等时,解决方案将会很困难。相反,我认为应该使用堆栈和映射来完成这个任务。 - Kunal Puri
1
@KunalPuri 添加了另一种解决方案,使用栈来支持不同的括号。 - Bubletan
@Bubletan 我只能给一次+1 :) 对我来说,这是使用流的最佳答案。 - Kunal Puri

0

流(Streams)确实有提前终止的概念,但只有在终端操作实际支持它时才会生效。

从您所描述的操作来看,forEachOrdered将遍历流中的每个元素,并且它没有中途停止的能力。请记住:流可能是无限的,因此在对每个元素进行排序时提前终止流可能被视为运行时错误。

实际上,我建议您坚持使用循环变量而不是流变量,因为循环变量使您能够提前终止。鉴于流变量必须处理的约束条件,您编写的流变量实际上是合理的。


谢谢您的回复。您回答了我的问题 - 所以有时候我们最好只使用普通循环,而不是强迫自己使用流。 - Pat

0

您可以通过支持此功能的终端操作之一来提前终止流评估。这些操作实际上相对较少,但如果您愿意容忍一些小的滥用,并且您正在使用Java 9或更高版本,则可以使用takeWhile()在几乎所有情况下执行提前终止。诀窍(也是滥用)是使用具有状态保留功能的谓词。例如:

public static boolean isBalanced(String text) {
    final int[] state = new int[0];

    text.chars().takeWhile(c -> {
        if (c == '(') state[0]++; if (c == ')') state[0]--; return state[0] >= 0; 
    });

    return state[0] == 0;
}

这非常类似于您原始的循环。


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