Gids voor verzamelaars van Java 8

1. Overzicht

In deze tutorial gaan we door Java 8's Collectors, die worden gebruikt bij de laatste stap van het verwerken van een Stroom.

Als je er meer over wilt lezen Stroom API zelf, bekijk dit artikel.

Als je wilt zien hoe je de kracht van Collectors kunt gebruiken voor parallelle verwerking, bekijk dan dit project.

2. Het Stream.collect () Methode

Stream.collect () is een van de Java 8's Stream API‘S terminale methoden. Het stelt ons in staat om veranderlijke vouwbewerkingen uit te voeren (elementen opnieuw inpakken op sommige datastructuren en wat extra logica toepassen, ze aaneenschakelen, enz.) Op data-elementen die in een Stroom voorbeeld.

De strategie voor deze operatie wordt gegeven via Verzamelaar interface implementatie.

3. Verzamelaars

Alle voorgedefinieerde implementaties zijn te vinden in het Verzamelaars klasse. Het is gebruikelijk om de volgende statische import met hen te gebruiken om de leesbaarheid te vergroten:

importeer statische java.util.stream.Collectors. *;

of enkel enkele importverzamelaars naar keuze:

importeer statische java.util.stream.Collectors.toList; importeer statische java.util.stream.Collectors.toMap; importeer statische java.util.stream.Collectors.toSet;

In de volgende voorbeelden zullen we de volgende lijst hergebruiken:

Lijst gegevenList = Arrays.asList ("a", "bb", "ccc", "dd");

3.1. Collectors.toList ()

ToList collector kan worden gebruikt voor het verzamelen van alles Stroom elementen in een Lijst voorbeeld. Het belangrijkste om te onthouden is het feit dat we niet iets specifieks kunnen aannemen Lijst implementatie met deze methode. Als je hier meer controle over wilt hebben, gebruik dan toCollection in plaats daarvan.

Laten we een Stroom instantie die een reeks elementen vertegenwoordigt en deze verzamelt in een Lijst voorbeeld:

Lijstresultaat = gegevenList.stream () .collect (toList ());

3.1.1. Collectors.toUnmodifiableList ()

Java 10 introduceerde een handige manier om het Stroom elementen in een niet te wijzigen Lijst:

Lijstresultaat = gegevenList.stream () .collect (toUnmodifiableList ());

Als we nu proberen het resultaatLijst, we krijgen een UnsupportedOperationException:

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

3.2. Collectors.toSet ()

ToSet collector kan worden gebruikt voor het verzamelen van alles Stroom elementen in een Set voorbeeld. Het belangrijkste om te onthouden is het feit dat we niet iets specifieks kunnen aannemen Set implementatie met deze methode. Als we hier meer controle over willen hebben, kunnen we gebruik maken van toCollection in plaats daarvan.

Laten we een Stroom instantie die een reeks elementen vertegenwoordigt en deze verzamelt in een Set voorbeeld:

Stel resultaat = gegevenList.stream () .collect (toSet ());

EEN Set bevat geen dubbele elementen. Als onze collectie elementen bevat die aan elkaar gelijk zijn, verschijnen ze in het resultaat Set slechts één keer:

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

3.2.1. Collectors.toUnmodifiableSet ()

Sinds Java 10 kunnen we gemakkelijk een niet-wijzigbaar Set gebruik makend van toUnmodifiableSet () verzamelaar:

Stel resultaat = gegevenList.stream () .collect (toUnmodifiableSet ());

Elke poging om het resultaat Set zal eindigen met UnsupportedOperationException:

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

3.3. Collectors.toCollection ()

Zoals je waarschijnlijk al hebt opgemerkt, tijdens het gebruik toSet en toList verzamelaars, kunt u geen aannames doen over hun implementaties. Als u een aangepaste implementatie wilt gebruiken, moet u de toCollection verzamelaar met een verstrekte collectie naar keuze.

Laten we een Stroom instantie die een reeks elementen vertegenwoordigt en deze verzamelt in een LinkedList voorbeeld:

Lijstresultaat = gegevenList.stream () .collect (toCollection (LinkedList :: nieuw))

Merk op dat dit niet zal werken met onveranderlijke verzamelingen. In dat geval moet u ofwel een gewoonte schrijven Verzamelaar implementatie of gebruik collectionAndThen.

3.4. Verzamelaars.in kaart brengen()

In kaart brengen collector kan worden gebruikt om te verzamelen Stroom elementen in een Kaart voorbeeld. Om dit te doen, hebben we twee functies nodig:

  • keyMapper
  • valueMapper

keyMapper wordt gebruikt voor het extraheren van een Kaart sleutel van een Stroom element, en valueMapper wordt gebruikt voor het extraheren van een waarde die is gekoppeld aan een bepaalde sleutel.

Laten we die elementen verzamelen in een Kaart die strings opslaat als sleutels en hun lengte als waarden:

Kaartresultaat = gegevenList.stream () .collect (toMap (Function.identity (), String :: length))

Functie. Identiteit () is slechts een snelkoppeling voor het definiëren van een functie die dezelfde waarde accepteert en retourneert.

Wat gebeurt er als onze collectie dubbele elementen bevat? In tegenstelling tot toSet, in kaart brengen filtert niet stilzwijgend duplicaten. Het is begrijpelijk: hoe moet het uitzoeken welke waarde voor deze sleutel moet worden gekozen?

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

Let daar op in kaart brengen evalueert niet eens of de waarden ook gelijk zijn. Als het dubbele sleutels ziet, gooit het onmiddellijk een IllegalStateException.

In dergelijke gevallen met sleutelbotsing, moeten we gebruiken in kaart brengen met een andere handtekening:

Kaart resultaat = gegevenList.stream () .collect (toMap (Function.identity (), String :: lengte, (item, identiekItem) -> item));

Het derde argument hier is a BinaryOperator, waar we kunnen specificeren hoe we willen dat botsingen worden afgehandeld. In dit geval kiezen we gewoon een van deze twee botsende waarden, omdat we weten dat dezelfde strings ook altijd dezelfde lengte zullen hebben.

3.4.1. Collectors.toUnmodifiableMap ()

Evenzo als voor Lijsts en Sets introduceerde Java 10 een gemakkelijke manier om Stroom elementen in een niet te wijzigen Kaart:

Kaartresultaat = gegevenList.stream () .collect (toMap (Function.identity (), String :: length))

Zoals we kunnen zien, als we proberen een nieuw item in een resultaat kaart, We zullen krijgen UnsupportedOperationException:

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

3.5. Verzamelaars.collectingAndThen ()

CollectingAndThen is een speciale verzamelaar die het mogelijk maakt een andere actie uit te voeren op een resultaat direct nadat het verzamelen is voltooid.

Laten we verzamelen Stroom elementen naar een Lijst instantie en converteer het resultaat vervolgens naar een Onveranderlijke lijst voorbeeld:

Lijstresultaat = gegevenList.stream () .collect (collectionAndThen (toList (), ImmutableList :: copyOf))

3.6. Verzamelaars.joining ()

Deelnemen collector kan worden gebruikt om te verbinden Stroom elementen.

We kunnen ze samenvoegen door te doen:

String resultaat = gegevenList.stream () .collect (samenvoegen ());

wat zal resulteren in:

"abbcccdd"

U kunt ook aangepaste scheidingstekens, voorvoegsels, postfixen specificeren:

String resultaat = gegevenList.stream () .collect (samenvoegen (""));

wat zal resulteren in:

"een bb ccc dd"

of je kunt schrijven:

String resultaat = gegevenList.stream () .collect (samenvoegen ("", "PRE-", "-POST"));

wat zal resulteren in:

"PRE-a bb ccc dd-POST"

3.7. Verzamelaars.counting ()

Tellen is een eenvoudige verzamelaar waarmee u eenvoudig alles kunt tellen Stroom elementen.

Nu kunnen we schrijven:

Lang resultaat = gegevenList.stream () .collect (counting ());

3.8. Verzamelaars.summarizingDouble / Long / Int ()

