Veiledning til Java 8’s Collectors

1. Oversikt

I denne opplæringen vil vi gå gjennom Java 8s Collectors, som brukes i det siste trinnet i behandlingen av a Strøm.

Hvis du vil lese mer om Strøm API selv, sjekk denne artikkelen.

Hvis du vil se hvordan du kan utnytte kraften til Collectors for parallell behandling, sjekk dette prosjektet.

2. Den Stream.collect () Metode

Stream.collect () er en av Java 8-ene Stream APISine terminalmetoder. Det lar oss utføre foranderlige foldoperasjoner (ompakking av elementer til noen datastrukturer og bruk av ekstra logikk, sammenkobling av dem osv.) På dataelementer som holdes i en Strøm forekomst.

Strategien for denne operasjonen er gitt via Samler grensesnittimplementering.

3. Samlere

Alle forhåndsdefinerte implementeringer finner du i Samlere klasse. Det er vanlig å bruke følgende statiske import med dem for å utnytte økt lesbarhet:

importer statisk java.util.stream.Collectors. *;

eller bare enkelt importsamlere etter eget valg:

importer statisk java.util.stream.Collectors.toList; importer statisk java.util.stream.Collectors.toMap; importer statisk java.util.stream.Collectors.toSet;

I de følgende eksemplene vil vi bruke følgende liste på nytt:

List givenList = Arrays.asList ("a", "bb", "ccc", "dd");

3.1. Collectors.toList ()

Å liste opp samler kan brukes til å samle alle Strøm elementer til en Liste forekomst. Det viktige å huske er det faktum at vi ikke kan anta noe spesielt Liste implementering med denne metoden. Hvis du vil ha mer kontroll over dette, bruk til Samling i stedet.

La oss lage en Strøm forekomst som representerer en sekvens av elementer og samler dem i en Liste forekomst:

Listeresultat = givenList.stream () .collect (toList ());

3.1.1. Collectors.toUnmodifiableList ()

Java 10 introduserte en praktisk måte å akkumulere Strøm elementer til en umodifiserbar Liste:

Listeresultat = givenList.stream () .collect (toUnmodifiableList ());

Hvis vi nå prøver å endre resultatListe, vi får en Ikke-støttetOperationException:

assertThatThrownBy (() -> result.add ("foo")) .isInstanceOf (UnsupportedOperationException.class);

3.2. Collectors.toSet ()

Å sette samler kan brukes til å samle alle Strøm elementer til en Sett forekomst. Det viktige å huske er det faktum at vi ikke kan anta noe spesielt Sett implementering med denne metoden. Hvis vi vil ha mer kontroll over dette, kan vi bruke til Samling i stedet.

La oss lage en Strøm forekomst som representerer en sekvens av elementer og samler dem i en Sett forekomst:

Sett resultat = givenList.stream () .collect (toSet ());

EN Sett inneholder ikke dupliserte elementer. Hvis samlingen vår inneholder elementer som er like hverandre, vises de i resultatet Sett bare én gang:

List listWithDuplicates = Arrays.asList ("a", "bb", "c", "d", "bb"); Sett resultat = listWithDuplicates.stream (). Collect (toSet ()); assertThat (resultat) .hasSize (4);

3.2.1. Collectors.toUnmodifiableSet ()

Siden Java 10 kan vi enkelt lage et umodifiserbart Sett ved hjelp av toUnmodifiableSet () samler:

Sett resultat = givenList.stream () .collect (toUnmodifiableSet ());

Ethvert forsøk på å endre resultat Sett vil ende opp med Ikke-støttetOperationException:

assertThatThrownBy (() -> result.add ("foo")) .isInstanceOf (UnsupportedOperationException.class);

3.3. Collectors.toCollection ()

Som du sikkert allerede har lagt merke til, når du bruker toSet og toList samlere, kan du ikke gjøre noen antakelser om implementeringene deres. Hvis du vil bruke en tilpasset implementering, må du bruke til Samling samler med en gitt samling etter eget valg.

La oss lage en Strøm forekomst som representerer en sekvens av elementer og samler dem i en LinkedList forekomst:

Listeresultat = givenList.stream () .collect (toCollection (LinkedList :: new))

