沿元素将列表拆分为子列表

2022-08-31 14:17:50

我有这个列表():List<String>

["a", "b", null, "c", null, "d", "e"]

我想要这样的东西:

[["a", "b"], ["c"], ["d", "e"]]

换句话说,我想使用值作为分隔符将我的列表拆分为子列表,以便获得列表列表()。我正在寻找一个Java 8解决方案。我已经尝试过,但我不确定它是否是我正在寻找的。谢谢!nullList<List<String>>Collectors.partitioningBy


答案 1

虽然已经有几个答案,并且有一个被接受的答案,但这个主题仍然缺少几点。首先,共识似乎是,使用流来解决这个问题只是一种练习,传统的for-loop方法更可取。其次,到目前为止给出的答案忽略了使用数组或矢量式技术的方法,我认为这大大改善了流解决方案。

首先,这是一个传统的解决方案,用于讨论和分析:

static List<List<String>> splitConventional(List<String> input) {
    List<List<String>> result = new ArrayList<>();
    int prev = 0;

    for (int cur = 0; cur < input.size(); cur++) {
        if (input.get(cur) == null) {
            result.add(input.subList(prev, cur));
            prev = cur + 1;
        }
    }
    result.add(input.subList(prev, input.size()));

    return result;
}

这基本上是直截了当的,但有一点微妙之处。有一点是,从 到 的挂起子列表始终处于打开状态。当我们遇到时,我们将其关闭,将其添加到结果列表中,然后前进 。循环后,我们无条件关闭子列表。prevcurnullprev

另一个观察结果是,这是对索引的循环,而不是对值本身的循环,因此我们使用算术 for 循环而不是增强的“for-each”循环。但它表明,我们可以使用索引来生成子范围,而不是对值进行流式处理并将逻辑放入收集器中(就像Joop Eggen提出的解决方案所做的那样)。

一旦我们意识到这一点,我们可以看到输入中的每个位置都是子列表的分隔符:它是子列表的右端,它(加一)是子列表的左端。如果我们能够处理边缘情况,则会导致一种方法,在该方法中,我们可以找到元素出现的索引,将它们映射到子列表,并收集子列表。nullnull

生成的代码如下所示:

static List<List<String>> splitStream(List<String> input) {
    int[] indexes = Stream.of(IntStream.of(-1),
                              IntStream.range(0, input.size())
                                       .filter(i -> input.get(i) == null),
                              IntStream.of(input.size()))
                          .flatMapToInt(s -> s)
                          .toArray();

    return IntStream.range(0, indexes.length-1)
                    .mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
                    .collect(toList());
}

获取发生的索引非常容易。绊脚石是在左侧和右侧添加。我选择使用来做附加,然后将它们压平。(我尝试了其他几种方法,但这种方法似乎是最干净的。null-1sizeStream.offlatMapToInt

在这里使用数组作为索引会更方便一些。首先,用于访问数组的表示法比用于 List 的表示法更好:vs. 。其次,使用数组可以避免装箱。indexes[i]indexes.get(i)

此时,数组中的每个索引值(最后一个索引值除外)都比子列表的起始位置小一个。紧挨着右侧的索引是子列表的末尾。我们只需对数组进行流式传输,并将每对索引映射到子列表中并收集输出。

讨论

流方法比 for 循环版本略短,但更密集。for-loop版本很熟悉,因为我们一直在Java中做这些事情,但是如果你还不知道这个循环应该做什么,它就不明显了。在弄清楚正在做什么以及为什么在循环结束后必须关闭打开的子列表之前,您可能必须模拟几个循环执行。(我最初忘记了拥有它,但我在测试中发现了这一点。prev

我认为,流方法更容易概念化正在发生的事情:获取一个列表(或数组),指示子列表之间的边界。这是一个简单的流双线。正如我上面提到的,困难在于找到一种方法将边缘值固定在末端。如果有更好的语法来执行此操作,例如,

    // Java plus pidgin Scala
    int[] indexes =
        [-1] ++ IntStream.range(0, input.size())
                         .filter(i -> input.get(i) == null) ++ [input.size()];

它会让事情变得不那么混乱。(我们真正需要的是数组或列表理解。获得索引后,只需将它们映射到实际的子列表并将其收集到结果列表中即可。

当然,这在并行运行时是安全的。

更新 2016-02-06

下面是创建子列表索引数组的更好方法。它基于相同的原则,但它会调整索引范围并向筛选器添加一些条件,以避免必须连接和平面映射索引。

static List<List<String>> splitStream(List<String> input) {
    int sz = input.size();
    int[] indexes =
        IntStream.rangeClosed(-1, sz)
                 .filter(i -> i == -1 || i == sz || input.get(i) == null)
                 .toArray();

    return IntStream.range(0, indexes.length-1)
                    .mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1]))
                    .collect(toList());
}

更新 2016-11-23

我与Brian Goetz在Devoxx Antwerp 2016上共同举办了一场演讲,“并行思考”(视频),介绍了这个问题和我的解决方案。提出的问题是轻微的变化,在“#”而不是null上分裂,但它是相同的。在演讲中,我提到我有一堆针对这个问题的单元测试。我将它们作为独立程序附加到下面,以及我的循环和流实现。对于读者来说,一个有趣的练习是针对我在这里提供的测试用例运行其他答案中提出的解决方案,并查看哪些失败以及原因。(其他解决方案必须适应基于谓词的拆分,而不是基于 null 进行拆分。

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

import static java.util.Arrays.asList;

