【问题标题】:Remove duplicates from a list of objects based on property in Java 8 [duplicate]基于 Java 8 中的属性从对象列表中删除重复项 [重复]
【发布时间】:2015-04-16 09:07:08
【问题描述】:

我正在尝试根据某些属性从对象列表中删除重复项。

我们可以使用 java 8 以简单的方式做到这一点

List<Employee> employee

我们可以根据员工的id 属性从中删除重复项吗?我已经看到从字符串数组列表中删除重复字符串的帖子。

【问题讨论】:

  • 为什么你使用 List 来做这个...使用 Set 而不是 List。
  • 您要搜索employee.name 的重复项吗?或者你的目的是什么,请提供更多信息
  • @Ranjeet 仅在 Employee 以正确识别重复项的方式正确实现 equalshashCode 时才有效。

标签: java list java-8


【解决方案1】:

您可以从List 获取一个流并放入TreeSet,您可以从中提供一个自定义比较器来唯一地比较id。

如果你真的需要一个列表,你可以把这个集合放回一个 ArrayList。

import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.toCollection;

...
List<Employee> unique = employee.stream()
                                .collect(collectingAndThen(toCollection(() -> new TreeSet<>(comparingInt(Employee::getId))),
                                                           ArrayList::new));

举个例子:

List<Employee> employee = Arrays.asList(new Employee(1, "John"), new Employee(1, "Bob"), new Employee(2, "Alice"));

它会输出:

[Employee{id=1, name='John'}, Employee{id=2, name='Alice'}]

另一个想法可能是使用包装器来包装员工,并使用基于其 id 的 equals 和 hashcode 方法:

class WrapperEmployee {
    private Employee e;

    public WrapperEmployee(Employee e) {
        this.e = e;
    }

    public Employee unwrap() {
        return this.e;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        WrapperEmployee that = (WrapperEmployee) o;
        return Objects.equals(e.getId(), that.e.getId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(e.getId());
    }
}

然后你包装每个实例,调用distinct(),打开它们并将结果收集到一个列表中。

List<Employee> unique = employee.stream()
                                .map(WrapperEmployee::new)
                                .distinct()
                                .map(WrapperEmployee::unwrap)
                                .collect(Collectors.toList());

事实上,我认为你可以通过提供一个进行比较的函数来使这个包装器通用:

public class Wrapper<T, U> {
    private T t;
    private Function<T, U> equalityFunction;

    public Wrapper(T t, Function<T, U> equalityFunction) {
        this.t = t;
        this.equalityFunction = equalityFunction;
    }

    public T unwrap() {
        return this.t;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        @SuppressWarnings("unchecked")
        Wrapper<T, U> that = (Wrapper<T, U>) o;
        return Objects.equals(equalityFunction.apply(this.t), that.equalityFunction.apply(that.t));
    }

    @Override
    public int hashCode() {
        return Objects.hash(equalityFunction.apply(this.t));
    }
}

映射将是:

.map(e -> new Wrapper<>(e, Employee::getId))

【讨论】:

  • 您的第一个建议是一个比包装更好的答案:)。 Wrapper 很明显,但第一个要好得多。我不知道collectionAndThen
  • @Patan 没有测试用例很难判断,检查列表中是否没有任何空引用。
  • 第一个例子对我有用,但我不明白为什么:)
  • @Bevor 它使用将比较器作为参数的集合构造函数。在示例中,具有相同 id 的每个员工都将被视为相等,并且在结果集中是唯一的。
  • @AvijitBarua 您可以根据需要比较多个字段。 TreeSet 构造函数将接受任何 Comparator。在 Java 8 及更高版本中,comparingInt 方法只是创建比较 int 字段的 Comparator 的快速方法。如果您想在比较中添加另一个字段,您可以使用链接到原始比较的thenComparing,使其看起来像comparingInt(Employee::getId).thenComparing(Employee::getName)。这似乎是一篇解释比较器的好文章 - baeldung.com/java-8-comparator-comparing
【解决方案2】:

直接在列表中做最简单的方法是

HashSet<Object> seen=new HashSet<>();
employee.removeIf(e->!seen.add(e.getID()));
  • removeIf 将删除满足指定条件的元素
  • 如果Set.add没有修改Set,则Set.add将返回false,即已经包含该值
  • 结合这两者,它将删除之前遇到过id的所有元素(员工)

当然,它只有在列表支持删除元素时才有效。

