Объединение / объединение элементов объекта в списке

#java #java-11

Вопрос:

Я получил список объектов из класса А в списке. Некоторые из этих объектов равны по идентификатору и имени , но не в списке <B>, а список b ВСЕГДА отличается.

Мне нужно объединить их, чтобы мой список состоял только из объектов a с одинаковым именем и идентификатором, и все объекты b из одной группы были собраны.Я могу использовать утилиты jdk 8 plus, поэтому потоки можно использовать здесь.. Хотя я думаю, что отражение здесь более полезно?

PS: Я не могу изменить содержимое класса a класса b, так как они являются сгенерированными классами и нет возможности доступа / расширения

     @Test
    public void test() {
        List.of(new A(1, "a1", List.of(new B(1, "1b"))),
                new A(1, "a1", List.of(new B(2, "2b"))),
                new A(2, "a2", List.of(new B(3, "3b"))));
//expected
        List.of(new A(1, "a1", List.of(new B(1, "1b"), new B(2, "2b"))),
                new A(2, "a2", List.of(new B(3, "3b"))));


    }


    class A {
        public A(int id, String name, List<B> listB) {
            this.id = id;
            this.name = name;
            this.listB = listB;
        }
        int id;
        String name;
        List<B> listB;
    }

    class B {
        public B(int id, String name) {
            this.id = id;
            this.name = name;
        }
        int id;
        String name;
    }

 

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

1. У них общий супер — класс или что-то в этом роде? Каков формат ввода?

2. ноп.. A-это огромный класс, а B тоже xD и никакой связи, вот имена свойств, действительно вводящие в заблуждение

Ответ №1:

Вы могли бы использовать

 record Key(int id, String name) {};

List<A> result = input.stream().collect(
    Collectors.groupingBy(a -> new Key(a.getId(), a.getName()),
        LinkedHashMap::new,
        Collectors.flatMapping(a -> a.getListB().stream(), Collectors.toList())))
    .entrySet().stream()
    .map(e -> new A(e.getKey().id(), e.getKey().name(), e.getValue()))
    .collect(Collectors.toList());

if(!result.equals(expected)) {
    throw new AssertionError("expected "   expected   " but got "   result);
}
 

Это создает новые списки с новыми A объектами, что подходит для неизменяемых объектов. Ваше использование List.of(…) предполагает предпочтение неизменяемым объектам. Если у вас есть изменяемые объекты и вы хотите выполнить операцию на месте, вы можете сделать

 List<A> result = new ArrayList<>(input); // only needed if input is an immutable list

record Key(int id, String name) {};
HashMap<Key,A> previous = new HashMap<>();

result.removeIf(a -> previous.merge(new Key(a.getId(), a.getName()), a, (old, newA) -> {
        var l = old.getListB();
        if(l.getClass() != ArrayList.class) old.setListB(l = new ArrayList<>(l));
        l.addAll(newA.getListB());
        return old;
    }) != a);

if(!result.equals(expected)) {
    throw new AssertionError("expected "   expected   " but got "   result);
}
 

Это удаляет дубликаты из списка и добавляет их B к ранее обнаруженному оригиналу. Он вносит минимум изменений, необходимых для получения нужного списка, например, если дубликатов нет, он ничего не делает.

Если A объекты с одинаковым идентификатором всегда имеют одно и то же имя, другими словами, нет необходимости в том, чтобы ключевой объект проверял оба, вы могли бы упростить этот подход к

 List<A> result = new ArrayList<>(input); // only needed if input is an immutable list

HashMap<Integer,A> previous = new HashMap<>();

result.removeIf(a -> previous.merge(a.getId(), a, (old, newA) -> {
        var l = old.getListB();
        if(l.getClass() != ArrayList.class) old.setListB(l = new ArrayList<>(l));
        l.addAll(newA.getListB());
        return old;
    }) != a);

if(!result.equals(expected)) {
    throw new AssertionError("expected "   expected   " but got "   result);
}
 

Ответ №2:

Если вам нужно сохранить экземпляр для каждого id , вы можете написать (я предполагаю, что у объектов есть геттеры и сеттеры)

 System.out.println(xs.stream()
        .collect(groupingBy(A::getId, toList()))
        .values().stream()
        .peek(g -> g.get(0).setListB(
                g.stream()
                        .flatMap(h -> h.getListB().stream())
                        .collect(groupingBy(B::getId, toList()))
                        .values().stream()
                        .map(i -> i.get(0))
                        .collect(toList())))
        .map(g -> g.get(0))
        .collect(toList()));
 

ваш входной кейс с выходом

 [A(id=1, name=a1, listB=[B(id=1, name=b1), B(id=2, name=b2)]), A(id=2, name=a2, listB=[B(id=3, name=b3)])]
 

если вы можете создавать новые экземпляры, вы можете перенормировать списки

 System.out.println(xs.stream()
        .flatMap(a -> a.getListB().stream().map(b -> List.<Object>of(a.id, a.name, b.id, b.name)))
        .distinct()
        .collect(groupingBy(o -> o.get(0), toList()))
        .values()
        .stream()
        .map(zs -> new A((int) zs.get(0).get(0), (String) zs.get(0).get(1),
                zs.stream().map(z -> new B((int) z.get(2), (String) z.get(3))).collect(toList())))
        .collect(toList()));
 

(вы можете изменить уродливое .get(0).get(0) , используя какой-нибудь промежуточный класс, называемый DenormalizedRow или около того)

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

1. единственная проблема здесь, я не знаю, какая часть B заполнена, я просто знаю, что они всегда разные:-S, но они могут быть даже нулевыми … однако у вас не будет 2 нулей для одного и того же A!

2. @UnnameDSoS фильтрует по мере необходимости каждый поток

3. можно ли из этого создать коллектор ? или это было бы невозможно, так как вам нужен предыдущий товар?

4. @UnnameDSoS Я не понимаю вашего вопроса (вы можете создать коллектор, но зачем?)