public class ListSplitting {
    static final Map<List<String>, List<List<String>>> TESTCASES = new LinkedHashMap<>();
    static {
        TESTCASES.put(asList(),
                  asList(asList()));
        TESTCASES.put(asList("a", "b", "c"),
                  asList(asList("a", "b", "c")));
        TESTCASES.put(asList("a", "b", "#", "c", "#", "d", "e"),
                  asList(asList("a", "b"), asList("c"), asList("d", "e")));
        TESTCASES.put(asList("#"),
                  asList(asList(), asList()));
        TESTCASES.put(asList("#", "a", "b"),
                  asList(asList(), asList("a", "b")));
        TESTCASES.put(asList("a", "b", "#"),
                  asList(asList("a", "b"), asList()));
        TESTCASES.put(asList("#"),
                  asList(asList(), asList()));
        TESTCASES.put(asList("a", "#", "b"),
                  asList(asList("a"), asList("b")));
        TESTCASES.put(asList("a", "#", "#", "b"),
                  asList(asList("a"), asList(), asList("b")));
        TESTCASES.put(asList("a", "#", "#", "#", "b"),
                  asList(asList("a"), asList(), asList(), asList("b")));
    }

    static final Predicate<String> TESTPRED = "#"::equals;

    static void testAll(BiFunction<List<String>, Predicate<String>, List<List<String>>> f) {
        TESTCASES.forEach((input, expected) -> {
            List<List<String>> actual = f.apply(input, TESTPRED);
            System.out.println(input + " => " + expected);
            if (!expected.equals(actual)) {
                System.out.println("  ERROR: actual was " + actual);
            }
        });
    }

    static <T> List<List<T>> splitStream(List<T> input, Predicate<? super T> pred) {
        int[] edges = IntStream.range(-1, input.size()+1)
                               .filter(i -> i == -1 || i == input.size() ||
                                       pred.test(input.get(i)))
                               .toArray();

        return IntStream.range(0, edges.length-1)
                        .mapToObj(k -> input.subList(edges[k]+1, edges[k+1]))
                        .collect(Collectors.toList());
    }

    static <T> List<List<T>> splitLoop(List<T> input, Predicate<? super T> pred) {
        List<List<T>> result = new ArrayList<>();
        int start = 0;

        for (int cur = 0; cur < input.size(); cur++) {
            if (pred.test(input.get(cur))) {
                result.add(input.subList(start, cur));
                start = cur + 1;
            }
        }
        result.add(input.subList(start, input.size()));

        return result;
    }

    public static void main(String[] args) {
        System.out.println("===== Loop =====");
        testAll(ListSplitting::splitLoop);
        System.out.println("===== Stream =====");
        testAll(ListSplitting::splitStream);
    }
}

答案 2

我目前想出的唯一解决方案是实现您自己的自定义收集器。

在阅读解决方案之前,我想添加一些关于此的注意事项。我把这个问题更多地作为一个编程练习,我不确定它是否可以用并行流来完成。

因此,您必须知道,如果管道并行运行,它将以静默方式中断

这不是一种可取的行为,应避免。这就是为什么我在合并器部分(而不是 )中抛出一个异常,因为它在组合两个列表时并行使用,因此您有一个异常而不是错误的结果。(l1, l2) -> {l1.addAll(l2); return l1;}

此外,由于列表复制,这并不是很有效(尽管它使用本机方法来复制基础数组)。

下面是收集器实现:

private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
    final List<String> current = new ArrayList<>();
    return Collector.of(() -> new ArrayList<List<String>>(),
        (l, elem) -> {
            if (sep.test(elem)) {
                l.add(new ArrayList<>(current));
                current.clear();
            }
            else {
                current.add(elem);
            }
        },
        (l1, l2) -> {
            throw new RuntimeException("Should not run this in parallel");
        },
        l -> {
            if (current.size() != 0) {
                l.add(current);
                return l;
            }
        );
}

以及如何使用它:

List<List<String>> ll = list.stream().collect(splitBySeparator(Objects::isNull));

输出:

[[a, b], [c], [d, e]]


由于Joop Eggen的答案已经出来,它似乎可以并行完成(给他加分!)。这样,它将自定义收集器实现简化为:
private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) {
    return Collector.of(() -> new ArrayList<List<String>>(Arrays.asList(new ArrayList<>())),
                        (l, elem) -> {if(sep.test(elem)){l.add(new ArrayList<>());} else l.get(l.size()-1).add(elem);},
                        (l1, l2) -> {l1.get(l1.size() - 1).addAll(l2.remove(0)); l1.addAll(l2); return l1;});
}

这让关于并行性的段落有点过时了,但是我让它成为一个很好的提醒。


请注意,流 API 并不总是替代。有些任务使用流更容易,更合适,有些任务则不是。在你的例子中,你也可以为此创建一个实用程序方法:

private static <T> List<List<T>> splitBySeparator(List<T> list, Predicate<? super T> predicate) {
    final List<List<T>> finalList = new ArrayList<>();
    int fromIndex = 0;
    int toIndex = 0;
    for(T elem : list) {
        if(predicate.test(elem)) {
            finalList.add(list.subList(fromIndex, toIndex));
            fromIndex = toIndex + 1;
        }
        toIndex++;
    }
    if(fromIndex != toIndex) {
        finalList.add(list.subList(fromIndex, toIndex));
    }
    return finalList;
}

并称它为.List<List<String>> list = splitBySeparator(originalList, Objects::isNull);

可以改进它以检查边缘情况。