【讨论】:

  • 你假设 id 是唯一的,如果我有复合键怎么办
  • @user3871754:您需要一个对象来保存复合键并具有适当的equalshashCode 实现,例如yourList.removeIf(e -&gt; !seen.add(Arrays.asList(e.getFirstKeyPart(), e.getSecondKeyPart()));。通过Arrays.asList 组合密钥适用于任意数量的组件,而对于少量组件,专用密钥类型可能更有效。
  • 你是什么意思?我需要至少留下一个
  • 很好的答案!这对我来说比接受的答案更好,即使两者都很好!
  • 很好的解决方案,对我有用。
【解决方案3】:

如果您可以使用equals,请在流中使用distinct 过滤列表(请参阅上面的答案)。如果您不能或不想覆盖 equals 方法,您可以通过以下方式为任何属性 filter 流,例如属性名称(属性 ID 等相同):

Set<String> nameSet = new HashSet<>();
List<Employee> employeesDistinctByName = employees.stream()
            .filter(e -> nameSet.add(e.getName()))
            .collect(Collectors.toList());

【讨论】:

  • 这很好,它利用了过滤器的简单功能,该过滤器决定过滤或维护基于谓词的每个元素(谓词应用于每个元素以确定是否应该包含它),基于集合中的属性(字符串类型)插入:如果是新插入则为 true,如果它已经存在则为 false...这很聪明!对我很有用!
  • 这个例子又好又简单。
  • 在多线程场景/并行流中可以正常工作吗?我的意思是,它是线程安全的吗?
  • 这是从列表中删除重复项的好方法。但我的问题是获取 id 不同的 item form 2 列表。
  • 哇!又好又简单。
【解决方案4】:

另一种解决方案是使用 Predicate,然后您可以在任何过滤器中使用它:

public static <T> Predicate<T> distinctBy(Function<? super T, ?> f) {
  Set<Object> objects = new ConcurrentHashSet<>();
  return t -> objects.add(f.apply(t));
}

然后在任何地方简单地重用谓词:

employees.stream().filter(distinctBy(e -> e.getId));

注意:在过滤器的 JavaDoc 中,它说它需要一个无状态的 Predicte。实际上,即使流是并行的,这也能正常工作。


关于其他解决方案:

1) 使用.collect(Collectors.toConcurrentMap(..)).values() 是一个很好的解决方案,但如果你想排序并保持顺序,那就很烦人了。

2) stream.removeIf(e-&gt;!seen.add(e.getID())); 也是另一个非常好的解决方案。但是我们需要确保集合实现了removeIf,例如如果我们使用Arrays.asList(..)构造集合会抛出异常。

【讨论】:

  • 当您无法覆盖 equals 方法并且不想使用 Set/List 转换(如已接受的答案)使您的 lambda 膨胀时,这是一个很好的解决方案。谢谢!
  • 我想知道为什么java 8库中没有添加这个。使用stream().distinctBy(Employee::Id) 之类的东西会非常方便
  • f 可能为 null 并抛出 nullpointer。
  • 不错!如果您没有 ConcurrentHashSet,您可以将 new ConcurrentHashSet 更改为 ConcurrentHashMap.newKeySet()
【解决方案5】:

试试这个代码:

Collection<Employee> nonDuplicatedEmployees = employees.stream()
   .<Map<Integer, Employee>> collect(HashMap::new,(m,e)->m.put(e.getId(), e), Map::putAll)
   .values();

【讨论】:

    【解决方案6】:

    这对我有用:

    list.stream().distinct().collect(Collectors.toList());
    

    当然,你需要实现equals

    【讨论】:

    【解决方案7】:

    如果顺序无关紧要并且并行运行的性能更高,则收集到地图然后获取值:

    employee.stream().collect(Collectors.toConcurrentMap(Employee::getId, Function.identity(), (p, q) -> p)).values()
    

    【讨论】:

    • 所以,如果你想返回一个列表,我猜是这样的:employee.stream().collect(Collectors.toConcurrentMap(Employee::getId, Function.identity(), (p, q) -&gt; p)).values().stream().collect(Collectors.toList())。而且,关于并行,你可以在这里使用还是不使用 - 我的意思是 parallelStream API?
    • @RokT.. 无需重新创建流,只需将其包装在 ArrayList 中即可。例如:- new ArrayList(.stream().collect()......values());
    【解决方案8】:

    这里有很多好的答案,但我没有找到关于使用reduce 方法的答案。因此,对于您的情况,您可以通过以下方式应用它:

     List<Employee> employeeList = employees.stream()
          .reduce(new ArrayList<>(), (List<Employee> accumulator, Employee employee) ->
          {
            if (accumulator.stream().noneMatch(emp -> emp.getId().equals(employee.getId())))
            {
              accumulator.add(employee);
            }
            return accumulator;
          }, (acc1, acc2) ->
          {
            acc1.addAll(acc2);
            return acc1;
          });
    

    【讨论】:

    • 使用并行流时,合并器有可能再次将具有相同 ID 的员工加在一起。在这种情况下,您还需要检查是否有重复项。
    【解决方案9】:

    另一个简单的版本

    BiFunction<TreeSet<Employee>,List<Employee> ,TreeSet<Employee>> appendTree = (y,x) -> (y.addAll(x))? y:y;
    
    TreeSet<Employee> outputList = appendTree.apply(new TreeSet<Employee>(Comparator.comparing(p->p.getId())),personList);
    

    【讨论】:

    • 这是TreeSet&lt;Employee&gt; outputList = new TreeSet&lt;&gt;(Comparator.comparing(p-&gt;p.getId())); outputList.addAll(personList);的混淆版本,直接的代码要简单得多。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-22
    • 2017-06-23
    • 2021-01-24
    • 1970-01-01
    相关资源
    最近更新 更多