Der sauberste Weg, eine Sammlung anhand einer Eigenschaft des Elements zu indizieren, das selbst eine Sammlung ist
-
20-12-2019 - |
Frage
Ich habe ein List<Foo>
und möchte eine Guave Multimap<String, Foo>
wo wir die gruppiert haben Foo
ist bei jedem ihrer Tags angegeben Collection<String> getTags()
Funktion.
Ich verwende Java 8, daher sind Lambdas und Methodenreferenzen in Ordnung bzw. werden empfohlen.
Wenn ich zum Beispiel:
foo1, tags=a,b,c
foo2, tags=c,d
foo3, tags=a,c,e
Ich würde eins bekommen Multimap<String, Foo>
mit:
a -> foo1, foo3
b -> foo1
c -> foo1, foo2, foo3
d -> foo2
e -> foo3
Lösung
Sie können hierfür einen benutzerdefinierten Collector verwenden:
Multimap<String, Foo> map = list.stream().collect(
ImmutableMultimap::builder,
(builder, value) -> value.getTags().forEach(tag -> builder.put(tag, value)),
(builder1, builder2) -> builder1.putAll(builder2.build())
).build();
Dies verursacht keine zusätzlichen Nebenwirkungen (siehe Hier dazu) ist gleichzeitig und idiomatischer.
Sie können diese Ad-hoc-Lambdas auch in einen vollwertigen Collector extrahieren, etwa so:
public static <T, K> Collector<T, ?, Multimap<K, T>> toMultimapByKey(Function<? super T, ? extends Iterable<? extends K>> keysMapper) {
return new MultimapCollector<>(keysMapper);
}
private static class MultimapCollector<T, K> implements Collector<T, ImmutableMultimap.Builder<K, T>, Multimap<K, T>> {
private final Function<? super T, ? extends Iterable<? extends K>> keysMapper;
private MultimapCollector(Function<? super T, ? extends Iterable<? extends K>> keysMapper) {
this.keysMapper = keysMapper;
}
@Override
public Supplier<ImmutableMultimap.Builder<K, T>> supplier() {
return ImmutableMultimap::builder;
}
@Override
public BiConsumer<ImmutableMultimap.Builder<K, T>, T> accumulator() {
return (builder, value) -> keysMapper.apply(value).forEach(k -> builder.put(k, value));
}
@Override
public BinaryOperator<ImmutableMultimap.Builder<K, T>> combiner() {
return (b1, b2) -> b1.putAll(b2.build());
}
@Override
public Function<ImmutableMultimap.Builder<K, T>, Multimap<K, T>> finisher() {
return ImmutableMultimap.Builder<K, T>::build;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
Dann würde die Sammlung so aussehen:
Multimap<String, Foo> map = list.stream().collect(toMultimapByKey(Foo::getTags));
Sie können auch zurückkehren EnumSet.of(Characteristics.UNORDERED)
aus characteristics()
Methode, wenn die Reihenfolge für Sie nicht wichtig ist.Dies kann dazu führen, dass die internen Sammelmechanismen effizienter arbeiten, insbesondere bei paralleler Reduzierung.
Andere Tipps
ImmutableMultimap.Builder<String, Foo> builder = ImmutableMultimap.builder();
list.forEach(foo -> foo.getTags().forEach(tag -> builder.put(tag, foo));
return builder.build();
Dies ist für Java 8-Streams etwas idiomatischer:
Multimap<String, Foo> map = list.stream()
//First build a stream of Pair<String, Foo>
.flatMap(f -> f.tags.stream().map(s -> new AbstractMap.SimpleImmutableEntry<>(s, f)))
//Then collect it up into a multimap.
.collect(
Multimaps.toMultimap(
x -> x.getKey(),
x -> x.getValue(),
MultimapBuilder.hashKeys().arrayListValues()::build
)
);
Ich weiß, dass die Verwendung einer Paarklasse zwar hässlich ist, aber das Wichtigste, was ich zeigen wollte, ist das Multimaps.toMultmap
Kollektor.Es gibt auch ein Multmaps.flatteningToMultimap
für andere Anwendungsfälle.