Zoeken…


Invoering

Een Stream vertegenwoordigt een reeks elementen en ondersteunt verschillende soorten bewerkingen om berekeningen op die elementen uit te voeren. Met Java 8 heeft de Collection interface twee methoden om een Stream te genereren: stream() en parallelStream() . Stream zijn intermediair of terminal. Tussentijdse bewerkingen retourneren een Stream zodat meerdere tussentijdse bewerkingen kunnen worden gekoppeld voordat de Stream wordt gesloten. Terminalbewerkingen zijn ongeldig of geven een niet-streamresultaat terug.

Syntaxis

  • collection.stream ()
  • Arrays.stream (array)
  • Stream.iterate (firstValue, currentValue -> nextValue)
  • Stream.generate (() -> waarde)
  • Stream.of (elementOfT [, elementOfT, ...])
  • Stream.empty ()
  • StreamSupport.stream (iterable.spliterator (), false)

Streams gebruiken

Een Stream is een reeks elementen waarop opeenvolgende en parallelle aggregatiebewerkingen kunnen worden uitgevoerd. Elke gegeven Stream kan potentieel een onbeperkte hoeveelheid gegevens doorlopen. Als gevolg hiervan worden gegevens die zijn ontvangen van een Stream afzonderlijk verwerkt zodra deze binnenkomt, in tegenstelling tot het uitvoeren van batchverwerking op de gegevens helemaal. In combinatie met lambda-expressies bieden ze een beknopte manier om bewerkingen uit te voeren op gegevensreeksen met behulp van een functionele benadering.

Voorbeeld: ( zie het werken op Ideone )

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .forEach(System.out::println);

Output:

APPEL
BANAAN
ORANJE
PEER

De bewerkingen die worden uitgevoerd met de bovenstaande code kunnen als volgt worden samengevat:

  1. Een Stream<String> met een sequentiebepaling onderworpen geordende Stream fruit String elementen volgens de statische fabrieksmethode Stream.of(values) .

  2. De bewerking filter() behoudt alleen elementen die overeenkomen met een bepaald predicaat (de elementen die bij testen door het predicaat true retourneren). In dit geval behoudt het de elementen die een "a" . Het predicaat wordt gegeven als een lambda-uitdrukking .

  3. De bewerking map() transformeert elk element met behulp van een gegeven functie, een mapper genoemd. In dit geval, elke vrucht String is toegewezen aan de hoofd String versie met de werkwijze verwijzing String::toUppercase .

    Merk op dat de bewerking map() een stream met een ander generiek type retourneert als de toewijzingsfunctie een ander type retourneert dan de invoerparameter. Op een Stream<String> die bijvoorbeeld .map(String::isEmpty) wordt een Stream<Boolean> geretourneerd

  4. De bewerking Sort sorted() sorteert de elementen van de Stream volgens hun natuurlijke volgorde (lexicografisch, in het geval van String ).

  5. Ten slotte voert de forEach(action) bewerking een actie uit die op elk element van de Stream inwerkt en aan een Consument forEach(action) . In het voorbeeld wordt elk element eenvoudig afgedrukt op de console. Deze bewerking is een terminalbewerking, waardoor het onmogelijk wordt er opnieuw op te werken.

    Merk op dat bewerkingen die in de Stream zijn gedefinieerd, worden uitgevoerd vanwege de terminalbewerking. Zonder terminalbewerking wordt de stream niet verwerkt. Streams kunnen niet worden hergebruikt. Zodra een terminalbewerking wordt aangeroepen, wordt het Stream object onbruikbaar.

Geketende operaties

Bewerkingen (zoals hierboven te zien) zijn aan elkaar gekoppeld om te vormen wat kan worden gezien als een query op de gegevens.


Streams sluiten

Merk op dat een Stream algemeen niet hoeft te worden afgesloten. Het is alleen nodig om streams te sluiten die op IO-kanalen werken. De meeste Stream typen werken niet op bronnen en hoeven daarom niet te worden afgesloten.

De Stream interface breidt AutoCloseable . Stromen kunnen worden gesloten door de methode close aan te roepen of door try-with-resource-instructies te gebruiken.

Een voorbeeld van een geval waarin een Stream moet worden gesloten, is wanneer u een Stream met lijnen vanuit een bestand maakt:

try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
    lines.forEach(System.out::println);
}

De Stream interface geeft ook de methode Stream.onClose() waarmee u Runnable handlers kunt registreren die worden aangeroepen wanneer de stream wordt gesloten. Een voorbeeld van een use case is waar code die een stream produceert, moet weten wanneer deze wordt verbruikt om op te schonen.

