Java 8 - 从列表中删除重复序列的元素

11

我有一个需求,希望使用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等的示例,但都没有成功。似乎找不到一种方法来将流与累加器中的最后一个元素进行比较,如果有这样的可能性。


5
顺便提一下,考虑阅读“何时使用LinkedList而不是ArrayList?”(https://dev59.com/Y3RC5IYBdhLWcg3wW_tk)。简单地说,几乎永远不需要使用LinkedList... - Holger
重复的项一定是连续的吗?例如,在一个“d”之后,你可能还有另一个“a”吗?如果有,应该删除还是保留? - Mureinik
2
@Mureinik 语句“这将删除在序列中重复多次的相同事件”已经涵盖了我个人认为的这种情况。 - Chetan Kinger
1
@CKing 我完全没注意到那句话,不知道为什么。是我的错。 - Mureinik
6个回答

9
你可以使用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 -> ((i < list.size() - 1 && !list.get(i).equals(list.get(i + 1) || i == list.size() - 1)) :仅在当前索引位置的元素不等于下一个索引位置的元素或达到最后一个索引位置时继续执行。
  3. mapToObj(i -> list.get(i) :将流转换为Stream<String>
  4. collect(Collectors.toList()) :将结果收集到List中。

嗨@CKing,感谢您的快速回复。我刚刚尝试了您的解决方案,它在逻辑上似乎是正确的,但我没有得到期望的输出。请检查https://gist.github.com/amitoj/6b1705cd127e282cf87921ebe9e5d82e 输出与输入相同。 - Amitoj
@Amitoj 我在 Ideone 上测试了它,结果符合预期。请查看我的运行的 stdout。你是否完全复制了我的解决方案,并确定你的代码中没有其他错误? - Chetan Kinger
1
显而易见的问题是,这段代码只适用于测试数据,即字符串字面量,而不适用于从文件中读取的字符串。原因在“如何在Java中比较字符串?”中有所描述。 - Holger
1
@Holger 不好意思.. 感谢您的评论。我知道如何在Java中比较String,并且从问题中的代码中可以看出OP也知道。有时候会发生这种情况。(在这种特殊情况下,我的大脑似乎读到了IntStream,并忘记了List中的值是String - Chetan Kinger
但是你没有“流式传输”!因为你需要知道项目集的大小并且需要访问列表。 - dash1e
显示剩余3条评论

4

您可以使用自定义收集器来实现您的目标。请参阅以下详细信息:

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流时,可能会出现一些问题。


4
使用并行执行的问题在于合并器并不检查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; - Holger

3
另一种简洁的语法是:
AtomicReference<Character> previous = new AtomicReference<>(null);
Stream.of('a', 'b', 'b', 'a').filter(cur -> !cur.equals(previous.getAndSet(cur)));

0

使用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已经有了一个可行的Java 8之前版本,并明确要求您如何使用_streams_。 - Thomas

0

编辑:如@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(...) - Ömer Erden
在代码片段中的注释处,HashMap 只会记住最后一个元素。因此 ["a","b","c","d","a"] 不会删除第二个 "a",因为 s.put(Boolean.TRUE, e) 将返回 "d",所以 !e.equals("d") 将是 true。对吗? - Serg M Ten
1
抱歉,我在您的评论中错过了s.put(Boolean.TRUE, e)。看起来,t.filter(e -> !e.equals(s.put(Boolean.TRUE, e)));也可以完成工作。但是这种方法不是线程安全的,因为您有状态。参数应该是有效的最终值,这种方法是一种规避该规则的技巧。但是在单个线程中,它将按预期工作。 - Ömer Erden
当然,这种方法依赖于元素由单个线程按顺序处理的方式。但是是否有可能并行执行连续的去重过滤? - Serg M Ten
@SergioMontoro 你能详细说明一下你所说的“冗长”是什么意思吗? - Chetan Kinger
显示剩余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);

    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]

不使用 map 的解决方案:

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)));
}

}


1
这个解决方案需要一个额外的步骤,将输入的 List 转换为 Map,因为输入数据来自文件。 - Chetan Kinger

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