Группировка элементов списка в подсписках (возможно, с использованием guava)
Я хочу группировать элементы списка. В настоящее время я делаю это так:
public static <E> List<List<E>> group(final List<E> list, final GroupFunction<E> groupFunction) {
List<List<E>> result = Lists.newArrayList();
for (final E element : list) {
boolean groupFound = false;
for (final List<E> group : result) {
if (groupFunction.sameGroup(element, group.get(0))) {
group.add(element);
groupFound = true;
break;
}
}
if (! groupFound) {
List<E> newGroup = Lists.newArrayList();
newGroup.add(element);
result.add(newGroup);
}
}
return result;
}
public interface GroupFunction<E> {
public boolean sameGroup(final E element1, final E element2);
}
Есть ли лучший способ сделать это, желательно с помощью guava?
Ответы
Ответ 1
Конечно, возможно, и еще проще с Guava:) Используйте Multimaps.index(Iterable, Function):
ImmutableListMultimap<E, E> indexed = Multimaps.index(list, groupFunction);
Если вы дадите конкретный вариант использования, было бы легче показать его в действии.
Пример из документов:
List<String> badGuys =
Arrays.asList("Inky", "Blinky", "Pinky", "Pinky", "Clyde");
Function<String, Integer> stringLengthFunction = ...;
Multimap<Integer, String> index =
Multimaps.index(badGuys, stringLengthFunction);
System.out.println(index);
печатает
{4=[Inky], 6=[Blinky], 5=[Pinky, Pinky, Clyde]}
В случае, если GroupFunction определяется как:
GroupFunction<String> groupFunction = new GroupFunction<String>() {
@Override public String sameGroup(final String s1, final String s2) {
return s1.length().equals(s2.length());
}
}
то он переводится на:
Function<String, Integer> stringLengthFunction = new Function<String, Integer>() {
@Override public Integer apply(final String s) {
return s.length();
}
}
что возможно stringLengthFunction
реализация, используемая в примере Guava.
Наконец, в Java 8 весь фрагмент может быть еще проще, так как ссылки lambas и методы достаточно кратки, чтобы быть вложенными:
ImmutableListMultimap<E, E> indexed = Multimaps.index(list, String::length);
Для чистого примера Java 8 (без Guava), использующего Collector.groupingBy
, см. Jeffrey Bosboom answer, хотя в этом подходе мало различий:
- он не возвращает
ImmutableListMultimap
, а скорее Map
с Collection
значениями,
-
Нет гарантий по типу, изменчивости, сериализуемости или безопасности потоков возвращаемой карты (source),
- он немного более подробный, чем ссылка метода Guava +.
EDIT. Если вам не нужны индексированные ключи, вы можете получить сгруппированные значения:
List<List<E>> grouped = Lists.transform(indexed.keySet().asList(), new Function<E, List<E>>() {
@Override public List<E> apply(E key) {
return indexed.get(key);
}
});
// or the same view, but with Java 8 lambdas:
List<List<E>> grouped = Lists.transform(indexed.keySet().asList(), indexed::get);
что дает вам Lists<List<E>>
представление, содержимое которого можно легко скопировать в ArrayList
или просто использовать как есть, как вы хотели на первом месте. Также обратите внимание, что indexed.get(key)
ImmutableList
.
// bonus: similar as above, but not a view, instead collecting to list using streams:
List<List<E>> grouped = indexed.keySet().stream()
.map(indexed::get)
.collect(Collectors.toList());
EDIT 2: как Петр Гладких упоминает в комментарии ниже, если Collection<List<E>>
достаточно, пример выше может быть проще:
Collection<List<E>> grouped = indexed.asMap().values();
Ответ 2
Collector.groupingBy
из библиотеки потоков Java 8 обеспечивает ту же функциональность, что и Guava Multimaps.index
. Вот пример в Xaerxess answer, переписанный для использования потоков Java 8:
List<String> badGuys = Arrays.asList("Inky", "Blinky", "Pinky", "Pinky", "Clyde");
Map<Integer, List<String>> index = badGuys.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(index);
Откроется
{4=[Inky], 5=[Pinky, Pinky, Clyde], 6=[Blinky]}
Если вы хотите комбинировать значения с одним и тем же ключом каким-то другим способом, чем создавать список, вы можете использовать перегрузку groupingBy
, которая берет другой сборщик. Этот пример объединяет строки с разделителем:
Map<Integer, String> index = badGuys.stream()
.collect(Collectors.groupingBy(String::length, Collectors.joining(" and ")));
Откроется
{4=Inky, 5=Pinky and Pinky and Clyde, 6=Blinky}
Если у вас большой список или функция группировки стоит дорого, вы можете перейти параллельно с помощью parallelStream
и параллельного коллектора.
Map<Integer, List<String>> index = badGuys.parallelStream()
.collect(Collectors.groupingByConcurrent(String::length));
Это может печатать (порядок больше не детерминирован)
{4=[Inky], 5=[Pinky, Clyde, Pinky], 6=[Blinky]}
Ответ 3
Самый простой и простой способ: Функция группировки Lamdaj
Приведенный выше пример можно переписать:
List<String> badGuys = Arrays.asList("Inky", "Blinky", "Pinky", "Pinky", "Clyde");
Group group = group(badGuys, by(on(String.class).length)));
System.out.println(group.keySet());
Ответ 4
С Java 8, Guava и несколькими вспомогательными функциями вы можете реализовать группировку с помощью пользовательского Comparator
public static <T> Map<T, List<T>> group(List<T> items, Comparator<T> comparator)
{
ListMultimap<T, T> blocks = LinkedListMultimap.create();
if (!ArrayUtils.isNullOrEmpty(items))
{
T currentItem = null;
for (T item : items)
{
if (currentItem == null || comparator.compare(currentItem, item) != 0)
{
currentItem = item;
}
blocks.put(currentItem, ObjectUtils.clone(item));
}
}
return Multimaps.asMap(blocks);
}
Пример
Comparator<SportExercise> comparator = Comparator.comparingInt(SportExercise::getEstimatedTime)
.thenComparingInt(SportExercise::getActiveTime).thenComparingInt(SportExercise::getIntervalCount)
.thenComparingLong(SportExercise::getExerciseId);
Map<SportExercise, List<SportExercise>> blocks = group(sportWorkout.getTrainingExercises(), comparator);
blocks.forEach((key, values) -> {
System.out.println(key);
System.out.println(values);
});