Grouping list items in sublists (possibly using guava)

I want to group list items. I am currently doing it like this:

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

Is there a better way to do this, preferably with guava?

+31
java function list guava grouping
Dec 11 '11 at 11:21
source share
4 answers

Of course, perhaps, and even easier with Guava :) Use Multimaps.index (Iterable, Function) :

 ImmutableListMultimap<E, E> indexed = Multimaps.index(list, groupFunction); 

If you give a specific use case, it would be easier to show it in action.

Example from the docs:

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

prints

 {4=[Inky], 6=[Blinky], 5=[Pinky, Pinky, Clyde]} 

In case GroupFunction is defined as:

 GroupFunction<String> groupFunction = new GroupFunction<String>() { @Override public String sameGroup(final String s1, final String s2) { return s1.length().equals(s2.length()); } } 

then it translates to:

 Function<String, Integer> stringLengthFunction = new Function<String, Integer>() { @Override public Integer apply(final String s) { return s.length(); } } 

what is possible stringLengthFunction implementation used in the Guava example.




Finally, in Java 8, the entire snippet can be even simpler, since lambas references and methods are short enough to be nested:

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

For a clean Java 8 example (without Guava) using Collector.groupingBy , see Jeffrey Bosboom's answer , although there are few differences in this approach:

  • it does not return an ImmutableListMultimap , but rather a Map with Collection values,
  • There are no guarantees on the type, variability, serializability or security of the flows of the returned card ( source ),

  • it is a bit more verbose than the Guava + method reference.



EDIT . If you don't need indexed keys, you can get grouped values:

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

which gives you a Lists<List<E>> view, the contents of which can be easily copied into an ArrayList or simply used as is, as you wanted in the first place. Also note that 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 : as Peter Gladkikh mentions in the comment below , if Collection<List<E>> enough, the example above might be simpler:

 Collection<List<E>> grouped = indexed.asMap().values(); 
+64
Dec 11 '11 at 12:51 on
source share

Collector.groupingBy from the Java 8 thread library provides the same functionality as Guava Multimaps.index . Here is an example in Xaerxess answer rewritten to use Java 8 threads:

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

Will open

 {4=[Inky], 5=[Pinky, Pinky, Clyde], 6=[Blinky]} 

If you want to combine the values ​​with the same key in some other way than creating a list, you can use groupingBy overload, which takes a different collector. This example concatenates strings with a separator:

 Map<Integer, String> index = badGuys.stream() .collect(Collectors.groupingBy(String::length, Collectors.joining(" and "))); 

Will open

 {4=Inky, 5=Pinky and Pinky and Clyde, 6=Blinky} 

If you have a large list or grouping function is expensive, you can go in parallel using parallelStream and parallel collector.

 Map<Integer, List<String>> index = badGuys.parallelStream() .collect(Collectors.groupingByConcurrent(String::length)); 

It can print (order is no longer deterministic)

 {4=[Inky], 5=[Pinky, Clyde, Pinky], 6=[Blinky]} 
+8
Sep 20 '14 at 22:12
source share

The easiest and easiest way: Lamdaj grouping function

The above example can be rewritten:

 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
Jul 09 '14 at 23:40
source share

With Java 8, Guava, and a few helper functions, you can implement grouping with a custom 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); } 

Example

 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); }); 
+1
Jan 28 '15 at 9:15
source share



All Articles