如何在Java 8中从List<T>中删除重复项

6

实际上,我知道通过distinct(),或将List分配给Set等方法来减少重复,但我有一个稍微不同的问题。 如何在JAVA 8中使用stream或者可能使用StreamEx来聪明地解决下面的问题?

假设我们有一个对象列表:

A, A, A, B, B, A, A, A, C, C, C, A, A, B, B, A

现在我需要:

A, B, A, C, A, B, A

所以当下一个元素与当前元素相同时删除重复的,但是如果下一个元素与当前元素不同时,则应保留。 我尝试了一些解决方案,但它们很丑陋,难以阅读。


2
我可能错了,但是流似乎不是最好的工具,因为你需要存储某个状态,以便告诉我们先前的值,如果我没记错的话,流更喜欢是无状态的。为什么不使用简单的循环呢? - Pshemo
3
你可以使用有状态的过滤器来实现,但是不建议这样做,因为如果流式处理是并行的话,它就会失败。 - Andreas
4
你最好的选择可能是创建自己的Collector,这样重复的元素就可以在添加到结果List时被移除。更好的选择是不要使用流。 - Andreas
这听起来真的像是一个钉子问题。使用 Set 并解决它。 :) - Stefan Haberl
6个回答

12

选项1:筛选

可以编写一个有状态的筛选器,但是你绝不能这样做,因为它违反了filter(Predicate<? super T> predicate)的契约:

predicate - 应用于每个元素以确定是否应包括它的非干扰性无状态谓词

public class NoRepeatFilter<T> implements Predicate<T> {
    private T prevValue;
    @Override
    public boolean test(T value) {
        if (value.equals(this.prevValue))
            return false;
        this.prevValue = value;
        return true;
    }
}

测试

List<String> result = Stream
        .of("A", "A", "A", "B", "B", "A", "A", "A", "C", "C", "C", "A", "A", "B", "B", "A")
//      .parallel()
        .filter(new NoRepeatFilter<>())
        .collect(Collectors.toList());
System.out.println(result);

输出

[A, B, A, C, A, B, A]

它必须是无状态的原因是,如果流是并行的,例如取消注释 .parallel() 后再次运行测试,则会失败:

[A, A, B, B, A, C, C, C, A, B, B, A]


选项2:收集器

一种有效的解决方案是使用 Collector 创建自己的收集器,使用 of(...)

public class NoRepeatCollector {
    public static <E> Collector<E, ?, List<E>> get() {
        return Collector.of(ArrayList::new,
                            NoRepeatCollector::addNoRepeat,
                            NoRepeatCollector::combineNoRepeat);
    }
    private static <E> void addNoRepeat(List<E> list, E value) {
        if (list.isEmpty() || ! list.get(list.size() - 1).equals(value))
            list.add(value);
    }
    private static <E> List<E> combineNoRepeat(List<E> left, List<E> right) {
        if (left.isEmpty())
            return right;
        if (! right.isEmpty())
            left.addAll(left.get(left.size() - 1).equals(right.get(0))
                        ? right.subList(1, right.size()) : right);
        return left;
    }
}

测试

List<String> result = Stream
        .of("A", "A", "A", "B", "B", "A", "A", "A", "C", "C", "C", "A", "A", "B", "B", "A")
//      .parallel()
        .collect(NoRepeatCollector.get());
System.out.println(result);

