【问题标题】:Java 8 - Remove repeated sequence of elements from a ListJava 8 - 从列表中删除重复的元素序列
【发布时间】:2017-01-16 11:28:53
【问题描述】:

我有一个要求,我想使用 Java Stream Api 处理来自系统的事件流,并应用数据清理过程来删除重复的事件。 这是删除按顺序重复多次的同一事件,而不是创建不同事件的列表。大多数在线可用的 Java Stream api 示例都针对从给定输入创建不同的输出。

例如,输入流

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

输出列表或流应该是

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

我当前的实现(不使用 Stream api)看起来像

public class Test {
    public static void main(String[] args) {
        String fileName = "src/main/resources/test.log";
        try {
            List<String> list = Files.readAllLines(Paths.get(fileName));
            LinkedList<String> acc = new LinkedList<>();

            for (String line: list) {
                if (acc.isEmpty())
                    acc.add(line);
                else if (! line.equals(acc.getLast()) )
                    acc.add(line);
            }

            System.out.println(list);
            System.out.println(acc);

        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
}

输出,

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

我尝试了各种使用 reduce、groupingBy 等的示例,但均未成功。如果有这样的可能性,我似乎找不到将流与累加器中的最后一个元素进行比较的方法。

【问题讨论】:

  • 附带说明,请考虑阅读“When to use LinkedList over ArrayList?”。简单地说,你几乎不想使用LinkedList...
  • 重复项一定是连续的吗?你能在后面加上另一个“a”,例如“d”吗?如果,是否应该将其删除?
  • @Mureinik 声明“这将删除按顺序重复多次的同一事件”已经涵盖了这种情况 IMO。
  • @CKing 出于某种原因,我完全跳过了那句话。过失。
  • @Amitoj 欢迎来到 Stack Overflow 和好问题。请阅读What should I do when someone answers my question。您的问题有 3 个答案。您可以留下 cmets 进行澄清或接受其中一个答案作为最能解决您的问题的答案。

标签: java java-8 java-stream


【解决方案1】:

另一种简洁的语法是

AtomicReference<Character> previous = new AtomicReference<>(null);
Stream.of('a', 'b', 'b', 'a').filter(cur -> !cur.equals(previous.getAndSet(cur)));

【讨论】:

    【解决方案2】:

    您可以使用自定义Collector 来实现您的目标。请在下面找到详细信息:

    Stream<String> lines =  Files.lines(Paths.get("distinct.txt"));
    LinkedList<String> values = lines.collect(Collector.of(
                LinkedList::new,
                (list, string) -> {
                    if (list.isEmpty())
                        list.add(string);
                    else if (!string.equals(list.getLast()))
                        list.add(string);
                },
                (left, right) -> {
                    left.addAll(right);
                    return left;
                }
        ));
    
    values.forEach(System.out::println);
    

    但是使用parallel 流时可能会出现一些问题。

    【讨论】:

    • 并行执行的问题是合并器不会检查left的最后一个元素是否匹配right的第一个元素。在这种情况下,不得添加第一个元素。正确的组合器是 if(left.isEmpty()) return right; else if(!right.isEmpty()) left.addAll(left.getLast().equals(right.getFirst())? right.subList(1, right.size()): right); return left;
    【解决方案3】:

    您可以使用IntStream 来获取List 中的索引位置,并使用它来发挥您的优势,如下所示:

    List<String> acc = IntStream
                .range(0, list.size())
                .filter(i -> ((i < list.size() - 1 && !list.get(i).equals(list
                        .get(i + 1))) || i == list.size() - 1))
                .mapToObj(i -> list.get(i)).collect(Collectors.toList());
    System.out.println(acc);
    

    说明

    1. IntStream.range(0,list.size()) :返回原始 int 值元素序列,这些元素将用作访问列表的索引位置。
    2. filter(i -&gt; ((i &lt; list.size() - 1 &amp;&amp; !list.get(i).equals(list.get(i + 1) || i == list.size() - 1)) :仅当当前索引位置的元素不等于下一个索引位置的元素或到达最后一个索引位置时才继续
    3. mapToObj(i -&gt; list.get(i) :将流转换为 Stream&lt;String&gt;
    4. collect(Collectors.toList()) :将结果收集到一个列表中。

    【讨论】:

    • 嗨@CKing,感谢您的快速回复。我刚刚尝试了您的解决方案,这在逻辑上似乎是正确的,但我没有得到想要的输出。请检查gist.github.com/amitoj/6b1705cd127e282cf87921ebe9e5d82e 输出是否与输入相同。
    • @Amitoj 我在 Ideone 中对其进行了测试,并按预期工作。请参阅stdout 了解我的运行情况。您是否按原样复制了我的解决方案,并且确定您的代码中没有其他错误?
    • 嗯,明显的问题是这段代码只适用于测试数据,即字符串文字,但不能从文件中读取字符串。原因在“How do I compare strings in Java?”中描述
    • @Holger 我的错.. 感谢您的评论。我知道如何在 Java 中比较 Strings,我可以从问题中的代码中看到 OP 也是如此。这有时会发生。 (在这种特殊情况下,我的大脑似乎已经阅读了IntStream 并忘记了List 中的值是String
    • 感谢@CKing,我仍在努力思考您的答案,但它有效并且会接受它作为答案。但是,我之前应该提到我想将解决方案应用于连续的数据流 - 源可能是 TCP/UDP 套接字或 Web 套接字,在这种情况下 list.size() 对我们不可用,因为我们将应用直接对流进行操作。你有什么想法可以实现吗?
    【解决方案4】:

    编辑:正如@Bolzano 评论的那样,这种方法不符合要求。

    如果t是输入流那么

    Map<String,Boolean> s = new HashMap<>();
    Stream<String> u = t.filter(e -> s.put(e, Boolean.TRUE)==null);
    

    将产生一个独特元素的流而不创建一个列表。

    然后是普通的

    List<String> m = u.collect(Collectors.toList());
    

    可以在唯一元素上创建一个列表。

    我不明白为什么需要@CKing 和@Anton 提出的冗长解决方案?我错过了什么吗?

    【讨论】:

    • 是的,你遗漏了一些东西,再次比较输入数组和输出数组。他不想要唯一的元素,他想要将重复的元素序列转换为单个元素。如果您想收集独特的元素,您的解决方案也不短,您可以使用流的 distinct() 方法然后收集。 -> list.stream().distinct().collect(...
    • 是的! @Bolzano 你是对的,但是一个非常相似的方法Map&lt;Boolean,String&gt; s = new HashMap&lt;&gt;(); Stream&lt;String&gt; u = t.filter(e -&gt; !e.equals(s.put(Boolean.TRUE, e))); 应该可以过滤与前一个相同的方法。不是吗?
    • 考虑流在第一个元素上并且它的值是“a”,所以“a”在你的哈希图中被标记为真,然后流在 3 个不同的元素之后找到第二个“a”,在这种情况下第二个“a”将被过滤,因为它已经位于您的哈希图中,这种行为与 distinct() 的作用相同。所以是的,它会过滤,但主要问题是不同的。
    • 在注释处的代码片段中,HashMap 只记住最后看到的元素。因此 ["a","b","c","d","a"] 不会删除第二个 "a" 因为 s.put(Boolean.TRUE, e) 将返回 "d" 所以 !e.equals("d") 将是 true。对吗?
    • 抱歉,我在您的评论中错过了 s.put(Boolean.TRUE, e)。看起来,t.filter(e -&gt; !e.equals(s.put(Boolean.TRUE, e))); 也可以完成这项工作。但这种方法不是线程安全的,你有状态。参数实际上应该是最终的,这种方法是绕过该规则的一种技巧。但它会在单个线程中按预期工作。
    【解决方案5】:

    在 Java 7 中,您可以使用迭代器来做到这一点。

    Iterator<Integer> iterator = list.values().iterator();
    Integer previousValue = null;
    
    while(iterator.hasNext()) {
        Integer currentValue = iterator.next();
        if(currentValue.equals(previousValue)){
            iterator.remove();
        }
        previousValue = currentValue;
    }
    

    【讨论】:

    • 请注意,由于各种原因,这实际上并没有回答问题:1) OP 没有询问如何就地进行操作,即创建一个新列表是可以的 2) OP 已经有了一个工作的 pre-Java 8 版本,并明确询问您将如何使用 streams
    【解决方案6】:

    请试试这个解决方案:

    public class TestDuplicatePreviousEvent {
    
    public static void main(String[] args) {
        List<Integer> inputData = new ArrayList<>();
        List<Integer> outputData = new ArrayList<>();
    
        inputData.add(1);
        inputData.add(2);
        inputData.add(2);
        inputData.add(3);
        inputData.add(3);
        inputData.add(3);
        inputData.add(4);
        inputData.add(4);
        inputData.add(4);
        inputData.add(4);
        inputData.add(1);
    
        AtomicInteger index = new AtomicInteger();
        Map<Integer, Integer> valueByIndex = inputData.stream().collect(Collectors.toMap(i -> index.incrementAndGet(), i -> i));
    
        outputData = valueByIndex.entrySet().stream().filter(i -> !i.getValue().equals(valueByIndex.get(i.getKey() - 1))).map(x -> x.getValue()).collect(Collectors.toList());
        System.out.println(outputData);
    }
    

    }

    输出: [1、2、3、4、1]

    没有地图的解决方案:

    public class TestDuplicatePreviousEvent {
    
    public static void main(String[] args) {
        List<Integer> inputData = new ArrayList<>();
        List<Integer> outputData = new ArrayList<>();
    
        inputData.add(1);
        inputData.add(2);
        inputData.add(2);
        inputData.add(3);
        inputData.add(3);
        inputData.add(3);
        inputData.add(4);
        inputData.add(4);
        inputData.add(4);
        inputData.add(4);
        inputData.add(1);
        inputData.add(1);
        inputData.add(1);
        inputData.add(4);
        inputData.add(4);
    
        AtomicInteger index = new AtomicInteger();
        outputData = inputData.stream().filter(i -> filterInputEvents(i, index, inputData)).collect(Collectors.toList());
        System.out.println(outputData);
    }
    
    private static boolean filterInputEvents(Integer i, AtomicInteger index, List<Integer> inputData) {
    
        if (index.get() == 0) {
            index.incrementAndGet();
            return true;
        }
        return !(i.equals(inputData.get(index.getAndIncrement() - 1)));
    }
    

    }

    【讨论】:

    • 此解决方案需要一个额外的步骤来将输入 List 转换为 Map,因为输入数据来自文件。
    猜你喜欢
    • 2018-08-08
    • 2012-05-09
    • 2015-01-05
    • 1970-01-01
    • 2018-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-11-02
    • 2020-08-02
    相关资源
    最近更新 更多