Пересечение двух коллекций

Всем, привет, помогите, пожалуйста, нужен совет.

Есть объект

Person {
    private String name;
}

И два списка заполненных Person, 2й список содержит одинаковые с точки зрения equals объекты, 1й и одинаковые и разные

    List<Person> list1 = List.of(new Person("name1"), new Person("name1"), new Person("name2"),new Person("name3"),new Person("name4"),)
    List<Person> list2 = List.of(new Person("name1"), new Person("name1"))

Как создать 3й лист в который войдут объекты из первого листа, на основании совпадения имен у объектов в list1 и list2? т.е. в итоге должен list3, который содержит те же объекты что и list2

Можно написать что-то типа:

List<Person> list3 = list1.stream().filter(p-> p.getName().equals(list2.get(0).getName())).collect(Collectors.toList());

в принципе это рабочий вариант, но выглядит коряво кмк.. Наверняка есть вариант поизящней.


Ответы (1 шт):

Автор решения: Nowhere Man

Для корректного решения данной задачи следует вычислить частоты вхождений каждого объекта в обоих начальных списках и затем брать минимальную частоту для вычисления пересечения. Стандартные методы типа retainAll не подходят, так как игнорируется частота вхождений в обоих списках:

List<Person> list1 = List.of(new Person("name1"), new Person("name4"));
List<Person> list2 = List.of(new Person("name1"), new Person("name1"), new Person("n1"));
List<Person> copy = new ArrayList<>(list2);
copy.retainAll(list1);
System.out.println("retain: " + copy); // -> [name1, name1], хотя только 1 общее вхождение

Поэтому следует решать так:

List<Person> list1 = List.of(new Person("name1"), new Person("name1"), new Person("name2"), new Person("name3"), new Person("name4"));
List<Person> list2 = List.of(new Person("name1"), new Person("name1"), new Person("name1"));

Map<Person, Integer> map1 = list1.stream()
    .collect(Collectors.groupingBy(p -> p, Collectors.summingInt(x -> 1)));
Map<Person, Integer> map2 = list2.stream()
    .collect(Collectors.groupingBy(p -> p, Collectors.summingInt(x -> 1)));

List<Person> common = map2.keySet().stream()
    .filter(map1::containsKey)
    .flatMap(k -> Stream.generate(() -> k).limit(Math.min(map1.get(k), map2.get(k))))
    .collect(Collectors.toList());

System.out.println(common); // [name1, name1] 

Может быть и более лаконичный вариант, использующий один проход по каждому списку (без построения отдельной мапы частот для второго списка), однако здесь возникает побочный эффект, так как вне стрима модифицируется мапа частот для первого списка, для чего применяется функция Map::merge.

Здесь подходящие элементы второго списка "вычитаются" из второго.

List<Person> common2 = list2.stream()
    .filter(p -> map1.merge(p, -1, Integer::sum) >= 0)
    .collect(Collectors.toList());
System.out.println(common2); // [name1, name1] 
→ Ссылка