Samenvattend Dubbel / Lang / Int is een verzamelaar die een speciale klasse retourneert die statistische informatie bevat over numerieke gegevens in een Stroom van geëxtraheerde elementen.

We kunnen informatie over snaarlengtes verkrijgen door het volgende te doen:

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

In dit geval is het volgende waar:

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

3.9. Collectors.averagingDouble / Long / Int ()

Middeling Dubbel / Lang / Int is een verzamelaar die eenvoudigweg een gemiddelde van geëxtraheerde elementen retourneert.

We kunnen de gemiddelde snaarlengte krijgen door te doen:

Dubbel resultaat = gegevenList.stream () .collect (averagingDouble (String :: lengte));

3.10. Verzamelaars.summingDubbel / Lang / Int ()

Opsomming Dubbel / Lang / Int is een verzamelaar die simpelweg een som van geëxtraheerde elementen retourneert.

We kunnen een som krijgen van alle snaarlengtes door te doen:

Dubbel resultaat = gegevenList.stream () .collect (summingDouble (String :: lengte));

3.11. Collectors.maxBy () / minBy ()

MaxBy/MinBy verzamelaars retourneren het grootste / kleinste element van een Stroom volgens een verstrekt Comparator voorbeeld.

We kunnen het grootste element kiezen door te doen:

Optioneel resultaat = gegevenList.stream () .collect (maxBy (Comparator.naturalOrder ()));

Merk op dat de geretourneerde waarde is verpakt in een Optioneel voorbeeld. Dit dwingt gebruikers om opnieuw na te denken over de lege opvanghoek.

3.12. Verzamelaars.groupingBy ()

GroupingBy collector wordt gebruikt voor het groeperen van objecten op basis van een eigenschap en het opslaan van resultaten in een Kaart voorbeeld.

We kunnen ze groeperen op stringlengte en de resultaten van de groepering opslaan in Set gevallen:

Kaart resultaat = gegevenList.stream () .collect (groupingBy (String :: length, toSet ()));

Dit zal ertoe leiden dat het volgende waar is:

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

Merk op dat het tweede argument van de groupingBy methode is een Verzamelaar en je bent vrij om elke Verzamelaar van jouw keuze.

3.13. Collectors.partitioningBy ()

PartitioningBy is een gespecialiseerd geval van groupingBy dat accepteert een Predikaat instantie en verzamelt Stroom elementen in een Kaart instantie die Boolean waarden als sleutels en verzamelingen als waarden. Onder de "true" -toets vindt u een verzameling elementen die overeenkomen met de gegeven Predikaat, en onder de "false" -sleutel vindt u een verzameling elementen die niet overeenkomen met de gegeven Predikaat.

Je kan schrijven:

Kaart resultaat = gegevenList.stream () .collect (partitioningBy (s -> s.length ()> 2))

Wat resulteert in een kaart met:

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

3.14. Collectors.teeing ()

Laten we de maximum- en minimumaantallen van een gegeven vinden Stroom met behulp van de verzamelaars die we tot nu toe hebben geleerd:

Lijstnummers = Arrays.asList (42, 4, 2, 24); Optioneel min = numbers.stream (). Collect (minBy (Integer :: CompareTo)); Optioneel max = numbers.stream (). Collect (maxBy (Integer :: CompareTo)); // doe iets nuttigs met min en max

Hier gebruiken we twee verschillende verzamelaars en combineren we het resultaat van die twee om iets zinvols te creëren. Vóór Java 12 moesten we, om dergelijke use-cases te dekken, werken met het gegeven Stroom Sla twee keer de tussenresultaten op in tijdelijke variabelen en combineer die resultaten daarna.

Gelukkig biedt Java 12 een ingebouwde verzamelaar die deze stappen namens ons uitvoert: het enige wat we hoeven te doen is de twee verzamelaars en de combinerfunctie te voorzien.

Omdat deze nieuwe verzamelaar de gegeven stroom in twee verschillende richtingen leidt, wordt hij genoemd teeing:

numbers.stream (). collect (teeing (minBy (Integer :: CompareTo), // De eerste collector maxBy (Integer :: CompareTo), // De tweede collector (min, max) -> // Ontvangt het resultaat van die verzamelaars en combineert ze));

Dit voorbeeld is beschikbaar op GitHub in het core-java-12-project.

4. Aangepaste verzamelaars

Als u uw Collector-implementatie wilt schrijven, moet u de Collector-interface implementeren en de drie algemene parameters specificeren:

openbare interface Collector {...}
  1. T - het soort objecten dat beschikbaar zal zijn voor verzameling,
  2. EEN - het type van een veranderlijk accumulatorobject,
  3. R - het soort eindresultaat.

Laten we een voorbeeld van een Collector schrijven voor het verzamelen van elementen in een Onveranderlijk ingesteld voorbeeld. We beginnen met het specificeren van de juiste typen:

private class ImmutableSetCollector implementeert Collector {...}

Aangezien we een veranderlijke verzameling nodig hebben voor het afhandelen van interne verzamelingsbewerkingen, kunnen we Onveranderlijk ingesteld voor deze; we moeten een andere veranderlijke verzameling of een andere klasse gebruiken die tijdelijk objecten voor ons zou kunnen verzamelen.

In dit geval gaan we verder met een ImmutableSet.Builder en nu moeten we 5 methoden implementeren:

  • Leverancierleverancier()
  • BiConsumeraccumulator()
  • BinaryOperatorcombiner()
  • Functieafwerker()
  • Set kenmerken()

De leverancier()methode retourneert een Leverancier instantie die een lege accumulatorinstantie genereert, dus in dit geval kunnen we eenvoudig schrijven:

@Override openbare leverancier leverancier () {retourneer ImmutableSet :: builder; } 

De accumulator method geeft een functie terug die wordt gebruikt voor het toevoegen van een nieuw element aan een bestaand accumulator object, dus laten we gewoon de Bouwer‘S toevoegen methode.

@Override openbare BiConsumer accumulator () {return ImmutableSet.Builder :: add; }

De combiner ()methode geeft een functie terug die wordt gebruikt om twee accumulatoren samen te voegen:

@Override openbare BinaryOperator combiner () {return (left, right) -> left.addAll (right.build ()); }

De afmaker methode retourneert een functie die wordt gebruikt voor het converteren van een accumulator naar het type eindresultaat, dus in dit geval zullen we gewoon gebruiken Bouwer‘S bouwen methode:

@Override openbare functie finisher () {retourneer ImmutableSet.Builder :: build; }

De karaktertrekken() methode wordt gebruikt om Stream wat aanvullende informatie te geven die zal worden gebruikt voor interne optimalisaties. In dit geval letten we niet op de volgorde van de elementen in a Set zodat we zullen gebruiken Kenmerken.UNORDERED. Raadpleeg voor meer informatie over dit onderwerp Kenmerken‘JavaDoc.

@Override public Set kenmerken () {return Sets.immutableEnumSet (Characteristics.UNORDERED); }

Hier is de volledige implementatie samen met het gebruik:

public class ImmutableSetCollector implementeert Collector {@Override openbare leverancier leverancier () {retourneer ImmutableSet :: builder; } @Override openbare BiConsumer accumulator () {return ImmutableSet.Builder :: add; } @Override openbare BinaryOperator combiner () {return (left, right) -> left.addAll (right.build ()); } @Override openbare functie finisher () {return ImmutableSet.Builder :: build; } @Override public Set-kenmerken () {retourneer Sets.immutableEnumSet (Characteristics.UNORDERED); } openbare statische ImmutableSetCollector toImmutableSet () {retourneer nieuwe ImmutableSetCollector (); }

en hier in actie:

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

5. Conclusie

In dit artikel hebben we dieper ingegaan op Java 8's Verzamelaars en liet zien hoe je er een kunt implementeren. Zorg ervoor dat u een van mijn projecten bekijkt die de mogelijkheden van parallelle verwerking in Java verbetert.

Alle codevoorbeelden zijn beschikbaar op de GitHub. Op mijn site kunt u meer interessante artikelen lezen.