【问题标题】:Picking elements of a list until condition is met with Java 8 Lambdas选择列表中的元素,直到满足 Java 8 Lambda 条件
【发布时间】:2015-08-29 20:15:03
【问题描述】:

我正试图改变我的想法以思考功能性方式,最近遇到了一种情况,我需要从列表中挑选元素直到满足条件,但我找不到一种简单自然的方式来实现这一点。显然我还在学习。

假设我有这个清单:

List<String> tokens = Arrays.asList("pick me", "Pick me", "pick Me",
    "PICK ME", "pick me and STOP", "pick me", "pick me and Stop", "pick me");

// In a non lambdas was you would do it like below
List<String> myTokens = new ArrayList<>();
for (String token : tokens) {
    myTokens.add(token);
    if (token.toUpperCase().endsWith("STOP")) {
        break;
    }
}

提前感谢您的意见

注意: 在发布此之前,我阅读了Limit a stream by a predicate,但我看不出如何使该答案适应我的问题。任何帮助将不胜感激。

【问题讨论】:

  • 我在发表我的问题之前已经阅读过这个问题,但我认为并且仍然认为 java 会开箱即用地实现这一目标。说类似myTokens = tokens.stream().collect(toListWhile(...)) 或实现toListWhile() 自定义收集器的简单方法
  • 我认为 Streams API 中没有一个操作会假设一个有序的流,而对于一个无序的流则失败。这就是你想象中的操作。
  • @AnidMonsur,这不是完全重复的,因为这里 OP 也想采用 STOP 元素。这是一个有点不同的问题。
  • @MarkoTopolnik, takeWhile 实际上是这样的。无序流的JavaDoc statement 特别棒!任何子集,原文如此!在我看来,如果它抛出异常实际上会更好......
  • @tagir 啊,所以他们把它加回来了——如果这些是在 Java 8 发布之前计划好的,但后来又放弃了。与 GIGO 相比,Java 通常具有快速失败的语义。这肯定会产生很多傻眼的 SO 问题。

标签: java lambda java-8 java-stream


【解决方案1】:

如果您确实必须使用 Streams API,请保持简单并使用索引流:

int lastIdx = IntStream.range(0, tokens.size())
        .filter(i -> tokens.get(i).toUpperCase().endsWith("STOP"))
        .findFirst()
        .orElse(-1);

List<String> myTokens = tokens.subList(0, lastIdx + 1);

如果您想要一个不受原始列表支持的独立副本,或者从子列表中创建一个新的List

【讨论】:

  • 一个非常干净和不错的解决方案
【解决方案2】:

在 JDK9 中将有一个名为 takeWhile 的新 Stream 操作,它的功能与您需要的类似。我将此操作反向移植到我的 StreamEx 库中,因此您甚至可以在 Java-8 中使用它:

List<String> list = StreamEx.of(tokens)
                            .takeWhile(t -> !t.toUpperCase().endsWith("STOP"))
                            .toList();

很遗憾它本身不带"STOP"元素,所以第二遍需要手动添加:

list.add(StreamEx.of(tokens).findFirst(t -> t.toUpperCase().endsWith("STOP")).get());

请注意,takeWhilefindFirst 都是短路操作(如果不需要,它们不会处理整个输入流),因此您可以将它们用于非常长甚至无限的流。

但是,使用 StreamEx,您可以使用 groupRuns 的技巧一次性解决它。 groupRuns 方法根据提供的谓词将相邻的 Stream 元素分组到 List,该谓词告诉两个给定的相邻元素是否应该分组。我们可以认为该组以包含"STOP" 的元素结束。那么我们只需要拿第一组:

List<String> list = StreamEx.of(tokens)
                            .groupRuns((a, b) -> !a.toUpperCase().endsWith("STOP"))
                            .findFirst().get();

当第一组完成时,此解决方案也不会做额外的工作。

【讨论】:

  • @Julian,Stream API 的开发是为了支持顺序和并行处理,但 takeWhile 操作本质上是顺序的。正确并行化它并不容易。可能这就是它没有包含在 JDK 8 中的原因。
  • @TagirValeev 可以并行化,但可能涉及大量缓冲。我们在 Java 8 中主要是没时间做这件事。
  • @StuartMarks,是的,我研究了实现。然而,对于有序流并行化很少会提高性能(除非条件在流结束附近失败或始终为真)。对于无序并行流,此操作不太可能有用。我无法想象任何“任何子集”都会得到令人满意的结果的场景。毕竟你可以使用filter,它也会返回一些子集。我的 backport 实现很蹩脚AbstractSpliterator 子类默认trySplit:我认为特殊的并行处理是浪费时间。
  • @TagirValeev Unordered takeWhile 对于我们经常看到的传统的基于集合的问题来说确实几乎没有用处。如果谓词基于外部状态(例如在固定的时间内处理事件流。
  • @StuartMarks,好点子!我从未想过以这种方式使用 Stream API。将这样的示例添加到takeWhileJavaDoc 可能会很好...
【解决方案3】:

仅使用 Java 8 API:

public static <R> Stream<? extends R> takeUntil(Iterator<R> iterator, Predicate<? super R> stopFilter) {
    final boolean isParallelStream = false;
    
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(new Iterator<R>() {
        private R next = null;
        private boolean conditionSatisfied = false;
        private boolean hasTaken = true;
        
        @Override
        public boolean hasNext() {
            if (conditionSatisfied || !iterator.hasNext()) {
                return false;
            }

            if (hasTaken) {
                next = iterator.next();
                conditionSatisfied = stopFilter.test(next);
                hasTaken = false;
            }
            return !conditionSatisfied;
        }

        @Override
        public R next() {
            if (!hasNext()) {
                throw new NoSuchElementException("There are no more items to consume");
            }
            hasTaken = true;
            return next;
        }
    }, 0), isParallelStream);
}