public Stream<String>streamAndDelete(Path path) throws IOException {
    return Files.lines(path).onClose(() -> someClass.deletePath(path));
}

De run-handler wordt alleen uitgevoerd als de methode close() wordt aangeroepen, expliciet of impliciet door een instructie try-with-resources.


Bestelling verwerken

De verwerking van een Stream object kan sequentieel of parallel zijn .

In een sequentiële modus worden de elementen verwerkt in de volgorde van de bron van de Stream . Als de Stream is besteld (zoals een SortedMap implementatie of een List ), is de verwerking gegarandeerd gelijk aan de volgorde van de bron. In andere gevallen moet u er echter op letten dat u niet afhankelijk bent van de volgorde (zie: is de iteratievolgorde van Java HashMap keySet() consistent? ).

Voorbeeld:

List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42); 

// sequential 
long howManyOddNumbers = integerList.stream()
                                    .filter(e -> (e % 2) == 1)
                                    .count(); 

System.out.println(howManyOddNumbers); // Output: 2

Live op Ideone

In de parallelle modus kunnen meerdere threads op meerdere cores worden gebruikt, maar er is geen garantie voor de volgorde waarin elementen worden verwerkt.

Als meerdere methoden in een opeenvolgende Stream worden aangeroepen, hoeft niet elke methode te worden aangeroepen. Als een Stream wordt gefilterd en het aantal elementen wordt teruggebracht tot één, zal een volgende aanroep van een methode zoals sort niet plaatsvinden. Dit kan de prestaties van een sequentiële Stream - een optimalisatie die niet mogelijk is met een parallelle Stream .

Voorbeeld:

// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
                                            .filter(e -> (e % 2) == 1)
                                            .count();

System.out.println(howManyOddNumbersParallel); // Output: 2

Live op Ideone


Verschillen met containers (of verzamelingen )

Hoewel sommige acties op zowel containers als streams kunnen worden uitgevoerd, dienen ze uiteindelijk verschillende doelen en ondersteunen ze verschillende bewerkingen. Containers zijn meer gericht op hoe de elementen worden opgeslagen en hoe deze elementen efficiënt kunnen worden benaderd. Een Stream daarentegen biedt geen directe toegang tot en manipulatie van de elementen; het is meer toegewijd aan de groep objecten als een collectieve entiteit en verricht operaties aan die entiteit als geheel. Stream en Collection zijn afzonderlijke abstracties op hoog niveau voor deze verschillende doeleinden.

Verzamel elementen van een stroom in een verzameling

Verzamelen met toList() en toSet()

Elementen uit een Stream kunnen eenvoudig in een container worden verzameld met behulp van de Stream.collect bewerking:

System.out.println(Arrays
    .asList("apple", "banana", "pear", "kiwi", "orange")
    .stream()
    .filter(s -> s.contains("a"))
    .collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]

Andere verzamelinginstanties, zoals een Set , kunnen worden gemaakt met behulp van andere ingebouwde methoden van Collectors . Collectors.toSet() verzamelt bijvoorbeeld de elementen van een Stream in een Set .


Expliciete controle over de implementatie van List of Set

Volgens de documentatie van Collectors#toList() en Collectors#toSet() zijn er geen garanties voor het type, de veranderlijkheid, de serialiseerbaarheid of de threadveiligheid van de geretourneerde List of Set .

Voor expliciete controle over de te retourneren implementatie kan Collectors#toCollection(Supplier) worden gebruikt, waarbij de gegeven leverancier een nieuwe en lege verzameling retourneert.

// syntax with method reference
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(ArrayList::new))
);

// syntax with lambda
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);

Elementen verzamelen met toMap

Collector verzamelt elementen in een kaart, waarbij sleutel de student-ID is en waarde studentwaarde is.

  List<Student> students = new ArrayList<Student>(); 
    students.add(new Student(1,"test1"));
    students.add(new Student(2,"test2"));
    students.add(new Student(3,"test3"));
    
    Map<Integer, String> IdToName = students.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName));
    System.out.println(IdToName);

Uitgang:

{1=test1, 2=test2, 3=test3}

De Collectors.toMap heeft een andere implementatie Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) . De mergeFunction wordt meestal gebruikt om een nieuwe waarde te selecteren of om een oude waarde te behouden als de sleutel wordt herhaald bij het toevoegen van een nieuw lid in de kaart uit een lijst.

De mergeFunction ziet er vaak uit als: (s1, s2) -> s1 om de waarde te behouden die overeenkomt met de herhaalde sleutel, of (s1, s2) -> s2 om een nieuwe waarde voor de herhaalde sleutel in te voeren.

Elementen verzamelen om kaart van collecties

