在Java 8中从列表中提取重复的对象

10
这段代码从原始列表中移除了重复项,但我希望从原始列表中提取重复项而不是将它们删除(此包名称仅是另一个项目的一部分):
已知:
一个 Person POJO:
package at.mavila.learn.kafka.kafkaexercises;

import org.apache.commons.lang3.builder.ToStringBuilder;

public class Person {

private final Long id;
private final String firstName;
private final String secondName;


private Person(final Builder builder) {
    this.id = builder.id;
    this.firstName = builder.firstName;
    this.secondName = builder.secondName;
}


public Long getId() {
    return id;
}

public String getFirstName() {
    return firstName;
}

public String getSecondName() {
    return secondName;
}

public static class Builder {

    private Long id;
    private String firstName;
    private String secondName;

    public Builder id(final Long builder) {
        this.id = builder;
        return this;
    }

    public Builder firstName(final String first) {
        this.firstName = first;
        return this;
    }

    public Builder secondName(final String second) {
        this.secondName = second;
        return this;
    }

    public Person build() {
        return new Person(this);
    }


}

@Override
public String toString() {
    return new ToStringBuilder(this)
            .append("id", id)
            .append("firstName", firstName)
            .append("secondName", secondName)
            .toString();
}
}

复制提取代码。

请注意,我们在这里过滤id和名字以检索新列表,我在别处看到了这段代码,不是自己写的:

package at.mavila.learn.kafka.kafkaexercises;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static java.util.Objects.isNull;

public final class DuplicatePersonFilter {


private DuplicatePersonFilter() {
    //No instances of this class
}

public static List<Person> getDuplicates(final List<Person> personList) {

   return personList
           .stream()
           .filter(duplicateByKey(Person::getId))
           .filter(duplicateByKey(Person::getFirstName))
           .collect(Collectors.toList());

}

private static <T> Predicate<T> duplicateByKey(final Function<? super T, Object> keyExtractor) {
    Map<Object,Boolean> seen = new ConcurrentHashMap<>();
    return t -> isNull(seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE));

}

}

测试代码。如果您运行此测试用例,您将获得[alex,lolita,elpidio,romualdo]。

根据id和firstName提取重复项,我希望得到的是[romualdo,otroRomualdo]而不是之前的结果。

package at.mavila.learn.kafka.kafkaexercises;


import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.*;

public class DuplicatePersonFilterTest {

private static final Logger LOGGER = LoggerFactory.getLogger(DuplicatePersonFilterTest.class);



@Test
public void testList(){

    Person alex = new Person.Builder().id(1L).firstName("alex").secondName("salgado").build();
    Person lolita = new Person.Builder().id(2L).firstName("lolita").secondName("llanero").build();
    Person elpidio = new Person.Builder().id(3L).firstName("elpidio").secondName("ramirez").build();
    Person romualdo = new Person.Builder().id(4L).firstName("romualdo").secondName("gomez").build();
    Person otroRomualdo = new Person.Builder().id(4L).firstName("romualdo").secondName("perez").build();


    List<Person> personList = new ArrayList<>();

    personList.add(alex);
    personList.add(lolita);
    personList.add(elpidio);
    personList.add(romualdo);
    personList.add(otroRomualdo);

    final List<Person> duplicates = DuplicatePersonFilter.getDuplicates(personList);

    LOGGER.info("Duplicates: {}",duplicates);

}

}

在我的工作中,我使用TreeMap和ArrayList使用Comparator能够获得所需的结果,但是这样创建一个列表,然后过滤它,再将过滤器传递到新创建的列表中,这看起来代码很臃肿(可能也不太高效)。
有没有更好的想法来提取重复项?而不是删除它们。
提前谢谢。
更新
感谢所有人的答案。
要使用uniqueAttributes相同的方法删除重复项:
  public static List<Person> removeDuplicates(List<Person> personList) {
    return getDuplicatesMap(personList).values().stream()
            .filter(duplicates -> duplicates.size() > 1)
            .flatMap(Collection::stream)
            .collect(Collectors.toList());
}

private static Map<String, List<Person>> getDuplicatesMap(List<Person> personList) {
    return personList.stream().collect(groupingBy(DuplicatePersonFilter::uniqueAttributes));
}

private static String uniqueAttributes(Person person){

    if(Objects.isNull(person)){
        return StringUtils.EMPTY;
    }

    return (person.getId()) + (person.getFirstName()) ;
}

更新2

但是 @brett-ryan 提供的答案也是正确的:

public static List<Person> extractDuplicatesWithIdentityCountingV2(final List<Person> personList){

        List<Person> duplicates = personList.stream()
                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
                .entrySet().stream()
                .filter(n -> n.getValue() > 1)
                .flatMap(n -> nCopies(n.getValue().intValue(), n.getKey()).stream())
                .collect(toList());

        return duplicates;

    }