然后您可以通过以下方式对其进行专门化:

对于流

public static <R> Stream<? extends R> takeUntil(Stream<R> stream, Predicate<? super R> stopFilter) {
    return takeUntil(stream.iterator(), stopFilter);
}

对于收藏

public static <R> Stream<? extends R> takeUntil(Collection<R> col, Predicate<? super R> stopFilter) {
    return takeUntil(col.iterator(), stopFilter);
}

【讨论】:

  • 很高兴,有人分享了有关 Streams API 的深入知识。
  • 也许是个愚蠢的问题:但是当您想将它与其他操作链接时如何使用它?
  • @CharybdeBE 还喜欢什么其他操作?
  • 我想要实现的是List&lt;string&gt; x = somestream.filter(...).takeUntil(....).collect(....)
  • @CharybdeBE 这样做:List&lt;String&gt; x = takeUntil(somestream.filter(...), pred).collect(...)
【解决方案4】:

一个选项使用需要两个函数的收集器,一个将字符串添加到列表中,另一个将先前可能并行创建的列表组合在一起。仅当先前的部分输出不以以 STOP 结尾的元素结尾时,它才添加字符串或整个列表:

tokens.stream().collect(() -> new ArrayList<String>(), (l, e) -> {
    if(l.isEmpty() || !l.get(l.size()-1).toUpperCase().endsWith("STOP"))
        l.add(e);
}, (l1, l2) -> {
    if(l1.isEmpty() || !l1.get(l1.size()-1).toUpperCase().endsWith("STOP"))
        l1.addAll(l2);
});

【讨论】:

  • 请注意,此解决方案不是短路的:您仍然需要遍历整个输入流,忽略"STOP" 之后的所有内容。如果您有很长(或无限)的流,这可能会出现问题。
  • 刚刚意识到这个解决方案实际上会非常慢,即使输入不是很长,因为它会在处理每个新元素时复制整个列表。如果您喜欢非短路解决方案,最好改为实现Collector
  • 我绝对不会在这里接受 O(n2) 解决方案。
  • @WillShackleford,收集器执行可变归约,因此它可以修改现有的累加器。您不必复制任何内容,只需检查当前累加器并在必要时添加新元素。 update 中类似链表的解决方案肯定是过度设计的。
  • @WillShackleford, here's 基于收集的解决方案。我也不喜欢它,因为它不是短路的,但它至少比你的和 O(n) 短。如果您愿意,可以使用它作为您的答案:-)
【解决方案5】:

尽管上述答案完全有效,但它们需要在处理元素之前收集和/或预取元素(如果 Stream 是很长)。

对于我的needs,因此我将Louis's answer 改编为 Julian 指出的问题,并对其进行了改编以保留停止/中断项目。见keepBreak参数::

public static <T> Spliterator<T> takeWhile(final Spliterator<T> splitr, final Predicate<? super T> predicate, final boolean keepBreak) {
    return new Spliterators.AbstractSpliterator<T>(splitr.estimateSize(), 0) {
        boolean stillGoing = true;

        @Override
        public boolean tryAdvance(final Consumer<? super T> consumer) {
            if (stillGoing) {
                final boolean hadNext = splitr.tryAdvance(elem -> {
                    if (predicate.test(elem)) {
                        consumer.accept(elem);
                    } else {
                        if (keepBreak) {
                            consumer.accept(elem);
                        }
                        stillGoing = false;
                    }
                });
                return hadNext && (stillGoing || keepBreak);
            }
            return false;
        }
    };
}

public static <T> Stream<T> takeWhile(final Stream<T> stream, final Predicate<? super T> predicate, final boolean keepBreak) {
    return StreamSupport.stream(takeWhile(stream.spliterator(), predicate, keepBreak), false);
}

用法:

public List<String> values = Arrays.asList("some", "words", "before", "BREAK", "AFTER");

    @Test
    public void testStopAfter() {
        Stream<String> stream = values.stream();
        //how to filter stream to stop at the first BREAK
        stream = stream.filter(makeUntil(s -> "BREAK".equals(s)));
        final List<String> actual = stream.collect(Collectors.toList());

        final List<String> expected = Arrays.asList("some", "words", "before", "BREAK");
        assertEquals(expected, actual);
    }

免责声明:我不能 100% 确定这将适用于并行(新流肯定不是并行)或非顺序流。如果您对此有一些提示,请发表评论/编辑。

【讨论】:

  • 问题是关于takeUntil 而不是takeWhile。他们是完全相反的
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多