Voorbeeld: van ArrayList naar Map <String, List <>>

Vaak is het nodig om een kaart of lijst te maken van een primaire lijst. Voorbeeld: van een student van lijst moeten we een kaart maken van de lijst met onderwerpen voor elke student.

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
    list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
    list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
    list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));

    list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));

    Map<String, List<SUBJECT>> map = new HashMap<>();
    list.stream().forEach(s -> {
                map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
            });
    System.out.println(map);

Output:

{ Robert=[LITERATURE], 
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE], 
Davis=[MATH, SCIENCE, GEOGRAPHY] }

Voorbeeld: van ArrayList naar Map <String, Map <>>

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
    list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
    list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));

    list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));

    Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();

    list.stream().forEach(student -> {
        map.computeIfAbsent(student.getName(), s -> new HashMap<>())
                .computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
                .add(student.getMarks());
    });

    System.out.println(map);

Output:

{ Robert={ENGLISH=[12.0]}, 
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]}, 
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }

Spiek briefje

Doel Code
Verzamelen naar een List Collectors.toList()
Verzamel op een ArrayList met vooraf toegewezen grootte Collectors.toCollection(() -> new ArrayList<>(size))
Verzamel naar een Set Collectors.toSet()
Verzamel naar een Set met betere iteratieprestaties Collectors.toCollection(() -> new LinkedHashSet<>())
Verzamel voor een hoofdlettergevoelige Set<String> Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))
Verzamelen naar een EnumSet<AnEnum> (beste prestaties voor enums) Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class))
Verzamel op een Map<K,V> met unieke sleutels Collectors.toMap(keyFunc,valFunc)
Wijs MyObject.getter () toe aan unieke MyObject Collectors.toMap(MyObject::getter, Function.identity())
Wijs MyObject.getter () toe aan meerdere MyObjects Collectors.groupingBy(MyObject::getter)

Oneindige stromen

Het is mogelijk om een Stream te genereren die niet eindigt. Door een eindmethode op een oneindige Stream roepen, gaat de Stream een oneindige lus binnen. De limit van een Stream kan worden gebruikt om het aantal termen van de Stream te beperken dat Java verwerkt.

Dit voorbeeld genereert een Stream van alle natuurlijke getallen, beginnend met het cijfer 1. Elke opeenvolgende termijn van de Stream is er één hoger dan de vorige. Door de limietmethode van deze Stream roepen, worden alleen de eerste vijf termen van de Stream en afgedrukt.

// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);

// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);

Output:

1
2
3
4
5


Een andere manier om een oneindige stroom te genereren, is met behulp van de methode Stream.generate . Deze methode vereist een lambda van het type Leverancier .

// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);

// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);

Stromen consumeren

Een Stream wordt alleen doorlopen als er een terminalbewerking is , zoals count() , collect() of forEach() . Anders wordt er geen bewerking op de Stream uitgevoerd.

In het volgende voorbeeld wordt geen terminalbewerking aan de Stream toegevoegd, dus de bewerking filter() wordt niet aangeroepen en er wordt geen uitvoer geproduceerd omdat peek() GEEN terminalbewerking is .

IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);

Live op Ideone

Dit is een Stream reeks met een geldige terminalbewerking , dus er wordt een uitvoer geproduceerd.

Je kunt ook forEach gebruiken forEach plaats van peek :

IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println); 

Live op Ideone

Output:

2
4
6
8

Nadat de terminalbewerking is uitgevoerd, is de Stream verbruikt en kan deze niet opnieuw worden gebruikt.


Hoewel een bepaald stream-object niet kan worden hergebruikt, is het eenvoudig om een herbruikbare Iterable die delegeert naar een stream-pipeline. Dit kan handig zijn voor het retourneren van een gewijzigde weergave van een live gegevensset zonder resultaten te hoeven verzamelen in een tijdelijke structuur.

List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();

for (String str : iterable) {
    System.out.println(str);
}
for (String str : iterable) {
    System.out.println(str);
}

Output:

foo
bar
foo
bar

Dit werkt omdat Iterable een enkele abstracte methode Iterator<T> iterator() declareert. Dat maakt het effectief een functionele interface, geïmplementeerd door een lambda die bij elke oproep een nieuwe stream creëert.


Over het algemeen werkt een Stream zoals weergegeven in de volgende afbeelding:

Stream operatie

OPMERKING : Argumentencontroles worden altijd uitgevoerd, zelfs zonder een terminalbewerking :

try {
    IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
    System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}

Live op Ideone

Output:

We hebben een NullPointerException gekregen omdat null is doorgegeven als een argument om te filteren ()

Een frequentiekaart maken