输出结果(使用和不使用.parallel()

[A, B, A, C, A, B, A]


选项3:循环

如果您的输入是List(或其他Iterable),您可以使用简单的循环删除重复值:

public static <E> void removeRepeats(Iterable<E> iterable) {
    E prevValue = null;
    for (Iterator<E> iter = iterable.iterator(); iter.hasNext(); ) {
        E value = iter.next();
        if (value.equals(prevValue))
            iter.remove();
        else
            prevValue = value;
    }
}

测试

List<String> list = new ArrayList<>(Arrays.asList(
        "A", "A", "A", "B", "B", "A", "A", "A", "C", "C", "C", "A", "A", "B", "B", "A"));
removeRepeats(list);
System.out.println(list);

Output

[A, B, A, C, A, B, A]


3
状态谓词是有状态的,它只能在流是顺序的情况下工作,但我认为这种解决方案不应该被鼓励。循环解决方案如果列表/可迭代对象在第一个位置包含空元素,则失败,否则就可以。最后,我赞成基于收集器的解决方案,这应该是2018年之前的Java开发人员共同掌握的常识。 - fps
@Andrew 选项3表现最佳,如果可以的话,请使用它,否则使用选项2。正如我在答案中已经说过的那样,不要使用选项1,但我包括它是因为它是你这种问题的常见建议,尽管它是一个糟糕的建议。 - Andreas
1
谢谢。对我来说,最易读的解决方案是实现Predicate,而且要尽可能简洁和清晰。是的,这违反了您所写的一些规则,但在这种情况下,我认为可接受,因为对我来说最重要的是易读性。 - Mbded
1
你只需要意识到,你的有状态谓词可能会在并行流中出现问题,以及在 flatMap 上下文中和其他一些情况下也可能会出现问题... - Holger
@Andreas - 对于第一个对象,我们有prevValue = null。为什么Predicate.test(T t)不会为第一个对象抛出异常,因为它具有空的prevValue? - armani
1
@armani 因为文档,即 equals() 的 javadoc 说明不允许抛出 NPE: 对于任何非空引用值 xx.equals(null) 应返回 false - Andreas

1

不使用流也很简单...像这样:

public List<T> noConsecutiveDuplicates(final List<T> input) {   
    final List<T> output = new ArrayList<>();
    for (final T element : input) {
        if (!element.equals(lastElement(output))) {
            output.add(element);
        }
    }
    return output;
}    

private T lastElement(final List<T> list) {
    if (list.size() == 0) {
        return null;
    }
    return list.get(list.size() - 1);
}

1

我建议尝试使用StreamEx,并使用StreamEx::collapse

List<String> strings = Arrays.asList("A", "A", "A", "B", "B", "A", "A", "A", "C", "C", "C", "A", "A", "B", "B", "A");

List<String> collect = StreamEx.of(strings)
        .collapse(Objects::equals)
        .collect(Collectors.toList());

通过使用原始的Java并利用"边缘检测"的思想也是可能的:

List<String> collect = IntStream.range(0, strings.size())
        .filter(i -> i == 0 || !Objects.equals(strings.get(i - 1), strings.get(i)))
        .mapToObj(strings::get)
        .collect(Collectors.toList());

1
List<String> lst = Arrays.asList("A", "A", "A", "B", "B", "A", "A", "A", "C", "C", "C", "A", "A", "B", "B", "A");
       List<String> result = IntStream.range(0, lst.size())
      .filter(index->index ==0 || !lst.get(index).equals(lst.get(index-1)))
      .mapToObj(i->lst.get(i)).collect(Collectors.toList());

result.stream().forEach(System.out::print);

你可以简单地迭代数据源的索引,并过滤掉那些与前一个元素不同的元素。

0

这可能不是最干净的解决方案,但您可以使用一个过滤器来记住先前的流值。

class noDuplicateFilter implementsd Function<T>{
    private T previous=null;

    public boolean test(T input){

       boolean distinct= !Objects.equals(input, previous);
       this.previous = input;
       return distinct;
    }
}

然后在您的流中使用它。

可能在JavaRx中有更清晰的解决方案。

这里还有一些解决方案此处


2
我也想要一个“清洁的解决方案” :-) - Jim Garrison
3
如果流是并行的,它就不够干净,因为会“失败”。 - Andreas

0

我认为最简洁的方法是使用以下的reduce方法;

import java.util.ArrayList; 
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;

public class Main {
    public static void main(String[] args) {
        List<String> ss =Arrays.asList("A","A","A","B","B", "A","A","A", "C", "C", "C","A","A","B","B","A");
        BiFunction<ArrayList<String>, String, ArrayList<String>> acc = new BiFunction<ArrayList<String>, String, ArrayList<String>>() {
        @Override
        public ArrayList<String> apply(ArrayList<String> strings, String s) {
                if(strings.isEmpty() || !strings.get(strings.size()-1).equals(s)){
                    strings.add(s);
                }
                return strings;
            }
        };
        BinaryOperator<ArrayList<String>> combiner = new BinaryOperator<ArrayList<String>>() {
            @Override
            public ArrayList<String> apply(ArrayList<String> strings, ArrayList<String> strings2) {
                strings.addAll(strings2);
                return strings;
            }
        };
        ss.stream().reduce(new ArrayList<String>(), acc, combiner).forEach(System.out::println);
    }
}

2
合并器出现了问题。如果右侧列表的第一个元素等于左侧列表的最后一个元素,则应该添加右侧子列表从其第二个元素开始,否则只能合并两个列表。(您还应检查两个列表是否为空)。此外,为什么不使用lambda表达式代替匿名内部类呢? - fps
是的,您对所有评论都是正确的,我只是想介绍另一种方法,用非正式的编码方式,关于组合器的问题是它在这种情况下永远不会被使用,然而为了让我们的代码整洁,应该按照您所说的进行修复。 - dursun

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