编辑

以上代码可在以下位置找到:

https://gitlab.com/totopoloco/marco_utilities/-/tree/master/duplicates_exercises

请参阅:

用法: https://gitlab.com/totopoloco/marco_utilities/-/blob/master/duplicates_exercises/src/test/java/at/mavila/exercises/duplicates/lists/DuplicatePersonFilterTest.java

实现: https://gitlab.com/totopoloco/marco_utilities/-/blob/master/duplicates_exercises/src/main/java/at/mavila/exercises/duplicates/lists/DuplicatePersonFilter.java


7
我认为你在这里发布的代码过多。如果可能的话,通常将其限制在10-20行以内。否则可能会让其他人难以理解你在做什么。我建议你删减一些代码。 - Tim Biegeleisen
3
没错,不知道为什么会有人点赞这样一个不太清晰的问题。 - Eugene
在一个方法中获取两个不同的列表有些棘手,最好为每种情况使用两种不同的方法。@MarcoTulioAvilaCerón - Ryuzaki L
@Deadpool 是的,最终类中应该有两种方法(可能还要找到更好的名称),你下面的代码看起来很有前途,我会试一试并向你更新。 - Marco Tulio Avila Cerón
大家好,我找到了我的代码:它做了类似这样的事情:personList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>( Comparator.comparingLong(Person::getId))),ArrayList::new)); //但是这并不起作用。 - Marco Tulio Avila Cerón
显示剩余9条评论
7个回答

11
为了识别重复项,我所知道的没有比 Collectors.groupingBy() 更适合的方法。这使您可以根据您选择的条件将列表分组为映射。
您的条件是 idfirstName 的组合。 让我们将此部分提取到 Person 中的一个方法中:
String uniqueAttributes() {
  return id + firstName;
}

getDuplicates()方法现在非常简单:

public static List<Person> getDuplicates(final List<Person> personList) {
  return getDuplicatesMap(personList).values().stream()
      .filter(duplicates -> duplicates.size() > 1)
      .flatMap(Collection::stream)
      .collect(Collectors.toList());
}

private static Map<String, List<Person>> getDuplicatesMap(List<Person> personList) {
  return personList.stream().collect(groupingBy(Person::uniqueAttributes));
}
  • 首行调用另一个方法getDuplicatesMap(),按上述解释创建映射。
  • 然后流式处理地遍历映射的值,即人员列表。
  • 筛选出除大小为1以外的所有列表,即找到重复项。
  • 最后使用flatMap()将列表的流扁平化为一个人员流,并将流收集到列表中。

另一种选择,如果您真正认为只有当两个人的idfirstName相同时才被视为相等,则可以采用Jonathan Johx的解决方案并实现一个equals()方法。


实际上,这也是一个很好的解决方案,去除重复项采用相同的方法。 - Marco Tulio Avila Cerón
我会给这个解决方案打上标签,因为这是我最终实现的一个。 - Marco Tulio Avila Cerón

5
如果您能在 Person 类中实现 equalshashCode 方法,就可以使用 groupingBy 的计数下游收集器来获取重复的不同元素。
List<Person> duplicates = personList.stream()
  .collect(groupingBy(identity(), counting()))
  .entrySet().stream()
  .filter(n -> n.getValue() > 1)
  .map(n -> n.getKey())
  .collect(toList());

如果您想保留一个连续重复元素的列表,则可以使用Collections.nCopies将其扩展回来。该方法将确保重复的元素在一起排序。

List<Person> duplicates = personList.stream()
    .collect(groupingBy(identity(), counting()))
    .entrySet().stream()
    .filter(n -> n.getValue() > 1)
    .flatMap(n -> nCopies(n.getValue().intValue(), n.getKey()).stream())
    .collect(toList());

这段代码片段的“正确性”在于它提取了重复元素中的单个元素,例如假设您的列表有5个“a”的副本和3个“b”的副本,您的代码片段将返回一个仅包含元素“a”和“b”的列表。 - Marco Tulio Avila Cerón
但是要求非常明确,如果我们使用相同的示例,我们需要返回一个由5个“a”和3个“b”组成的新列表,以给出8个元素的列表长度,但正如我之前提到的,您的代码片段仅返回一个包含2个元素的列表,因此不好,仍然@Magnilex给出的答案是正确的。 - Marco Tulio Avila Cerón
抱歉@MarcoTulioAvilaCerón,但在您提供的原始帖子中似乎不太干净。 - Brett Ryan
没问题,我将所有答案放在Gitlab的此帖子中:https://gitlab.com/totopoloco/marco_utilities/-/tree/master/duplicates_exercises - Marco Tulio Avila Cerón
1
我已更新了一个使用Collections.nCopies的解决方案。你也可以用IntStream.range(n, obj)来实现这个功能,但可读性会降低。 - Brett Ryan
谢谢,这是一个不依赖于显式唯一标识符的好解决方案。 - Marco Tulio Avila Cerón