Met de groupingBy(classifier, downstream) verzamelaar kunnen Stream elementen in een Map door elk element in een groep te classificeren en een stroomafwaartse bewerking uit te voeren op de elementen die in dezelfde groep zijn geclassificeerd.

Een klassiek voorbeeld van dit principe is om een Map te gebruiken om het aantal elementen in een Stream te tellen. In dit voorbeeld is de classificator gewoon de identiteitsfunctie, die het element retourneert zoals het is. De stroomafwaartse bewerking telt het aantal gelijke elementen met behulp van counting() .

Stream.of("apple", "orange", "banana", "apple")
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
      .entrySet()
      .forEach(System.out::println);

De downstream-bewerking is zelf een collector ( Collectors.counting() ) die werkt op elementen van het type String en een resultaat van het type Long produceert. Het resultaat van de collect aanroep is een Map<String, Long> .

Dit zou de volgende uitvoer opleveren:

banana = 1
orange = 1
appel = 2

Parallelle stroom

Opmerking: bekijk voordat u beslist welke Stream wilt gebruiken het gedrag van ParallelStream versus Sequential Stream .

Als u Stream bewerkingen tegelijkertijd wilt uitvoeren, kunt u een van deze manieren gebruiken.

List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();

Of:

Stream<String> aParallelStream = data.parallelStream();

Neem contact op met een terminaloperator om de bewerkingen uit te voeren die zijn gedefinieerd voor de parallelle stream:

aParallelStream.forEach(System.out::println);

(Een mogelijke) uitvoer van de parallelle Stream :

Drie
vier
een
Twee
Vijf

De volgorde kan veranderen als alle elementen worden verwerkt in parallel (wat sneller kunnen maken). Gebruik parallelStream wanneer bestellen niet uitmaakt.

Prestatie-impact

Indien een netwerk gaat, parallel Stream en kan de algehele prestatie van een toepassing omdat alle parallelle afgebroken Stream en gebruik een gemeenschappelijke fork-join threadgroep voor het netwerk.

Aan de andere kant kunnen parallelle Stream de prestaties in veel andere gevallen aanzienlijk verbeteren, afhankelijk van het aantal beschikbare cores in de actieve CPU op dit moment.

Een optionele stroom omzetten in een stroom waarden

Mogelijk moet u een Stream emitterende Optional omzetten in een Stream van waarden, waarbij alleen waarden van bestaande Optional . (dat wil zeggen: zonder null waarde en niet omgaan met Optional.empty() ).

 Optional<String> op1 = Optional.empty();
 Optional<String> op2 = Optional.of("Hello World");

 List<String> result = Stream.of(op1, op2)
                             .filter(Optional::isPresent)
                             .map(Optional::get)
                             .collect(Collectors.toList());

 System.out.println(result); //[Hello World]

Een stream maken

Alle java Collection<E> s hebben stream() en parallelStream() methoden waaruit een Stream<E> kan worden geconstrueerd:

Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();

Een Stream<E> kan op basis van een van de volgende twee methoden worden gemaakt:

String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);

Het verschil tussen Arrays.stream() en Stream.of() is dat Stream.of() een parameter varargs heeft, zodat deze kan worden gebruikt als:

Stream<Integer> integerStream = Stream.of(1, 2, 3);

Er zijn ook primitieve Stream die je kunt gebruiken. Bijvoorbeeld:

IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);

Deze primitieve streams kunnen ook worden geconstrueerd met de methode Arrays.stream() :

IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });

Het is mogelijk om een Stream te maken van een array met een opgegeven bereik.

int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);

Merk op dat elke primitieve stream kan worden geconverteerd naar een boxed stream met behulp van de boxed methode:

Stream<Integer> integerStream = intStream.boxed();

Dit kan in sommige gevallen handig zijn als u de gegevens wilt verzamelen, omdat de primitieve stroom geen collect die een Collector als argument neemt.

Hergebruik van tussenliggende bewerkingen van een stroomketen

Stream wordt gesloten wanneer ooit terminalbedrijf wordt aangeroepen. Hergebruik van de stroom van tussenliggende bewerkingen, terwijl alleen de terminalwerking varieert. we kunnen een streamleverancier creëren om een nieuwe stream te bouwen met alle tussenliggende activiteiten die al zijn opgezet.

Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();

  streamSupplier.get().filter(s ->   s.startsWith("A")).forEach(System.out::println);

// APPLE

  streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);

  // BANANA
  // BLACKBERRY
  // BLUEBERRY

int[] arrays kunnen worden geconverteerd naar List<Integer> met behulp van streams

int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());

Statistieken zoeken over numerieke stromen