Legg merke til at dette ikke vil fungere med uforanderlige samlinger. I slike tilfeller må du enten skrive en egendefinert Samler implementering eller bruk collectAndThen.

3.4. Samlere.å kartlegge()

Å kartlegge samler kan brukes til å samle inn Strøm elementer til en Kart forekomst. For å gjøre dette må vi tilby to funksjoner:

  • keyMapper
  • valueMapper

keyMapper vil bli brukt til å trekke ut en Kart nøkkel fra en Strøm element, og valueMapper vil bli brukt til å trekke ut en verdi tilknyttet en gitt nøkkel.

La oss samle disse elementene i en Kart som lagrer strenger som nøkler og deres lengder som verdier:

Kartresultat = givenList.stream () .collect (toMap (Function.identity (), String :: lengde))

Funksjon.identitet () er bare en snarvei for å definere en funksjon som aksepterer og returnerer den samme verdien.

Hva skjer hvis samlingen vår inneholder dupliserte elementer? I strid med å sette, å kartlegge filtrerer ikke duplikater stille. Det er forståelig - hvordan skal det finne ut hvilken verdi du skal velge for denne nøkkelen?

List listWithDuplicates = Arrays.asList ("a", "bb", "c", "d", "bb"); assertThatThrownBy (() -> {listWithDuplicates.stream (). collect (toMap (Function.identity (), String :: length));}). isInstanceOf (IllegalStateException.class);

Noter det å kartlegge vurderer ikke engang om verdiene også er like. Hvis den ser dupliserte nøkler, kaster den umiddelbart en IllegalStateException.

I slike tilfeller med nøkkelkollisjon, bør vi bruke å kartlegge med en annen signatur:

Kartresultat = givenList.stream () .collect (toMap (Funksjon.identitet (), String :: lengde, (element, identiskItem) -> element));

Det tredje argumentet her er en BinaryOperator, hvor vi kan spesifisere hvordan vi vil at kollisjoner skal håndteres. I dette tilfellet velger vi bare noen av disse to kolliderende verdiene fordi vi vet at de samme strengene også alltid vil ha samme lengder.

3.4.1. Collectors.toUnmodifiableMap ()

Tilsvarende som for Listes og Setts, Java 10 introduserte en enkel måte å samle på Strøm elementer til en umodifiserbar Kart:

Kartresultat = givenList.stream () .collect (toMap (Function.identity (), String :: lengde))

Som vi kan se, hvis vi prøver å sette en ny oppføring i en resultat Kart, vi får Ikke-støttetOperationException:

assertThatThrownBy (() -> result.put ("foo", 3)) .isInstanceOf (UnsupportedOperationException.class);

3.5. Samlere.collectingAndThen ()

CollectingAndThen er en spesiell samler som tillater å utføre en annen handling på et resultat rett etter at samlingen er avsluttet.

La oss samle inn Strøm elementer til en Liste forekomst og deretter konvertere resultatet til en ImmutableList forekomst:

Listeresultat = givenList.stream () .collect (collectAndThen (toList (), ImmutableList :: copyOf))

3.6. Samlere.joining ()

Bli med samler kan brukes til sammenføyning Strøm elementer.

Vi kan bli sammen med dem ved å gjøre:

String result = givenList.stream () .collect (joining ());

som vil resultere i:

"abbcccdd"

Du kan også spesifisere egendefinerte separatorer, prefikser, postfikser:

String result = givenList.stream () .collect (joining (""));

som vil resultere i:

"en bb ccc dd"

eller du kan skrive:

String result = givenList.stream () .collect (joining ("", "PRE-", "-POST"));

som vil resultere i:

"PRE-a bb ccc dd-POST"

3.7. Samlere.counting ()

Teller er en enkel samler som gjør det enkelt å telle alle Strøm elementer.

Nå kan vi skrive:

Langt resultat = givenList.stream () .collect (telling ());

3.8. Samlere.summarizingDouble / Long / Int ()

Sammendrag Dobbelt / Lang / Int er en samler som returnerer en spesiell klasse som inneholder statistisk informasjon om numeriske data i a Strøm av ekstraherte elementer.

Vi kan få informasjon om strenglengder ved å gjøre:

DoubleSummaryStatistics result = givenList.stream () .collect (summarizingDouble (String :: length));

I dette tilfellet vil følgende være sant:

assertThat (result.getAverage ()). erEqualTo (2); assertThat (result.getCount ()). erEqualTo (4); assertThat (result.getMax ()). erEqualTo (3); assertThat (result.getMin ()). erEqualTo (1); assertThat (result.getSum ()). erEqualTo (8);

3.9. Collectors.averagingDouble / Long / Int ()

Gjennomsnittlig Dobbelt / Lang / Int er en samler som ganske enkelt returnerer et gjennomsnitt av ekstraherte elementer.

Vi kan få gjennomsnittlig strenglengde ved å gjøre:

Dobbelt resultat = givenList.stream () .collect (averagingDouble (String :: lengde));

3.10. Samlere.summingDobbelt / Lang / Int ()

SummingDouble / Long / Int er en samler som ganske enkelt returnerer en sum av ekstraherte elementer.

Vi kan få en sum av alle strenglengder ved å gjøre:

Dobbelt resultat = givenList.stream () .collect (summingDouble (String :: lengde));

3.11. Collectors.maxBy () / minBy ()

MaxBy/MinBy samlere returnerer det største / minste elementet i en Strøm i henhold til en gitt Komparator forekomst.

Vi kan velge det største elementet ved å gjøre:

Valgfritt resultat = givenList.stream () .collect (maxBy (Comparator.naturalOrder ()));

Legg merke til at returnert verdi er pakket inn i en Valgfri forekomst. Dette tvinger brukere til å revurdere den tomme samlingen hjørnesaken.

3.12. Samlere.gruppering av ()

Gruppering av samler brukes til å gruppere objekter av noen eiendommer og lagre resultater i en Kart forekomst.

Vi kan gruppere dem etter strenglengde og lagre grupperingsresultater i Sett forekomster:

Kart resultat = givenList.stream () .collect (groupingBy (String :: lengde, toSet ()));

Dette vil føre til at følgende stemmer:

assertThat (resultat) .containsEntry (1, newHashSet ("a")) .containsEntry (2, newHashSet ("bb", "dd")) .containsEntry (3, newHashSet ("ccc")); 

Legg merke til at det andre argumentet til gruppering av metoden er en Samler og du er fri til å bruke noe Samler av ditt valg.

3.13. Collectors.partitioningBy ()

Partisjonering av er et spesialtilfelle av gruppering av som aksepterer en Predikere forekomst og samler Strøm elementer til en Kart eksempel som lagrer Boolsk verdier som nøkler og samlinger som verdier. Under den sanne nøkkelen kan du finne en samling elementer som samsvarer med det gitte Predikere, og under den "falske" nøkkelen, kan du finne en samling elementer som ikke samsvarer med det gitte Predikere.

Du kan skrive:

Kart resultat = givenList.stream () .collect (partitioningBy (s -> s.length ()> 2))

Hvilket resulterer i et kart som inneholder:

{false = ["a", "bb", "dd"], true = ["ccc"]} 

3.14. Collectors.teeing ()

La oss finne maksimums- og minimumstall fra et gitt Strøm ved hjelp av samlerne vi har lært så langt:

Listetall = Arrays.asList (42, 4, 2, 24); Valgfritt min = tall.strøm (). Samle (minBy (heltall :: sammenligne til)); Valgfritt max = numbers.stream (). Collect (maxBy (Integer :: CompareTo)); // gjør noe nyttig med min og maks

Her bruker vi to forskjellige samlere og kombinerer deretter resultatet av disse to for å skape noe meningsfylt. Før Java 12 måtte vi operere på det gitte for å dekke slike brukstilfeller Strøm lagre de mellomliggende resultatene i midlertidige variabler to ganger, og kombiner deretter resultatene etterpå.

Heldigvis tilbyr Java 12 en innebygd samler som tar seg av disse trinnene på våre vegne: alt vi trenger å gjøre er å tilby de to samlerne og kombinasjonsfunksjonen.

Siden denne nye samleren trekker den gitte strømmen i to forskjellige retninger, kalles den teeing:

numbers.stream (). collect (teeing (minBy (Integer :: comparTo), // The first collector maxBy (Integer :: comparTo), // The second collector (min, max) -> // Mottar resultatet fra de samlere og kombinerer dem));

Dette eksemplet er tilgjengelig på GitHub i core-java-12-prosjektet.

4. Egendefinerte samlere

Hvis du vil skrive Collector-implementeringen din, må du implementere Collector-grensesnittet og spesifisere de tre generiske parametrene:

samler for offentlig grensesnitt {...}
  1. T - typen objekter som vil være tilgjengelige for innsamling,
  2. EN - typen av et mutabelt akkumulatorobjekt,
  3. R - typen sluttresultat.

La oss skrive et eksempel på samler for å samle inn elementer i en ImmutableSet forekomst. Vi begynner med å spesifisere de riktige typene:

privat klasse ImmutableSetCollector implementerer Collector {...}

Siden vi trenger en foranderlig samling for håndtering av intern innsamlingsoperasjon, kan vi ikke bruke den ImmutableSet for dette; vi må bruke en annen foranderlig samling eller andre klasser som midlertidig kan samle gjenstander for oss.

I dette tilfellet vil vi fortsette med en ImmutableSet.Builder og nå må vi implementere fem metoder:

  • Leverandørleverandør()
  • BiConsumerakkumulator()
  • BinaryOperatorkombinator()
  • Funksjonetterbehandler()
  • Sett kjennetegn()

Leverandøren()metoden returnerer a Leverandør forekomst som genererer en tom akkumulatorforekomst, så i dette tilfellet kan vi ganske enkelt skrive:

@Override offentlig leverandør leverandør () {return ImmutableSet :: builder; } 

Akkumulatoren () metoden returnerer en funksjon som brukes for å legge til et nytt element til et eksisterende akkumulator objekt, så la oss bare bruke Bygger‘S legge til metode.

@Override offentlig BiConsumer akkumulator () {return ImmutableSet.Builder :: add; }

Kombineren ()metoden returnerer en funksjon som brukes til å slå sammen to akkumulatorer:

@Override public BinaryOperator combiner () {retur (venstre, høyre) -> left.addAll (right.build ()); }

Etterbehandleren () metoden returnerer en funksjon som brukes til å konvertere en akkumulator til sluttresultat, så i dette tilfellet vil vi bare bruke Bygger‘S bygge metode:

@Override public Function etterbehandler () {return ImmutableSet.Builder :: build; }

Egenskapene () metoden brukes til å gi Stream litt tilleggsinformasjon som vil bli brukt til interne optimaliseringer. I dette tilfellet tar vi ikke hensyn til elementrekkefølgen i a Sett slik at vi vil bruke Kjennetegn. UORDENERT. For å få mer informasjon om dette emnet, sjekk Kjennetegn‘JavaDoc.

@ Override public Sett egenskaper () {return Sets.immutableEnumSet (Characteristics.UNORDERED); }

Her er den komplette implementeringen sammen med bruken:

offentlig klasse ImmutableSetCollector implementerer Collector {@Override public Supplier leverandør () {return ImmutableSet :: builder; } @Override offentlig BiConsumer akkumulator () {return ImmutableSet.Builder :: add; } @Override public BinaryOperator combiner () {retur (venstre, høyre) -> left.addAll (right.build ()); } @Override public Function etterbehandler () {return ImmutableSet.Builder :: build; } @ Override public Sett egenskaper () {return Sets.immutableEnumSet (Characteristics.UNORDERED); } public static ImmutableSetCollector toImmutableSet () {return new ImmutableSetCollector (); }

og her i aksjon:

List givenList = Arrays.asList ("a", "bb", "ccc", "dddd"); ImmutableSet result = givenList.stream () .collect (toImmutableSet ());

5. Konklusjon

I denne artikkelen utforsket vi grundige Java 8-er Samlere og viste hvordan man implementerer en. Sørg for å sjekke et av prosjektene mine som forbedrer mulighetene for parallellbehandling i Java.

Alle kodeeksempler er tilgjengelige på GitHub. Du kan lese mer interessante artikler på nettstedet mitt.