Java Language
streams
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:
Een
Stream<String>
met een sequentiebepaling onderworpen geordendeStream
fruitString
elementen volgens de statische fabrieksmethodeStream.of(values)
.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 .De bewerking
map()
transformeert elk element met behulp van een gegeven functie, een mapper genoemd. In dit geval, elke vruchtString
is toegewezen aan de hoofdString
versie met de werkwijze verwijzingString::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 eenStream<String>
die bijvoorbeeld.map(String::isEmpty)
wordt eenStream<Boolean>
geretourneerdDe bewerking Sort
sorted()
sorteert de elementen van deStream
volgens hun natuurlijke volgorde (lexicografisch, in het geval vanString
).Ten slotte voert de
forEach(action)
bewerking een actie uit die op elk element van deStream
inwerkt en aan een ConsumentforEach(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 hetStream
object onbruikbaar.
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 meesteStream
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
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
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);
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);
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:
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()");
}
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
.
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:
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 alsn
negatief is ofmaxSize
negatief is - zowel
skip(long)
alslimit(long)
zijn tussenliggende bewerkingen - als een stream minder dan
n
elementen bevat, geeftskip(n)
een lege stream terug - zowel
skip(long)
alslimit(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 eenIllegalStateException
gegenereerd wanneer de verzamelbewerking wordt uitgevoerd. Als de toegewezen toetsen mogelijk duplicaten bevatten, gebruikt u in plaats daarvantoMap(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]}
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 ):
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:
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:
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.