Java 8 biedt klassen met de naam IntSummaryStatistics , DoubleSummaryStatistics en LongSummaryStatistics die een LongSummaryStatistics geven voor het verzamelen van statistieken zoals count , min , max , sum en average .

Java SE 8
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
                                           .mapToInt((x) -> x)
                                           .summaryStatistics();
System.out.println(stats);

Wat zal resulteren in:

Java SE 8
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}

Download een stukje stroom

Voorbeeld: verkrijg een Stream van 30 elementen, met een 21ste tot en met 50ste (inclusief) element van een verzameling.

final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);

Opmerkingen:

  • IllegalArgumentException wordt gegenereerd als n negatief is of maxSize negatief is
  • zowel skip(long) als limit(long) zijn tussenliggende bewerkingen
  • als een stream minder dan n elementen bevat, geeft skip(n) een lege stream terug
  • zowel skip(long) als limit(long) zijn goedkope bewerkingen op opeenvolgende stroompijpleidingen, maar kunnen best duur zijn op bestelde parallelle pijpleidingen

Stromen samenvoegen

Variabele verklaring voor voorbeelden:

Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");

Voorbeeld 1 - Voeg twee Stream

final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());

concat1.forEach(System.out::print);
// prints: abc123

Voorbeeld 2 - Meer dan twee Stream samenvoegen

final Stream<String> concat2 = Stream.concat(
    Stream.concat(abc.stream(), digits.stream()),
    greekAbc.stream());

System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Als alternatief om de geneste syntaxis van concat() te vereenvoudigen, kunnen de Stream s ook worden samengevoegd met flatMap() :

final Stream<String> concat3 = Stream.of(
    abc.stream(), digits.stream(), greekAbc.stream())
    .flatMap(s -> s);
    // or `.flatMap(Function.identity());` (java.util.function.Function)

System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Wees voorzichtig bij het samenstellen van Stream uit herhaalde aaneenschakeling, omdat toegang tot een element van een sterk aaneengeschakelde Stream kan resulteren in diepe oproepketens of zelfs een StackOverflowException .

IntStream naar String

Java heeft geen Char Stream , dus wanneer u werkt met String s en een Stream of Character s construeert, is een optie om een IntStream van codepunten te krijgen met de methode String.codePoints() . Dus IntStream kan als volgt worden verkregen:

public IntStream stringToIntStream(String in) {
  return in.codePoints();
}

Het is iets ingewikkelder om de conversie andersom te doen, bijvoorbeeld IntStreamToString. Dat kan als volgt worden gedaan:

public String intStreamToString(IntStream intStream) {
  return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}

Sorteren met stream

List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");

System.out.println(data);

List<String> sortedData = data.stream().sorted().collect(Collectors.toList());

System.out.println(sortedData);

Output:

[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]

Het is ook mogelijk om een ander vergelijkingsmechanisme te gebruiken, omdat er een overbelaste sorted versie is die een comparator als argument heeft.

U kunt ook een lambda-expressie gebruiken om te sorteren:

List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());

Dit levert [Sydney, New York, Mumbai, London, California, Amsterdam]

U kunt Comparator.reverseOrder() om een comparator te hebben die het reverse van de natuurlijke volgorde oplegt.

List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

Stromen van primitieven

Java biedt gespecialiseerde Stream s voor drie soorten primitieven IntStream (voor int s), LongStream (voor long s) en DoubleStream (voor double s). Naast geoptimaliseerde implementaties voor hun respectieve primitieven, bieden ze ook verschillende specifieke eindmethoden, meestal voor wiskundige bewerkingen. bv:

IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0

Verzamel resultaten van een stream in een array

Analoog om een verzameling voor een Stream door collect() een array worden verkregen met de methode Stream.toArray() :

List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");

String[] filteredFruits = fruits.stream()
    .filter(s -> s.contains("a"))
    .toArray(String[]::new);     

// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));

String[]::new is een speciaal soort methode-referentie: een constructor-referentie.

Het eerste element vinden dat overeenkomt met een predicaat

Het is mogelijk om het eerste element van een Stream dat overeenkomt met een voorwaarde.

In dit voorbeeld vinden we het eerste Integer waarvan het vierkant meer dan 50000 .

IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
    .filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
    .findFirst(); // Find the first filtered element

Deze uitdrukking retourneert een OptionalInt met het resultaat.

Merk op dat Java met een oneindige Stream elk element blijft controleren totdat het een resultaat vindt. Met een eindige Stream , als Java geen elementen meer heeft, maar nog steeds geen resultaat kan vinden, retourneert het een lege OptionalInt .

IntStream gebruiken om indexen te herhalen

