Java 8 фильтрует список объектов по уникальному имени, сохраняя при этом только самый высокий идентификатор?

#java #collections #java-8 #java-stream #maxby

Вопрос:

Предположим, у нас есть класс person с полями:

 Class Person {
  private String name;
  private Integer id (this one is unique);
}
 

И тогда у нас будет List<Person> people такое, что:

 ['Jerry', 993]
['Tom', 3]
['Neal', 443]
['Jerry', 112]
['Shannon', 259]
['Shannon', 533]
 

Как я могу создать новый List<Person> uniqueNames , чтобы он фильтровал только уникальные имена И сохранял самый высокий идентификатор этого имени.

Таким образом, конечный список будет выглядеть следующим образом:

 ['Jerry', 993]
['Tom', 3]
['Neal', 443]
['Shannon', 533]
 

Ответ №1:

Collectors.groupingBy Collectors.maxBy должен сделать трюк, чтобы построить карту людей, сгруппированных по именам, а затем выбрать максимальное значение:

 List<Person> persons = Arrays.asList(
    new Person("Jerry", 123),
    new Person("Tom", 234),
    new Person("Jerry", 456),
    new Person("Jake", 789)
);

List<Person> maxById = persons
    .stream()
    .collect(Collectors.groupingBy(
        Person::getName, 
        Collectors.maxBy(Comparator.comparingInt(Person::getID))
    ))
    .values() // Collection<Optional<Person>>
    .stream() // Stream<Optional<Person>>
    .map(opt -> opt.orElse(null))
    .collect(Collectors.toList());

System.out.println(maxById);
 

Выход:

 [789: Jake, 234: Tom, 456: Jerry]
 

Обновить

есть ли способ получить отдельный список объектов Person, которые были удалены, потому что они были дубликатами в этом потоке()?

Возможно, было бы лучше собрать сгруппированные элементы в список, который следует преобразовать, а затем в какой-нибудь класс-оболочку, предоставляющий информацию о maxById человеке и список выделенных лиц.:

 class PersonList {
    private final Person max;
    private final List<Person> deduped;
    
    public PersonList(List<Person> group) {
        this.max = Collections.max(group, Comparator.comparingInt(Person::getID));
        this.deduped = new ArrayList<>(group);
        this.deduped.removeIf(p -> p.getID() == max.getID());
    }
    
    @Override
    public String toString() {
        return "{max: "   max   "; deduped: "   deduped   "}";
    }
}
 

Тогда лица должны быть собраны вот так:

 List<PersonList> maxByIdDetails = new ArrayList<>(persons
    .stream()
    .collect(Collectors.groupingBy(
        Person::getName, 
        LinkedHashMap::new,
        Collectors.collectingAndThen(
            Collectors.toList(), PersonList::new
        )
    ))
    .values()); // Collection<PersonList>

maxByIdDetails.forEach(System.out::println);
 

Выход:

 {max: 456: Jerry; deduped: [123: Jerry]}
{max: 234: Tom; deduped: []}
{max: 789: Jake; deduped: []}
 

Обновление 2

Получение списка дублированных лиц:

 List<Person> duplicates = persons
    .stream()
    .collect(Collectors.groupingBy(Person::getName))
    .values() // Collection<List<Person>>
    .stream() // Stream<List<Person>>
    .map(MyClass::removeMax)
    .flatMap(List::stream) // Stream<Person>
    .collect(Collectors.toList()); // List<Person>

System.out.println(duplicates);
 

Выход:

 [123: Jerry]
 

где removeMax может быть реализовано подобное:

 private static List<Person> removeMax(List<Person> group) {
    List<Person> dupes = new ArrayList<>();
    Person max = null;

    for (Person p : group) {
        Person duped = null;
        if (null == max) {
            max = p;
        } else if (p.getID() > max.getID()) {
            duped = max;
            max = p;
        } else {
            duped = p;
        }
        if (null != duped) {
            dupes.add(duped);
        }
    }
    return dupes;
}
 

Или, при условии , что hashCode и equals правильно реализованы в классе Person , разница между двумя списками может быть рассчитана с использованием removeAll :

 List<Person> duplicates2 = new ArrayList<>(persons);
duplicates2.removeAll(maxById);
System.out.println(duplicates2);
 

Комментарии:

1. отличное решение. Спасибо

2. Да, это кажется замечательным, есть ли способ получить отдельный список объектов Person, которые были удалены, потому что они были дубликатами в этом потоке()? Я хотел бы объединить или объединить их в stringbuilder для регистрации дубликатов

3. При необходимости может быть применена дополнительная сортировка / упорядочение с помощью LinkedHashMap::new (порядок вставки) или путем сортировки потока значений

4. @stackerstack, пожалуйста, проверьте обновление

5. Хм, есть ли способ ПРОСТО получить список объектов person, которые являются обманом? С теми же полями, что и у моего объекта person, я бы предпочел, чтобы он был простым и понятным

Ответ №2:

Вы можете использовать Collectors#toMap вот так.

 record Person(String name, Integer id) {}

public static void main(String[] args) {
    List<Person> input = List.of(
        new Person("Jerry", 993),
        new Person("Tom", 3),
        new Person("Neal", 443),
        new Person("Jerry", 112),
        new Person("Shannon", 259),
        new Person("Shannon", 533));

    List<Person> output = input.stream()
        .collect(Collectors.toMap(Person::name, Function.identity(),
            (a, b) -> a.id() > b.id() ? a : b, LinkedHashMap::new))
        .values().stream().toList();

    for (Person e : output)
        System.out.println(e);
}
 

выход:

 Person[name=Jerry, id=993]
Person[name=Tom, id=3]
Person[name=Neal, id=443]
Person[name=Shannon, id=533]
 

Вы можете опустить , LinkedHashMap::new , если вас не волнует порядок.

Комментарии:

1. Это дает первый идентификатор для каждого имени, а не самый высокий. Шеннону должно быть 533 года.

Ответ №3:

Вы могли бы попробовать:

 import static java.util.stream.Collectors.*;

persons.stream()
       .collect(
          groupingBy(
            Person::getName, 
            collectingAndThen(
              maxBy(comparingInt(Person::getId)), 
              Optional::get
            )
          )
       )
       .values()
       ;
 
  • Вы группируетесь по имени
  • Затем вы запрашиваете максимальное количество сгруппированных лиц (на одно имя).
  • Затем вы возвращаете значения (так groupingBy как возвращает a Map<String, Optional<Person>> , collectAndThen вызов Optional::get ).

Обратите внимание, что при этом будут перечислены уникальные имена, но не дубликаты имен.

Комментарии:

1. Разве так не должно быть maxBy(comparingInt(Person::getId)) ?

2. ДА. Я не составлял его.

Ответ №4:

есть ли способ получить отдельный список объектов Person, которые были удалены, потому что они были дубликатами в этом потоке()?

 private static final Map<String, Person> highestIds = new HashMap<>();
private static final List<Person> duplicates = new ArrayList<>();

public static void main(String[] args) {
    for (Person person : people) {
        Person result = highestIds.get(person.name);
        if (isPresent(result) amp;amp; person.id > result.id) {
            duplicates.add(result);
            highestIds.put(person.name, person);
        } else if (result == null) {
            highestIds.put(person.name, person);
        } else {
            duplicates.add(person);
        }
    }
    System.out.println("Highest ids:");
    highestIds.values().forEach(System.out::println);
    System.out.println("Duplicates:");
    duplicates.forEach(System.out::println);
}

private static boolean isPresent(Person result) {
    return result != null;
}