4
List<Person> duplicates = personList.stream()
  .collect(Collectors.groupingBy(Person::getId))
  .entrySet().stream()
  .filter(e->e.getValue().size() > 1)
  .flatMap(e->e.getValue().stream())
  .collect(Collectors.toList());

那会给你一个包含重复idPerson列表。

好的,如果我们将其链接到按firstName分组的相同机制上,则可以运作。 - Marco Tulio Avila Cerón
其他人已经解决了这个问题,例如,将 Person::getId 替换为 @Magnilex 回答中的 Person::uniqueAttributes 就可以解决这个问题。 - YoYo
这个很容易阅读和实现。 - Sacky San

3
在这种情况下,您需要编写自定义逻辑来从列表中提取重复项,您将在Person列表中获取所有重复项。
   public static List<Person> extractDuplicates(final List<Person> personList) {

    return personList.stream().flatMap(i -> {
        final AtomicInteger count = new AtomicInteger();
        final List<Person> duplicatedPersons = new ArrayList<>();

        personList.forEach(p -> {

            if (p.getId().equals(i.getId()) && p.getFirstName().equals(i.getFirstName())) {
                count.getAndIncrement();
            }

            if (count.get() == 2) {
                duplicatedPersons.add(i);
            }

        });

        return duplicatedPersons.stream();
    }).collect(Collectors.toList());
}

适用于:

 List<Person> l = new ArrayList<>();
           Person alex = new 
 Person.Builder().id(1L).firstName("alex").secondName("salgado").build();
            Person lolita = new 
 Person.Builder().id(2L).firstName("lolita").secondName("llanero").build();
            Person elpidio = new 
 Person.Builder().id(3L).firstName("elpidio").secondName("ramirez").build();
            Person romualdo = new 
 Person.Builder().id(4L).firstName("romualdo").secondName("gomez").build();
            Person otroRomualdo = new 
 Person.Builder().id(4L).firstName("romualdo").secondName("perez").build();
      l.add(alex);
      l.add(lolita);
      l.add(elpidio);
      l.add(romualdo);
      l.add(otroRomualdo);

输出:

[Person [id=4, firstName=romualdo, secondName=gomez], Person [id=4, firstName=romualdo, secondName=perez]]

3

我认为首先你应该重写Person类的equals方法,并关注id和name。然后,你可以添加一个过滤器来更新它。

@Override
public int hashCode() {
    return Objects.hash(id, name);
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    final Person other = (Person) obj;
    if (!Objects.equals(name, other.name)) {
        return false;
    }
    if (!Objects.equals(id, other.id)) {
        return false;
    }
    return true;
}

 personList
       .stream() 
       .filter(p -> personList.contains(p))
       .collect(Collectors.toList());

当你重写equals方法时,你需要同时重写hashcode方法。 - Marco Tulio Avila Cerón
"Proveedor"是什么? - Marco Tulio Avila Cerón
抱歉我在睡觉 xD 没有那行代码 xD 是的!! hashCode 也是。我已经更新了,谢谢。 - Jonathan JOhx

1
基于通用密钥的解决方案:
public static <T> List<T> findDuplicates(List<T> list, Function<T, ?> uniqueKey) {
    if (list == null) {
        return emptyList();
    }
    Function<T, ?> notNullUniqueKey = el -> uniqueKey.apply(el) == null ? "" : uniqueKey.apply(el);
    return list.stream()
            .collect(groupingBy(notNullUniqueKey))
            .values()
            .stream()
            .filter(matches -> matches.size() > 1)
            .map(matches -> matches.get(0))
            .collect(toList());
}


// Example of usage:
List<Person> duplicates = findDuplicates(list, el -> el.getFirstName());

0
List<Person> arr = new ArrayList<>();
arr.add(alex);
arr.add(lolita);
arr.add(elpidio);
arr.add(romualdo);
arr.add(otroRomualdo);

Set<String> set = new HashSet<>();
List<Person> result = arr.stream()
                         .filter(data -> (set.add(data.name +";"+ Long.toString(data.id)) == false))
                         .collect(Collectors.toList());
arr.removeAll(result);
Set<String> set2 = new HashSet<>();
result.stream().forEach(data -> set2.add(data.name +";"+ Long.toString(data.id)));
List<Person> resultTwo = arr.stream()
                            .filter(data -> (set2.add(data.name +";"+ Long.toString(data.id)) == false))
                            .collect(Collectors.toList());
result.addAll(resultTwo);

以上代码将根据名称和ID进行过滤。结果列表将包含所有重复的Person对象


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