Stream van elementen geeft meestal geen toegang tot de indexwaarde van het huidige item. Gebruik IntStream.range om een array of ArrayList te herhalen terwijl u toegang hebt tot indexen IntStream.range(start, endExclusive) .

String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };

IntStream.range(0, names.length)
    .mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
    .forEach(System.out::println);

De methode range(start, endExclusive) retourneert een andere ÌntStream en de mapToObj(mapper) retourneert een stroom String .

Output:

# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc

Dit lijkt erg op het gebruik van een normale for lus met een teller, maar met het voordeel van pipelining en parallellisatie:

for (int i = 0; i < names.length; i++) {
    String newName = String.format("#%d %s", i + 1, names[i]);
    System.out.println(newName);
}

Streams afvlakken met flatMap ()

Een Stream items die op hun beurt streambaar zijn, kan worden samengevoegd tot een enkele continue Stream :

Matrix van lijst met items kan worden omgezet in een enkele lijst.

List<String> list1 =  Arrays.asList("one", "two");
      List<String> list2 =  Arrays.asList("three","four","five");
      List<String> list3 =  Arrays.asList("six");
          List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);

// [one, two, three, four, five, six]

Kaart met Lijst met items als waarden kan worden afgevlakt tot een gecombineerde lijst

Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));

List<Integer> allValues = map.values() // Collection<List<Integer>>
        .stream()                      // Stream<List<Integer>>
        .flatMap(List::stream)         // Stream<Integer>
        .collect(Collectors.toList());

System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]

List met Map kan worden samengevoegd tot een enkele continue Stream

List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");

Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);


Set<String> output= list.stream()  //  Stream<Map<String, String>>
    .map(Map::values)              // Stream<List<String>>
    .flatMap(Collection::stream)   // Stream<String>
    .collect(Collectors.toSet());  //Set<String>
// [one, two, three,four]

Maak een kaart op basis van een stream

Eenvoudig hoesje zonder dubbele sleutels

Stream<String> characters = Stream.of("A", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}

Om dingen meer declaratief te maken, kunnen we de statische methode gebruiken in Function interface - Function.identity() . We kunnen dit lambda- element -> element door Function.identity() .

Geval waarin er dubbele sleutels zijn

De javadoc voor Collectors.toMap stelt:

Als de toegewezen sleutels duplicaten bevatten (volgens Object.equals(Object) ), wordt een IllegalStateException gegenereerd wanneer de verzamelbewerking wordt uitgevoerd. Als de toegewezen toetsen mogelijk duplicaten bevatten, gebruikt u in plaats daarvan toMap(Function, Function, BinaryOperator) .

Stream<String> characters = Stream.of("A", "B", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(
                element -> element.hashCode(),
                element -> element,
                (existingVal, newVal) -> (existingVal + newVal)));

// map = {65=A, 66=BB, 67=C}

De BinaryOperator doorgegeven aan Collectors.toMap(...) genereert de waarde die moet worden opgeslagen in het geval van een botsing. Het kan:

  • retourneer de oude waarde, zodat de eerste waarde in de stream voorrang heeft,
  • retourneer de nieuwe waarde, zodat de laatste waarde in de stream voorrang heeft, of
  • combineer de oude en nieuwe waarden

Groeperen op waarde

U kunt Collectors.groupingBy wanneer u het equivalent van een "groep per" -bewerking in een database moet uitvoeren. Ter illustratie maakt het volgende een kaart waarin de namen van mensen worden toegewezen aan achternamen:

List<Person> people = Arrays.asList(
    new Person("Sam", "Rossi"),
    new Person("Sam", "Verdi"),
    new Person("John", "Bianchi"),
    new Person("John", "Rossi"),
    new Person("John", "Verdi")
);

Map<String, List<String>> map = people.stream()
        .collect(
                // function mapping input elements to keys
                Collectors.groupingBy(Person::getName, 
                // function mapping input elements to values,
                // how to store values
                Collectors.mapping(Person::getSurname, Collectors.toList()))
        );

// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}

Live op Ideone

Willekeurige tekenreeksen genereren met behulp van stromen

Het is soms handig om willekeurige Strings te maken, misschien als sessie-ID voor een webservice of een eerste wachtwoord na registratie voor een toepassing. Dit kan eenvoudig worden bereikt met behulp van Stream s.

Eerst moeten we een random number-generator initialiseren. Om de beveiliging voor de gegenereerde String s te verbeteren, is het een goed idee om SecureRandom te gebruiken.

Opmerking : het maken van een SecureRandom is vrij duur, dus het is een goede gewoonte om dit slechts eenmaal te doen en af en toe een van de methoden setSeed() aan te roepen om het opnieuw in te voeren.

