Java Language
Strömmar
Sök…
Introduktion
En Stream
representerar en sekvens av element och stöder olika slags operationer för att utföra beräkningar på dessa element. Med Java 8 har Collection
gränssnittet två metoder för att generera en Stream
: stream()
och parallelStream()
. Stream
är antingen mellanliggande eller terminal. Mellanoperationer returnerar en Stream
så att flera mellanoperationer kan kedjas innan Stream
stängs. Terminaloperationer är antingen ogiltiga eller returnerar ett icke-strömresultat.
Syntax
- collection.stream ()
- Arrays.stream (matris)
- Stream.iterate (firstValue, currentValue -> nextValue)
- Stream.generate (() -> värde)
- Stream.of (elementOfT [, elementOfT, ...])
- Stream.empty ()
- StreamSupport.stream (iterable.spliterator (), false)
Använda strömmar
En Stream
är en sekvens av element på vilka sekventiella och parallella aggregerade operationer kan utföras. Varje given Stream
kan potentiellt ha en obegränsad mängd data som strömmar genom den. Som ett resultat behandlas data som mottas från en Stream
individuellt när de anländer, i motsats till att utföra batchbehandling på data helt. I kombination med lambda-uttryck ger de ett kortfattat sätt att utföra operationer på datasekvenser med en funktionell strategi.
Exempel: ( se det fungerar på 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);
Produktion:
ÄPPLE
BANAN
ORANGE
PÄRON
Operationerna som utförs med ovanstående kod kan sammanfattas enligt följande:
Skapa en
Stream<String>
innehåller en sekvensbeställdStream
avString
med den statiska fabriksmetodenStream.of(values)
.filter()
behåller endast element som matchar ett givet predikat (elementen som testas av predikatet returnerar sant). I det här fallet behåller det elementen som innehåller ett"a"
. Predikatet ges som ett lambda-uttryck .map()
-operationen omvandlar varje element med en given funktion, kallad en mapper. I detta fall, varje fruktString
avbildas till dess versalerString
version med metoden-referensString::toUppercase
.Observera att
map()
-operationen kommer att returnera en ström med en annan generisk typ om mappningsfunktionen returnerar en typ som skiljer sig från sin ingångsparameter. Till exempel på enStream<String>
.map(String::isEmpty)
returnerar enStream<Boolean>
Den
sorted()
operationen sorterar elementen iStream
enligt deras naturliga ordning (leksikografiskt, i fallet medString
).Slutligen
forEach(action)
en åtgärd som verkar på varje element iStream
och överför den till en konsument . I exemplet skrivs varje element helt enkelt ut på konsolen. Denna operation är en terminaloperation, vilket gör det omöjligt att arbeta på den igen.Observera att operationer definierade på
Stream
utförs på grund av terminaloperationen. Utan en terminaloperation bearbetas inte strömmen. Strömmar kan inte återanvändas. När en terminaloperation har kallats, blirStream
objektet oanvändbart.
Verksamheten (som ses ovan) är kedjade ihop för att bilda vad som kan ses som en fråga på data.
Stäng strömmar
Observera att en
Stream
allmänhet inte behöver stängas. Det krävs endast att stänga strömmar som fungerar på IO-kanaler. De flestaStream
typer fungerar inte på resurser och därför inte kräver stängning.
Stream
gränssnittet utökar AutoCloseable
. Strömmar kan stängas genom att ringa close
eller genom att använda försök med resursuttalanden.
Ett exempel på ett användningsfall där en Stream
bör stängas är när du skapar en Stream
av rader från en fil:
try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
lines.forEach(System.out::println);
}
Stream
gränssnittet förklarar också Stream.onClose()
som låter dig registrera Runnable
hanterare som kommer att Runnable
när strömmen är stängd. Ett exempel på användningsfall är där kod som producerar en ström måste veta när den konsumeras för att utföra någon sanering.
public Stream<String>streamAndDelete(Path path) throws IOException {
return Files.lines(path).onClose(() -> someClass.deletePath(path));
}
Körhanteraren körs endast om metoden close()
kallas, antingen uttryckligen eller implicit av ett försök med resurser.
Behandlar order
En Stream
objektets bearbetning kan vara sekventiell eller parallell .
I en sekventiell mod, är elementen behandlas i den ordning av källan av den Stream
. Om Stream
beställs (t.ex. en SortedMap
implementering eller en List
) garanteras bearbetningen matchning av källans beställning. I andra fall bör man dock vara försiktig så att man inte beror på beställningen (se: är Java HashMap
keySet()
iterationsordning beständig? ).
Exempel:
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
Parallellt läge tillåter användning av flera trådar på flera kärnor men det finns ingen garanti för ordningen i vilken element behandlas.
Om flera metoder anropas i en sekventiell Stream
måste inte alla metoder åberopas. Till exempel, om en Stream
filtreras och antalet element reduceras till ett, kommer ett efterföljande samtal till en metod som sort
inte att inträffa. Detta kan öka prestandan för en sekvensiell Stream
- en optimering som inte är möjlig med en parallell Stream
.
Exempel:
// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbersParallel); // Output: 2
Skillnader från behållare (eller samlingar )
Medan vissa åtgärder kan utföras på både behållare och strömmar, tjänar de i slutändan olika syften och stöder olika operationer. Behållare är mer fokuserade på hur elementen lagras och hur dessa element kan nås effektivt. En Stream
å andra sidan ger inte direkt tillgång och manipulation till dess element; det är mer dedikerat till gruppen av objekt som en kollektiv enhet och utför operationer på den enheten som helhet. Stream
och Collection
är separata abstraktioner på hög nivå för dessa olika syften.
Samla element i en ström i en samling
Samla med toList()
och toSet()
Element från en Stream
kan enkelt samlas in i en behållare med hjälp av Stream.collect
:
System.out.println(Arrays
.asList("apple", "banana", "pear", "kiwi", "orange")
.stream()
.filter(s -> s.contains("a"))
.collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]
Andra samlingsinstanser, som en Set
, kan göras med hjälp av andra inbyggda metoder för Collectors
. Till exempel samlar Collectors.toSet()
elementen i en Stream
i en Set
.
Explicit kontroll över implementeringen av List
eller Set
Enligt dokumentation av Collectors#toList()
och Collectors#toSet()
finns det inga garantier för typ, mutabilitet, serierbarhet eller gängsäkerhet för List
eller Set
returneras.
För uttrycklig kontroll över implementeringen som ska returneras kan Collectors#toCollection(Supplier)
användas istället, där den givna leverantören returnerar en ny och tom samling.
// 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<>()))
);
Samla element med toMap
Collector samlar element i en karta, där nyckeln är student-id och värde är studentvärde.
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);
Utgång:
{1=test1, 2=test2, 3=test3}
Collectors.toMap har en annan implementering Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
. MergeFunction används oftast för att välja antingen nytt värde eller behålla gammalt värde om tangenten upprepas när du lägger till en ny medlem i kartan från en lista.
MergeFunction ser ofta ut: (s1, s2) -> s1
att behålla värde som motsvarar den upprepade tangenten, eller (s1, s2) -> s2
att sätta nytt värde för den upprepade tangenten.
Samla element till karta över samlingar
Exempel: från ArrayList till karta <String, List <>>
Ofta kräver det att göra en karta över listan ur en primärlista. Exempel: Från en studentlista måste vi göra en karta över listan med ämnen för varje 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);
Produktion:
{ Robert=[LITERATURE],
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE],
Davis=[MATH, SCIENCE, GEOGRAPHY] }
Exempel: från ArrayList till karta <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);
Produktion:
{ 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]} }
Fusklapp
Mål | Koda |
---|---|
Samla till en List | Collectors.toList() |
Samla till en ArrayList med fördelad storlek | Collectors.toCollection(() -> new ArrayList<>(size)) |
Samla till en Set | Collectors.toSet() |
Samla till en Set med bättre iterationsprestanda | Collectors.toCollection(() -> new LinkedHashSet<>()) |
Samla till en okänslig Set<String> | Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)) |
Samla till en EnumSet<AnEnum> (bästa prestanda för enums) | Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class)) |
Samla till en Map<K,V> med unika nycklar | Collectors.toMap(keyFunc,valFunc) |
Karta MyObject.getter () till unika MyObject | Collectors.toMap(MyObject::getter, Function.identity()) |
Karta MyObject.getter () till flera MyObjects | Collectors.groupingBy(MyObject::getter) |
Oändliga strömmar
Det är möjligt att generera en Stream
som inte slutar. Att ringa en terminalmetod i en oändlig Stream
gör att Stream
in i en oändlig slinga. limit
för en Stream
kan användas för att begränsa antalet termer för den Stream
som Java bearbetar.
Det här exemplet genererar en Stream
av alla naturliga nummer, börjar med siffran 1. Varje på varandra följande term i Stream
är en högre än föregående. Genom att ringa gränsmetoden för denna Stream
beaktas och skrivs endast de första fem termerna i Stream
.
// 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);
Produktion:
1
2
3
4
5
Ett annat sätt att generera en oändlig ström är att använda metoden Stream.generate . Denna metod tar en lambda av typen Leverantör .
// 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);
Konsumerar strömmar
En Stream
kommer bara att korsas när det finns en terminaloperation , som count()
, collect()
eller för forEach()
. Annars kommer ingen operation på Stream
att utföras.
I följande exempel läggs ingen terminaloperation till Stream
, så filter()
-operationen kommer inte att åberopas och ingen utgång kommer att produceras eftersom peek()
INTE är en terminaloperation .
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
Detta är en Stream
sekvens med en giltig terminaloperation , så att en utgång produceras.
Du kan också använda för forEach
istället för att peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Produktion:
2
4
6
8
När terminaloperationen har utförts konsumeras Stream
och kan inte återanvändas.
Även om ett visst strömobjekt inte kan återanvändas, är det enkelt att skapa en återanvändbar Iterable
som delegerar till en strömrörledning. Detta kan vara användbart för att returnera en modifierad vy av en live-datauppsättning utan att behöva samla resultat till en tillfällig struktur.
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);
}
Produktion:
foo
bar
foo
bar
Detta fungerar eftersom Iterable
förklarar en enda abstrakt metod Iterator<T> iterator()
. Det gör det effektivt till ett funktionellt gränssnitt, implementerat av en lambda som skapar en ny ström för varje samtal.
I allmänhet fungerar en Stream
som visas i följande bild:
OBS : Argumentkontroller utförs alltid, även utan terminaloperation :
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()");
}
Produktion:
Vi fick en NullPointerException eftersom noll överfördes som ett argument för att filtrera ()
Skapa en frekvenskarta
groupingBy(classifier, downstream)
samlaren tillåter insamling av Stream
till en Map
genom att klassificera varje element i en grupp och utföra en nedströmsoperation på elementen klassificerade i samma grupp.
Ett klassiskt exempel på denna princip är att använda en Map
att räkna förekomsten av element i en Stream
. I det här exemplet är klassificeringen helt enkelt identitetsfunktionen, som returnerar elementet som det är. Nedströmsoperationen räknar antalet lika element med counting()
.
Stream.of("apple", "orange", "banana", "apple")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.forEach(System.out::println);
Nedströmsoperationen är själv en samlare ( Collectors.counting()
) som fungerar på element av typen String och ger ett resultat av typen Long
. Resultatet av collect
är en Map<String, Long>
.
Detta skulle producera följande utgång:
banan = 1
apelsin = 1
äpple = 2
Parallell ström
Obs! Innan du bestämmer vilken Stream
ska användas ska du titta på ParallelStream vs Sequential Stream-beteende .
När du vill utföra Stream
samtidigt kan du använda något av dessa sätt.
List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();
Eller:
Stream<String> aParallelStream = data.parallelStream();
För att utföra de operationer som definierats för parallellströmmen, ring en terminaloperatör:
aParallelStream.forEach(System.out::println);
(En möjlig) som matas ut från den parallella Stream
:
Tre
fyra
Ett
Två
Fem
Ordningen kan ändras när alla element behandlas parallellt (vilket kan göra det snabbare). Använd parallelStream
när beställningen inte spelar någon roll.
Effektpåverkan
I fallet nätverk är inblandat, parallell Stream
s kan försämra den totala prestandan för en applikation, eftersom alla parallella Stream
s bruk en gemensam gaffel ansluta tråden pool för nätverket.
Å andra sidan kan parallella Stream
förbättra prestandan betydligt i många andra fall beroende på antalet tillgängliga kärnor i den löpande CPU för tillfället.
Konvertera en valfri ström till en värdeström
Du kan behöva konvertera en Stream
avger Optional
till en Stream
värden, endast avger värden från befintliga Optional
. (dvs: utan null
och inte handlar om 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]
Skapa en ström
Alla java Collection<E>
s har stream()
och parallelStream()
-metoder från vilka en Stream<E>
kan konstrueras:
Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();
En Stream<E>
kan skapas från en matris med hjälp av en av två metoder:
String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);
Skillnaden mellan Arrays.stream()
och Stream.of()
är att Stream.of()
har en parameter varargs, så den kan användas som:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Det finns också primitiva Stream
som du kan använda. Till exempel:
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
Dessa primitiva strömmar kan också konstrueras med Arrays.stream()
-metoden:
IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });
Det är möjligt att skapa en Stream
från en matris med ett specificerat intervall.
int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);
Observera att valfri primitiv ström kan konverteras till ström av boxad typ med metoden i boxed
:
Stream<Integer> integerStream = intStream.boxed();
Detta kan vara användbart i vissa fall om du vill samla in data eftersom primitiv ström inte har någon collect
som tar en Collector
som argument.
Återanvända mellanoperationer i en strömkedja
Strömmen stängs när någonsin terminaldrift kallas. Återanvända strömmen av mellanoperationer när endast terminaldrift endast varierar. vi kan skapa en strömleverantör för att konstruera en ny ström med alla mellanliggande operationer som redan är installerade.
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[]
matriser kan konverteras till List<Integer>
hjälp av strömmar
int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());
Hitta statistik om numeriska strömmar
Java 8 tillhandahåller klasser som kallas IntSummaryStatistics
, DoubleSummaryStatistics
och LongSummaryStatistics
som ger ett tillståndsobjekt för att samla in statistik som count
, min
, max
, sum
och 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);
Vilket kommer att resultera i:
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}
Skaffa en bit av en ström
Exempel: Skaffa en Stream
med 30 element, som innehåller 21 till 50: e (inklusive) element i en samling.
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);
Anmärkningar:
-
IllegalArgumentException
kastas omn
är negativ ellermaxSize
är negativ - både
skip(long)
ochlimit(long)
är mellanoperationer - om en ström innehåller färre än
n
element,skip(n)
en tom ström - både
skip(long)
ochlimit(long)
är billiga operationer på sekventiella strömledningar, men kan vara ganska dyra på beställda parallella rörledningar
Slå samman strömmar
Variabel deklaration för exempel:
Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");
Exempel 1 - Concatenate två Stream
s
final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
concat1.forEach(System.out::print);
// prints: abc123
Exempel 2 - Concatenate fler än två Stream
s
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
Alternativt för att förenkla concat()
kapslade concat()
kan Stream
s också sammanlänkas med 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
Var försiktig när du Stream
strömmar från upprepad sammanlänkning, eftersom åtkomst till ett element i en djupt sammankopplad Stream
kan resultera i djupa samtalskedjor eller till och med en StackOverflowException
.
IntStream to String
Java har inte en Char Stream , så när du arbetar med String
s och konstruerar en Stream
of Character
s är ett alternativ att få en IntStream
av kodpunkter med String.codePoints()
-metoden. Så IntStream
kan erhållas enligt nedan:
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
Det är lite mer involverat att göra konverteringen tvärtom, dvs IntStreamToString. Det kan göras på följande sätt:
public String intStreamToString(IntStream intStream) {
return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Sortera med hjälp av ström
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);
Produktion:
[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]
Det är också möjligt att använda olika jämförelsemekanismer eftersom det finns en överbelastad sorted
version som tar en komparator som sitt argument.
Du kan också använda ett lambda-uttryck för sortering:
List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());
Detta skulle producera [Sydney, New York, Mumbai, London, California, Amsterdam]
Du kan använda Comparator.reverseOrder()
att ha en komparator som sätter reverse
till den naturliga beställningen.
List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
Strömmar av primitiva
Java tillhandahåller specialiserade Stream
för tre typer av primitiva IntStream
(för int
), LongStream
(för long
s) och DoubleStream
(för double
s). Förutom att de är optimerade implementationer för sina respektive primitiv, tillhandahåller de också flera specifika terminalmetoder, vanligtvis för matematiska operationer. T.ex:
IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0
Samla resultat av en ström till en matris
Analog för att få en samling för en Stream
genom att collect()
en matris kan erhållas med 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
är en speciell typ av metodreferens: en konstruktorreferens.
Hitta det första elementet som matchar ett predikat
Det är möjligt att hitta det första elementet i en Stream
som matchar ett villkor.
I det här exemplet hittar vi det första Integer
vars kvadrat är över 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
Detta uttryck kommer att returnera en OptionalInt
med resultatet.
Observera att med en oändlig Stream
kommer Java att kontrollera varje element tills det hittar ett resultat. Med en ändlig Stream
, om Java slutar på element men fortfarande inte kan hitta ett resultat, returnerar det en tom OptionalInt
.
Använda IntStream för att iterera över index
Stream
av element tillåter vanligtvis inte åtkomst till indexvärdet för det aktuella objektet. För att iterera över en matris eller ArrayList
medan du har tillgång till index använder du 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);
Metoden range(start, endExclusive)
returnerar en annan ÌntStream
och mapToObj(mapper)
returnerar en ström av String
.
Produktion:
# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc
Detta liknar mycket att använda en normal for
slinga med en räknare, men med fördel av rörledning och parallellisering:
for (int i = 0; i < names.length; i++) {
String newName = String.format("#%d %s", i + 1, names[i]);
System.out.println(newName);
}
Flatten Streams med flatMap ()
En Stream
av artiklar som i sin tur är streambara kan plattas ut i en enda kontinuerlig Stream
:
Array of List of Items kan konverteras till en enda lista.
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]
Karta som innehåller lista över objekt som värden kan plattas till en kombinerad lista
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
över Map
kan plattas ut i en enda kontinuerlig 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]
Skapa en karta baserad på en ström
Enkelt fall utan duplikatnycklar
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}
För att göra saker mer förklarande kan vi använda statisk metod i Function
- Function.identity()
. Vi kan ersätta detta lambda- element -> element
med Function.identity()
.
Fall där det kan finnas duplikatnycklar
Javadoc för Collectors.toMap
anger:
Om de mappade nycklarna innehåller dubbletter (enligt
Object.equals(Object)
) kastas enIllegalStateException
när insamlingsoperationen utförs. Om de mappade knapparna kan ha dubbletter, använd iställettoMap(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}
BinaryOperator
skickas till Collectors.toMap(...)
genererar värdet som ska lagras vid en kollision. Det kan:
- returnera det gamla värdet, så att det första värdet i strömmen har företräde,
- returnera det nya värdet, så att det sista värdet i strömmen har företräde, eller
- kombinera de gamla och nya värdena
Gruppera efter värde
Du kan använda Collectors.groupingBy
när du behöver utföra motsvarigheten till en databas kaskad "grupp efter" operation. För att illustrera skapar följande en karta där människors namn mappas till efternamn:
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]}
Generera slumpmässiga strängar med strömmar
Det är ibland användbart att skapa slumpmässiga Strings
, kanske som session-ID för en webbtjänst eller ett initialt lösenord efter registrering för en applikation. Detta kan enkelt uppnås med hjälp av Stream
.
Först måste vi initiera en slumptalsgenerator. För att förbättra säkerheten för de genererade String
är det en bra idé att använda SecureRandom
.
Obs! Att skapa ett SecureRandom
är ganska dyrt, så det är bästa praxis att bara göra detta en gång och ringa en av dess setSeed()
-metoder då och då för att sätta in den igen.
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
När vi skapar slumpmässiga String
vill vi att de bara använder vissa tecken (t.ex. bara bokstäver och siffror). Därför kan vi skapa en metod som returnerar en boolean
som senare kan användas för att filtrera Stream
.
//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);
}
Därefter kan vi använda RNG för att generera en slumpmässig sträng av specifik längd som innehåller charset som passerar vår useThisCharacter
kontroll.
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;
}
Använda strömmar för att implementera matematiska funktioner
Stream
, och särskilt IntStream
s, är ett elegant sätt att implementera summeringsvillkor (∑). Intervallen av Stream
kan användas som gränserna för summeringen.
Till exempel, Madhavas approximation av Pi ges med formeln (Källa: wikipedia ):
Detta kan beräknas med godtycklig precision. Till exempel för 101 villkor:
double pi = Math.sqrt(12) *
IntStream.rangeClosed(0, 100)
.mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
.sum();
Obs: Med double
precision är det att välja en övre gräns på 29 tillräckligt för att få ett resultat som inte kan Math.Pi
från Math.Pi
Använda strömmar och metodreferenser för att skriva självdokumenterande processer
Metodreferenser gör utmärkt självdokumenterande kod, och att använda metodreferenser med Stream
gör komplicerade processer enkla att läsa och förstå. Tänk på följande kod:
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;
}
Denna sista metod som skrivs om med hjälp av Stream
och metodreferenser är mycket mer läsbar och varje steg i processen förstås snabbt och lätt - det är inte bara kortare, det visar också på ett ögonblick vilka gränssnitt och klasser som är ansvariga för koden i varje steg:
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());
}
Använda strömmar av kartor.Entry för att bevara initiala värden efter kartläggning
När du har en Stream
måste du kartlägga men också vill bevara de initiala värdena, du kan kartlägga Stream
till en Map.Entry<K,V>
hjälp av en verktygsmetod som följande:
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
Sedan kan du använda din omvandlare för att bearbeta Stream
har tillgång till både de ursprungliga och mappade värdena:
Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
Du kan sedan fortsätta att bearbeta den Stream
som normalt. Detta undviker omkostnaderna för att skapa en mellansamling.
Strömförsörjningskategorier
Stream-operationer ingår i två huvudkategorier, mellan- och terminaloperationer, och två underkategorier, statslösa och statliga.
Mellanoperationer:
En mellanoperation är alltid lat , till exempel en enkel Stream.map
. Det åberopas inte förrän strömmen faktiskt konsumeras. Detta kan verifieras enkelt:
Arrays.asList(1, 2 ,3).stream().map(i -> {
throw new RuntimeException("not gonna happen");
return i;
});
Mellanoperationer är de vanliga byggstenarna i en ström, kedjade efter källan och följs vanligtvis av en terminaloperation som utlöser strömkedjan.
Terminaloperationer
Terminaloperationer är det som utlöser förbrukningen av en ström. Några av de vanligare är Stream.forEach
eller Stream.collect
. De placeras vanligtvis efter en kedja av mellanoperationer och är nästan alltid ivriga .
Statslösa verksamheter
Statslöshet innebär att varje objekt behandlas utan sammanhanget med andra objekt. Statslösa operationer möjliggör minneseffektiv behandling av strömmar. Verksamheter som Stream.map
och Stream.filter
som inte kräver information om andra objekt i strömmen anses vara statslösa.
Statlig verksamhet
Statlighet innebär att operationen för varje objekt beror på (vissa) andra objekt i strömmen. Detta kräver att ett tillstånd bevaras. Åtgärder med tillstånd kan bryta med långa eller oändliga strömmar. Verksamheter som Stream.sorted
kräver att hela strömmen ska bearbetas innan något objekt sänds ut som kommer att brytas i en tillräckligt lång ström av objekt. Detta kan demonstreras med en lång ström ( kör på egen risk ):
// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);
Detta kommer att orsaka ett minne utanför Stream.sorted
:
// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);
Konvertera en iterator till en ström
Använd Spliterators.spliterator()
eller Spliterators.spliteratorUnknownSize()
att konvertera en iterator till en ström:
Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);
Minskning med strömmar
Reduktion är processen att tillämpa en binär operatör på varje element i en ström för att resultera i ett värde.
sum()
-metoden för en IntStream
är ett exempel på en reduktion; det gäller tillägg till varje term i strömmen, vilket resulterar i ett slutligt värde:
Detta motsvarar (((1+2)+3)+4)
En reduce
för en ström låter en skapa en anpassad reduktion. Det är möjligt att använda reduce
för att implementera sum()
-metoden:
IntStream istr;
//Initialize istr
OptionalInt istr.reduce((a,b)->a+b);
Den Optional
versionen returneras så att tomma strömmar kan hanteras på lämpligt sätt.
Ett annat exempel på reduktion är att kombinera en Stream<LinkedList<T>>
till en enda 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;
});
Du kan också tillhandahålla ett identitetselement . Exempelvis är identitetselementet för tillägg 0, som x+0==x
. För multiplikation är identitetselementet 1, som x*1==x
. I fallet ovan är identitetselementet en tom LinkedList<T>
, för om du lägger till en tom lista till en annan lista ändras inte listan som du "lägger till" till:
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;
});
Observera att när ett identitetselement tillhandahålls, är returvärdet inte lindat i ett Optional
— om det kallas på en tom ström, reduce()
kommer identitetselementet att returneras.
Den binära operatören måste också vara associerande , vilket betyder att (a+b)+c==a+(b+c)
. Detta beror på att elementen kan reduceras i valfri ordning. Exempelvis kan ovanstående tilläggsreduktion utföras så här:
Denna minskning motsvarar skrivande ((1+2)+(3+4))
. Egenskaperna för associativitet gör det också möjligt för Java att reducera Stream
parallellt - en del av strömmen kan minskas av varje processor, med en reduktion som kombinerar resultatet för varje processor i slutet.
Gå med i en ström till en enda sträng
Ett användningsfall som ofta stöter på är att skapa en String
från en ström, där strömobjekten är separerade med en viss karaktär. Metoden Collectors.joining()
kan användas för detta, som i följande exempel:
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);
Produktion:
APPEL, BANANA, ORANGE, Päron
Metoden Collectors.joining()
kan också tillgodose för- och postfix:
String result = fruitStream.filter(s -> s.contains("e"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", ", "Fruits: ", "."));
System.out.println(result);
Produktion:
Frukter: APPLE, ORANGE, Päron.