使用Guava将列表元素分组成子列表

38

我想将列表中的元素进行分组。目前我是这样做的:

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?

4个回答

71

当然可以,而且使用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();
  }
}

这是可能在Guava示例中使用的stringLengthFunction实现。


最后,在Java 8中,整个代码片段甚至可以更简单,因为lambda表达式和方法引用足够简洁,可以内联:

ImmutableListMultimap<E, E> indexed = Multimaps.index(list, String::length);

针对纯Java 8(不使用Guava)的例子,使用Collector.groupingBy请参见Jeffrey Bosboom的答案,尽管这种方法存在一些差异:

  • 它不返回ImmutableListMultimap,而是带有Collection值的Map
  • 返回的Map类型、可变性、可序列化性或线程安全性没有任何保证source),

  • 相比于Guava+方法引用,它略微冗长。

编辑: 如果您不关心索引键,您可以获取分组的值:

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());

编辑2: 正如Petr Gladkikh在下面的评论中提到的, 如果Collection<List<E>>足够,上面的示例可能会更简单:

Collection<List<E>> grouped = indexed.asMap().values();

5
这正是“Multimap”所设计的情况。 - Ray
5
最后一个代码示例中,使用indexed.asMap().values()可能已经足够得到Collection<List<E>> - Petr Gladkikh
谢谢,这非常有用。我该如何根据多个条件进行分组呢?例如,假设您在函数中收到一个具有两个字段的对象,并且您需要按这些字段进行分组,那么我该怎么做呢?Java 7和8。 - Cristian
如何在Guava中使用字符串作为索引进行分组? - Alex78191
@Alex78191 你具体是什么意思?看起来像是一个单独的问题。 - Grzegorz Rożniecki
@Xaerxess 好的,我找到了。将 Integer 替换为 String。 - Alex78191

12

Collector.groupingBy 是Java 8 streams库中提供的一种与Guava的Multimaps.index相同的功能。以下是Xaerxess的答案中的示例,改写为使用Java 8 streams:

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]}

对于多级分组,您可以按属性值的List<string>进行分组。 - Alex78191

4
最简单和最简洁的方法是使用:Lambdaj分组功能
上面的例子可以重新写成:
List<String> badGuys = Arrays.asList("Inky", "Blinky", "Pinky", "Pinky", "Clyde");
Group group = group(badGuys, by(on(String.class).length)));
System.out.println(group.keySet());

1

使用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);
        });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接