private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20)); 
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example

Bij het maken van willekeurige String willen we meestal dat ze alleen bepaalde tekens gebruiken (bijvoorbeeld alleen letters en cijfers). Daarom kunnen we een methode maken die een boolean retourneert en die later kan worden gebruikt om de Stream te filteren.

//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
    //check for range to avoid using all unicode Letter (e.g. some chinese symbols)
    return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}

Vervolgens kunnen we de RNG gebruiken om een willekeurige tekenreeks met een specifieke lengte te genereren die de useThisCharacter bevat die useThisCharacter aan onze useThisCharacter controle.

public String generateRandomString(long length){
    //Since there is no native CharStream, we use an IntStream instead
    //and convert it to a Stream<Character> using mapToObj.
    //We need to specify the boundaries for the int values to ensure they can safely be cast to char
    Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);

    //now we can use this Stream to build a String utilizing the collect method.
    String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
    return randomString;
}

Streams gebruiken om wiskundige functies te implementeren

Stream s, en in het bijzonder IntStream s, zijn een elegante manier om sommatie-termen (∑) te implementeren. De bereiken van de Stream kunnen worden gebruikt als de grenzen van de sommatie.

Bijvoorbeeld, Madhava's benadering van Pi wordt gegeven door de formule (Bron: wikipedia ): Madhava's benadering

Dit kan met een willekeurige precisie worden berekend. Bijvoorbeeld voor 101 termen:

double pi = Math.sqrt(12) * 
            IntStream.rangeClosed(0, 100)
                     .mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
                     .sum();

Opmerking: Met double precisie is het selecteren van een bovengrens van 29 voldoende om een resultaat te krijgen dat niet van Math.Pi onderscheiden is.

Streams en methode-referenties gebruiken om zelfdocumentatieprocessen te schrijven

Methodeverwijzingen zijn uitstekende zelfdocumenterende code en het gebruik van methodeverwijzingen met Stream s maakt gecompliceerde processen eenvoudig te lezen en te begrijpen. Overweeg de volgende code:

public interface Ordered {
    default int getOrder(){
        return 0;
    }
}

public interface Valued<V extends Ordered> {
    boolean hasPropertyTwo();
    V getValue();
}

public interface Thing<V extends Ordered> {
    boolean hasPropertyOne();
    Valued<V> getValuedProperty();
}

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    List<V> results = new ArrayList<V>();
    for (Thing<V> thing : things) {
        if (thing.hasPropertyOne()) {
            Valued<V> valued = thing.getValuedProperty();
            if (valued != null && valued.hasPropertyTwo()){
                V value = valued.getValue();
                if (value != null){
                    results.add(value);
                }
            }
        }
    }
    results.sort((a, b)->{
        return Integer.compare(a.getOrder(), b.getOrder());
    });
    return results;
}

Deze laatste methode herschreven met behulp van Stream s en methodeverwijzingen is veel leesbaarder en elke stap van het proces is snel en gemakkelijk te begrijpen - het is niet alleen korter, het toont ook in één oogopslag welke interfaces en klassen verantwoordelijk zijn voor de code in elke stap:

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    return things.stream()
        .filter(Thing::hasPropertyOne)
        .map(Thing::getValuedProperty)
        .filter(Objects::nonNull)
        .filter(Valued::hasPropertyTwo)
        .map(Valued::getValue)
        .filter(Objects::nonNull)
        .sorted(Comparator.comparing(Ordered::getOrder))
        .collect(Collectors.toList());
}    

Streams of Map gebruiken. Beginwaarden na het toewijzen behouden

Als u een Stream hebt die u moet toewijzen, maar ook de beginwaarden wilt behouden, kunt u de Stream Map.Entry<K,V> aan een Map.Entry<K,V> met een hulpprogramma zoals de volgende:

public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
    return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}

Dan kunt u uw converter gebruiken om te verwerken Stream s die toegang hebben tot zowel de originele en in kaart gebracht waarden:

Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
    .map(entryMapper(transformer));

U kunt die Stream vervolgens gewoon blijven verwerken. Dit voorkomt de overhead van het maken van een tussenliggende verzameling.

Stream bewerkingscategorieën

Streamoperaties vallen uiteen in twee hoofdcategorieën, tussenliggende en terminalactiviteiten, en twee subcategorieën, stateless en stateful.


Tussentijdse operaties:

Een tussenliggende bewerking is altijd lui , zoals een eenvoudige Stream.map . Het wordt pas aangeroepen als de stream daadwerkelijk is verbruikt. Dit kan eenvoudig worden geverifieerd:

Arrays.asList(1, 2 ,3).stream().map(i -> {
    throw new RuntimeException("not gonna happen");
    return i;
});

Tussentijdse bewerkingen zijn de gebruikelijke bouwstenen van een stroom, geketend na de bron en worden meestal gevolgd door een terminalbewerking die de stroomketen in gang zet.


Terminalactiviteiten

Terminalbewerkingen leiden tot het verbruik van een stream. Enkele van de meest voorkomende zijn Stream.forEach of Stream.collect . Ze worden meestal geplaatst na een reeks tussenliggende bewerkingen en zijn bijna altijd enthousiast .


Staatloze operaties

Staatloosheid betekent dat elk item wordt verwerkt zonder de context van andere items. Staatloze bewerkingen zorgen voor geheugen-efficiënte verwerking van streams. Bewerkingen zoals Stream.map en Stream.filter waarvoor geen informatie over andere items van de stream nodig is, worden als Stream.map beschouwd.


Stateful operaties

Statefulness betekent dat de bewerking van elk item afhankelijk is van (sommige) andere items van de stream. Dit vereist dat een staat behouden blijft. Statefulness-bewerkingen kunnen breken met lange of oneindige streams. Bewerkingen zoals Stream.sorted vereisen dat de hele stream wordt verwerkt voordat een item wordt uitgezonden, wat een voldoende lange stroom items zal verbreken. Dit kan worden aangetoond door een lange stroom ( op eigen risico ):

// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);

Dit veroorzaakt geheugengebrek vanwege de Stream.sorted van Stream.sorted :

// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);

Een iterator converteren naar een stream

Gebruik Spliterators.spliterator() of Spliterators.spliteratorUnknownSize() om een iterator naar een stream te converteren:

Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();    
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);

Reductie met beken

Reductie is het toepassen van een binaire operator op elk element van een stream om te resulteren in één waarde.

De methode sum() van een IntStream is een voorbeeld van een reductie; het is van toepassing op elke term van de Stream, wat resulteert in één definitieve waarde: Somvermindering

Dit is gelijk aan (((1+2)+3)+4)

Met de reduce van een stream kunt u een aangepaste reductie maken. Het is mogelijk om de reduce te gebruiken reduce methode sum() te implementeren:

IntStream istr;
    
//Initialize istr
    
OptionalInt istr.reduce((a,b)->a+b);

De Optional versie wordt geretourneerd zodat lege streams op de juiste manier kunnen worden verwerkt.

Een ander voorbeeld van reductie is het combineren van een Stream<LinkedList<T>> in een enkele LinkedList<T> :

Stream<LinkedList<T>> listStream;
    
//Create a Stream<LinkedList<T>>
    
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

U kunt ook een identiteitselement opgeven . Het identiteitselement voor optelling is bijvoorbeeld 0, als x+0==x . Voor vermenigvuldiging is het identiteitselement 1, als x*1==x . In het bovenstaande geval is het identiteitselement een lege LinkedList<T> , omdat als u een lege lijst toevoegt aan een andere lijst, de lijst waaraan u "toevoegt" niet verandert:

Stream<LinkedList<T>> listStream;

//Create a Stream<LinkedList<T>>

LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

Merk op dat wanneer een identiteitselement wordt opgegeven, de retourwaarde niet is ingepakt in een Optional —Als een lege stream wordt aangeroepen, zal reduce() het identiteitselement retourneren.

De binaire operator moet ook associatief zijn , wat betekent dat (a+b)+c==a+(b+c) . Dit komt omdat de elementen in elke volgorde kunnen worden verminderd. De bovenstaande toevoegingreductie kan bijvoorbeeld als volgt worden uitgevoerd:

Overige somvermindering

Deze vermindering komt overeen met schrijven ((1+2)+(3+4)) . De eigenschap van associativiteit stelt Java ook in staat om de Stream parallel te verkleinen - een deel van de Stream kan worden gereduceerd door elke processor, met een reductie die het resultaat van elke processor combineert aan het einde.

Een stream samenvoegen tot één string

Een use case die veel voorkomt, is het creëren van een String uit een stream, waarbij de stream-items worden gescheiden door een bepaald teken. De methode Collectors.joining() kan hiervoor worden gebruikt, zoals in het volgende voorbeeld:

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

String result = fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", "));
           
System.out.println(result);

Output:

APPEL, BANANA, SINAASAPPEL, PEER

De methode Collectors.joining() kan ook zorgen voor pre- en postfixes:

String result = fruitStream.filter(s -> s.contains("e"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", ", "Fruits: ", "."));
           
System.out.println(result);

Output:

Fruit: APPEL, SINAASAPPEL, PEER.

Live op Ideone



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow