Подтвердить что ты не робот

Коллекторы GroupBy с ручным вводом

Я хочу сгруппировать список людей. У человека есть некоторые атрибуты, такие как имя, страна, город, почтовый индекс и т.д. Я написал статический код, который работает очень хорошо:

Object groupedData = data.stream().collect(groupingBy(Person::getName, Collectors.groupingBy(Person::getCountry, Collectors.groupingBy(Person::getTown))));

Но проблема в том, что она не динамическая. Иногда я хочу просто группироваться по имени и городу, иногда по атрибутам. Как я могу это сделать? Также приветствуются решения без Java 8.

4b9b3361

Ответ 1

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

public static <T> Map collectMany(List<T> data, Function<T, ?>... groupers) {
    Iterator<Function<T, ?>> iter = Arrays.asList(groupers).iterator();
    Collector collector = Collectors.groupingBy(iter.next());
    while (iter.hasNext()) {
        collector = Collectors.groupingBy(iter.next(), collector);
    }
    return (Map) data.stream().collect(collector);
}

Обратите внимание, что порядок функций grouper отменяется, поэтому вам нужно передать их в обратном порядке (или отменить их внутри функции, например, используя Collections.reverse или Guava Lists.reverse, в зависимости от того, что вы предпочитаете).

Object groupedData = collectMany(data, Person::getTown, Person::getCountry, Person::getName);

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

public static <T> Map collectMany(List<T> data, Function<T, ?>... groupers) {
    Collector collector = Collectors.groupingBy(groupers[groupers.length-1]);
    for (int i = groupers.length - 2; i >= 0; i--) {
        collector = Collectors.groupingBy(groupers[i], collector);
    }
    return (Map) data.stream().collect(collector);
}

Оба подхода вернут Map<?,Map<?,Map<?,T>>>, как и в исходном коде. В зависимости от того, что вы хотите делать с этими данными, также стоит подумать об использовании Map<List<?>,T>, как предложено Tunaki.

Ответ 2

Вы можете группировать список, состоящий из атрибутов, которые вы хотите группировать.

Представьте, что вы хотите группировать имя и страну. Затем вы можете использовать:

Map<List<Object>, List<Person>> groupedData = 
    data.stream().collect(groupingBy(p -> Arrays.asList(p.getName(), p.getCountry())));

Это работает, потому что два списка считаются равными, когда они содержат один и тот же элемент в том же порядке. Поэтому в результирующей карте у вас будет ключ для каждой другой пары имя/страна, а в качестве значения - список лиц с указанным именем и страной. Иными словами, вместо того, чтобы говорить "группа по имени, а затем группа по странам", она эффективно говорит "группа по имени и стране". Преимущество состоит в том, что вы не получаете карту карт карт и т.д.