The Collector API

Patrik Lindgren
Avanza Tech

--

På Avanza är personlig utveckling en viktig del av kulturen. Vi styr till stor del själva både över inriktning och innehåll, vilket är motiverande och roligt.
Förutom bokcirklar, interna och externa utbildningar går vi förstås också på konferenser. Under hösten 2017 hade jag förmånen att åka till Devoxx i Belgien. En av de första dragningarna jag såg där var en tre timmars föreläsning av José Paumard, som handlade om Javas Collector API.
Detta inlägg är inspirerat av hans föreläsning och jag hoppas i min tur kunna väcka intresse för ämnet och kanske så ett frö till alternativa lösningar.

Collector API vs. Stream API

En sak som inte många vet om eller kanske tänker på, är att alla stream-operationer också kan modelleras med hjälp av en collector.

[Stream]    .stream().count()
[Collector] .collect(counting())
[Stream] .stream().min(naturalOrder())
[Collector] .collect(minBy(naturalOrder()))
[Stream] .stream().mapToInt(…).summaryStatistics()
[Collector] .collect(summarizingInt(…)

I det sistnämnda exemplet så är collect-varianten mer flexibel eftersom den jobbar på objekt (<T>), medan stream-varianten endast jobbar på int:ar.

The Collector Interface

Collector<T, A, R>
  • T = elementen som processas
  • A = en muterbar container
  • R = returtyp, om Function.identity() så är A == R

Metoder i interfacet:

BiConsumer<A, T> accumulator()

En funktion som lägger till elementet <T> i en container <A>.

BinaryOperator<A> combiner()

En funktion som tar två partiella resultat och slår ihop dem.

Supplier<A> supplier()

Returnerar en supplier för den muterbara containern.

Function<A, R> finisher()

Utför den sista transformeringen från <A> till <R>.

Set<Collector.Characteristics> characteristics()

Metadata om collectorn. Används framförallt för att ramverk och bibliotek ska kunna optimera och kan ha följande indikationer:
CONCURRENT, IDENTITY_FINISH samt UNORDERED

Exempel:

.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
Supplier Accumulator Combiner
(muterbar container) (om parallell)

Collectors.toList()

Vi tar en av de absolut mest använda metoderna och tittar lite närmare på vad som händer under ytan.

.collect(Collectors.toList());

Som egentligen är socker för

.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

Om man ändrar ovanstående till

.collect(ArrayList::new, Collection::add, Collection::addAll);

är det sedan lätt att ändra till

.collect(HashSet::new, Collection::add, Collection::addAll);

och vips har vi implementationen av sockermetoden

.collect(Collectors.toSet());

Vi ser ett mönster

Nu ska vi se om vi kan konkatenera strängar på liknande sätt:

.collect(String::new, String::concat, (s1, s2) -> s1.concat(s2))

Det ser ju bra ut på alla sätt och vis, men…

…det är såklart inte möjligt eftersom String är immutable. StringBuilder to the rescue!

.collect(StringBuilder::new,
StringBuilder::append,
StringBuilder::append)

Problemet nu är att collectorn inte returnerar en String utan en StringBuilder…

Vi får lägga till en Finisher: .toString() för att uppnå önskat resultat.

.collect(StringBuilder::new,    // Supplier
StringBuilder::append, // Accumulator
StringBuilder::append) // Combiner
.toString() // Finisher

Finishern är ofta Function.identity() som i fallet med .toList(), men ibland behovs ett litet extrasteg som med StringBuildern ovan.

FilteringCollector

Om man som i följande exempel vill skapa ett histogram över ett företags avdelningar för att se hur många som tjänar över en viss gräns på varje avdelning, blir resultatet missvisande om man filtrerar direkt på strömmen.

employees.stream()
.filter(e -> e.getSalary() > 2000)
.collect(groupingBy(Employee::getDepartment,
Collectors.counting()));

Ger resultatet:

  • Department{title=’HR’}=3
  • Department{title=’BO’}=2

För att även få med de avdelningar som inte har några anställda som uppfyller kriteriet så måste man filtera i collectorn.

employees.stream().collect(
groupingBy(Employee::getDepartment,
new FilteringCollector<>(e -> e.getSalary() > 2000,
Collectors.counting())));

Som då ger det önskade resultatet:

  • Department{title=’KS’}=0
  • Department{title=’HR’}=3
  • Department{title=’BO’}=2

I Java 9 kommer Collectors.filtering() men i väntan på att få sätta tänderna i 9:an vill jag visa att det är relativt enkelt att skriva en egen Collector, som då också blir en återanvändbar komponent.

Se presentationen här: Collectors in the wild! av och med José Paumard.

--

--