Java Language
I flussi
Ricerca…
introduzione
Un Stream
rappresenta una sequenza di elementi e supporta diversi tipi di operazioni per eseguire calcoli su tali elementi. Con Java 8, l'interfaccia Collection
ha due metodi per generare un Stream
: stream()
e parallelStream()
. Stream
operazioni di Stream
sono intermedie o terminali. Le operazioni intermedie restituiscono un Stream
modo che più operazioni intermedie possano essere concatenate prima che il Stream
venga chiuso. Le operazioni terminal sono nulle o restituiscono un risultato non stream.
Sintassi
- collection.stream ()
- Arrays.stream (array)
- Stream.iterate (firstValue, currentValue -> nextValue)
- Stream.generate (() -> valore)
- Stream.of (elementOfT [, elementOfT, ...])
- Stream.empty ()
- StreamSupport.stream (iterable.spliterator (), false)
Usando i flussi
Un Stream
è una sequenza di elementi su cui è possibile eseguire operazioni di aggregazione sequenziali e parallele. Qualsiasi Stream
dato può potenzialmente contenere una quantità illimitata di dati. Di conseguenza, i dati ricevuti da un Stream
vengono elaborati singolarmente al loro arrivo, anziché eseguire l'elaborazione batch sui dati del tutto. Quando combinati con espressioni lambda forniscono un modo conciso per eseguire operazioni su sequenze di dati utilizzando un approccio funzionale.
Esempio: ( vedi funziona su 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);
Produzione:
MELA
BANANA
ARANCIA
PERA
Le operazioni eseguite dal codice precedente possono essere riassunte come segue:
Crea uno
Stream<String>
contenente unStream
ordinato di elementiString
diStream
ordinati utilizzando il metodo statico di fabbricaStream.of(values)
.L'operazione
filter()
conserva solo gli elementi che corrispondono a un determinato predicato (gli elementi che vengono testati dal predicato restituiscono true). In questo caso, mantiene gli elementi contenenti una"a"
. Il predicato è dato come espressione lambda .L'operazione
map()
trasforma ogni elemento usando una determinata funzione, chiamata mapper. In questo caso, ogni FruitString
viene associata alla sua versioneString
maiuscolo utilizzando il metodo method:String::toUppercase
.Si noti che l'operazione
map()
restituirà un flusso con un diverso tipo generico se la funzione di mappatura restituisce un tipo diverso dal proprio parametro di input. Ad esempio su unoStream<String>
chiama.map(String::isEmpty)
restituisce unStream<Boolean>
L'operazione sort
sorted()
ordina gli elementi delStream
base al loro ordinamento naturale (lessicograficamente, nel caso diString
).Infine, l'operazione
forEach(action)
esegue un'azione che agisce su ciascun elemento delStream
, passandolo a un consumatore . Nell'esempio, ogni elemento viene semplicemente stampato sulla console. Questa operazione è un'operazione terminale, rendendo impossibile l'operazione su di esso.Si noti che le operazioni definite sullo
Stream
vengono eseguite a causa dell'operazione del terminale. Senza un'operazione terminale, lo stream non viene elaborato. Gli stream non possono essere riutilizzati. Una volta che viene chiamata un'operazione terminale, l'oggettoStream
diventa inutilizzabile.
Le operazioni (come visto sopra) sono concatenate per formare ciò che può essere visto come una query sui dati.
Flussi di chiusura
Nota che un
Stream
generalmente non deve essere chiuso. È richiesto solo per chiudere i flussi che operano su canali IO. La maggior parte dei tipi diStream
non funziona su risorse e pertanto non richiede la chiusura.
L'interfaccia Stream
estende AutoCloseable
. Gli stream possono essere chiusi chiamando il metodo close
o usando le istruzioni try-with-resource.
Un esempio di caso d'uso in cui un Stream
dovrebbe essere chiuso è quando crei un Stream
di linee da un file:
try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
lines.forEach(System.out::println);
}
L'interfaccia Stream
dichiara anche il metodo Stream.onClose()
che consente di registrare i gestori Runnable
che verranno chiamati quando lo stream viene chiuso. Un caso di utilizzo di esempio è dove il codice che produce un flusso deve sapere quando viene utilizzato per eseguire una pulizia.
public Stream<String>streamAndDelete(Path path) throws IOException {
return Files.lines(path).onClose(() -> someClass.deletePath(path));
}
Il gestore di esecuzione verrà eseguito solo se viene chiamato il metodo close()
, esplicitamente o implicitamente da un'istruzione try-with-resources.
Ordine di elaborazione
L'elaborazione di un oggetto Stream
può essere sequenziale o parallela .
In una modalità sequenziale , gli elementi vengono elaborati nell'ordine della sorgente del Stream
. Se il Stream
è ordinato (come un'implementazione SortedMap
o un List
), l'elaborazione è garantita per corrispondere all'ordine della fonte. In altri casi, tuttavia, è necessario prestare attenzione a non dipendere dall'ordinamento (si veda: l'ordine di iterazione di Java HashMap
keySet()
coerente? ).
Esempio:
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
La modalità parallela consente l'utilizzo di più thread su più core, ma non vi è alcuna garanzia dell'ordine in cui vengono elaborati gli elementi.
Se più metodi vengono richiamati su un Stream
sequenziale, non tutti i metodi devono essere richiamati. Ad esempio, se un Stream
viene filtrato e il numero di elementi è ridotto a uno, non verrà eseguita una chiamata successiva a un metodo come l' sort
. Ciò può aumentare le prestazioni di un Stream
sequenziale: un'ottimizzazione che non è possibile con un Stream
parallelo.
Esempio:
// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbersParallel); // Output: 2
Differenze da contenitori (o collezioni )
Mentre alcune azioni possono essere eseguite sia su Containers che su Stream, alla fine hanno scopi diversi e supportano diverse operazioni. I contenitori sono più focalizzati su come gli elementi sono memorizzati e su come è possibile accedere a tali elementi in modo efficiente. Un Stream
, d'altra parte, non fornisce accesso diretto e manipolazione ai suoi elementi; è più dedicato al gruppo di oggetti come entità collettiva ed esegue operazioni su quell'entità nel suo complesso. Stream
e Collection
sono astrazioni di alto livello separate per questi scopi diversi.
Raccogli elementi di un flusso in una raccolta
Raccogli con toList()
e toSet()
Gli elementi di un Stream
possono essere facilmente raccolti in un contenitore usando l'operazione 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]
Altre istanze di raccolta, come un Set
, possono essere create usando altri metodi built-in di Collectors
. Ad esempio, Collectors.toSet()
raccoglie gli elementi di un Stream
in un Set
.
Controllo esplicito sull'implementazione di List
o Set
Secondo la documentazione di Collectors#toList()
e Collectors#toSet()
, non ci sono garanzie sul tipo, sulla mutevolezza, sulla serializzabilità o sulla sicurezza del thread List
o del Set
restituito.
Per il controllo esplicito dell'implementazione da restituire, è possibile utilizzare Collectors#toCollection(Supplier)
, in cui il fornitore specificato restituisce una raccolta nuova e vuota.
// 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<>()))
);
Raccolta di elementi usando toMap
Il raccoglitore accumula elementi in una mappa, dove la chiave è l'id dello studente e il valore è il valore dello studente.
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);
Produzione :
{1=test1, 2=test2, 3=test3}
Collectors.toMap ha un'altra implementazione Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
. La funzione mergeFunction viene utilizzata principalmente per selezionare un nuovo valore o mantenere il vecchio valore se la chiave viene ripetuta quando si aggiunge un nuovo membro nella mappa da un elenco.
Spesso la mergeFunction assomiglia a: (s1, s2) -> s1
per mantenere il valore corrispondente alla chiave ripetuta, o (s1, s2) -> s2
per inserire un nuovo valore per la chiave ripetuta.
Raccolta di elementi per la mappa delle collezioni
Esempio: da ArrayList a Map <String, List <>>
Spesso è necessario creare una mappa dell'elenco da un elenco principale. Esempio: da uno studente della lista, abbiamo bisogno di fare una mappa dell'elenco delle materie per ogni studente.
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);
Produzione:
{ Robert=[LITERATURE],
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE],
Davis=[MATH, SCIENCE, GEOGRAPHY] }
Esempio: da ArrayList a 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);
Produzione:
{ 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]} }
Cheat-sheet
Obbiettivo | Codice |
---|---|
Raccogli a una List | Collectors.toList() |
Raccogli a un ArrayList con dimensioni pre-allocate | Collectors.toCollection(() -> new ArrayList<>(size)) |
Raccogli per un Set | Collectors.toSet() |
Raccogli in un Set con prestazioni di iterazione migliori | Collectors.toCollection(() -> new LinkedHashSet<>()) |
Raccogli in un Set<String> insensibile alle maiuscole / minuscole Set<String> | Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)) |
Raccogli a EnumSet<AnEnum> (miglior rendimento per enumerazioni) | Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class)) |
Raccogli a una Map<K,V> con chiavi univoche | Collectors.toMap(keyFunc,valFunc) |
Mappare MyObject.getter () su MyObject univoco | Collectors.toMap(MyObject::getter, Function.identity()) |
Mappare MyObject.getter () su più MyObjects | Collectors.groupingBy(MyObject::getter) |
Stream infiniti
È possibile generare un Stream
che non termina. Chiamando un metodo terminale su un Stream
infinito Stream
il Stream
entra in un ciclo infinito. Il metodo limit
di un Stream
può essere utilizzato per limitare il numero di termini del Stream
che Java elabora.
Questo esempio genera un Stream
di tutti i numeri naturali, a partire dal numero 1. Ogni successivo termine del Stream
è uno più alto del precedente. Chiamando il metodo limite di questo Stream
, vengono considerati e stampati solo i primi cinque termini del 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);
Produzione:
1
2
3
4
5
Un altro modo per generare un flusso infinito è utilizzare il metodo Stream.generate . Questo metodo richiede un lambda di tipo Supplier .
// 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);
Flussi di consumo
Un Stream
sarà attraversato solo quando c'è un'operazione terminale , come count()
, collect()
o forEach()
. In caso contrario, non verrà eseguita alcuna operazione sul Stream
.
Nell'esempio seguente, nessuna operazione terminale viene aggiunta al Stream
, quindi l'operazione filter()
non verrà invocata e non verrà prodotto alcun output perché peek()
NON è un'operazione terminale .
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
Questa è una sequenza Stream
con un'operazione di terminale valida, quindi viene prodotta un'uscita.
Puoi anche usare forEach
invece di peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Produzione:
2
4
6
8
Dopo aver eseguito l'operazione del terminale, il Stream
viene consumato e non può essere riutilizzato.
Sebbene un determinato oggetto flusso non possa essere riutilizzato, è facile creare un Iterable
riutilizzabile che deleghi ad una pipeline di flusso. Questo può essere utile per restituire una vista modificata di un set di dati live senza dover raccogliere i risultati in una struttura temporanea.
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);
}
Produzione:
foo
bar
foo
bar
Questo funziona perché Iterable
dichiara un singolo metodo astratto Iterator<T> iterator()
. Ciò lo rende efficacemente un'interfaccia funzionale, implementata da un lambda che crea un nuovo stream per ogni chiamata.
In generale, un Stream
funziona come mostrato nell'immagine seguente:
NOTA : i controlli degli argomenti vengono sempre eseguiti, anche senza un'operazione di terminale :
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()");
}
Produzione:
Abbiamo ottenuto una NullPointerException in quanto null è stato passato come argomento a filter ()
Creazione di una mappa di frequenza
Il collector groupingBy(classifier, downstream)
consente la raccolta di elementi Stream
in una Map
classificando ciascun elemento in un gruppo ed eseguendo un'operazione downstream sugli elementi classificati nello stesso gruppo.
Un classico esempio di questo principio è l'utilizzo di una Map
per contare le occorrenze di elementi in un Stream
. In questo esempio, il classificatore è semplicemente la funzione di identità, che restituisce l'elemento così com'è. L'operazione downstream conta il numero di elementi uguali, usando il counting()
.
Stream.of("apple", "orange", "banana", "apple")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.forEach(System.out::println);
L'operazione downstream è essa stessa un collector ( Collectors.counting()
) che opera su elementi di tipo String e produce un risultato di tipo Long
. Il risultato della chiamata al metodo collect
è una Map<String, Long>
.
Questo produrrebbe il seguente risultato:
banane = 1
arancione = 1
mela = 2
Stream parallelo
Nota: prima di decidere quale Stream
utilizzare, dare un'occhiata al comportamento di ParallelStream vs Sequential Stream .
Quando si desidera eseguire contemporaneamente operazioni di Stream
, è possibile utilizzare uno di questi modi.
List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();
O:
Stream<String> aParallelStream = data.parallelStream();
Per eseguire le operazioni definite per lo streaming parallelo, chiamare un operatore di terminale:
aParallelStream.forEach(System.out::println);
(Un possibile) output dal Stream
parallelo:
Tre
quattro
Uno
Due
Cinque
L'ordine potrebbe cambiare in quanto tutti gli elementi sono elaborati in parallelo (il che potrebbe renderlo più veloce). Usa parallelStream
quando l'ordine non ha importanza.
Impatto sulle prestazioni
Nel caso in cui sia coinvolta la rete, i Stream
paralleli possono peggiorare le prestazioni generali di un'applicazione in quanto tutti i Stream
paralleli utilizzano un comune pool di thread fork-join per la rete.
D'altra parte, parallel Stream
s può migliorare significativamente le prestazioni in molti altri casi, a seconda del numero di core disponibili nella CPU in esecuzione al momento.
Conversione di un flusso di facoltativo in un flusso di valori
Potrebbe essere necessario convertire un Stream
emette Optional
in un Stream
di valori, emettendo solo valori da Optional
esistente. (vale a dire: senza valore null
e senza trattare 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]
Creazione di un flusso
Tutti i Collection<E>
s di java Collection<E>
hanno i metodi stream()
e parallelStream()
da cui è possibile costruire uno Stream<E>
:
Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();
È possibile creare uno Stream<E>
da una matrice utilizzando uno dei seguenti due metodi:
String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);
La differenza tra Arrays.stream()
e Stream.of()
è che Stream.of()
ha un parametro varargs, quindi può essere usato come:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Ci sono anche i primitivi Stream
che puoi usare. Per esempio:
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
Questi stream primitivi possono anche essere costruiti usando il metodo Arrays.stream()
:
IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });
È possibile creare un Stream
da una matrice con un intervallo specificato.
int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);
Si noti che qualsiasi flusso primitivo può essere convertito in stream di tipo in scatola usando il metodo in boxed
:
Stream<Integer> integerStream = intStream.boxed();
Questo può essere utile in alcuni casi se si desidera raccogliere i dati poiché il flusso primitivo non ha alcun metodo di collect
che accetta un Collector
come argomento.
Riutilizzo delle operazioni intermedie di una catena di flusso
Lo streaming viene chiuso quando viene chiamata un'operazione terminale. Riutilizzare il flusso di operazioni intermedie, quando solo il funzionamento del terminale è solo variabile. potremmo creare un fornitore di stream per costruire un nuovo stream con tutte le operazioni intermedie già configurate.
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[]
array int[]
possono essere convertiti in List<Integer>
utilizzando gli stream
int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());
Ricerca di statistiche sui flussi numerici
Java 8 fornisce classi chiamate IntSummaryStatistics
, DoubleSummaryStatistics
e LongSummaryStatistics
che forniscono un oggetto di stato per la raccolta di statistiche quali count
, min
, max
, sum
e 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);
Quale risulterà in:
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}
Ottieni una fetta di un flusso
Esempio: ottieni un Stream
di 30 elementi, contenente elementi dal 21 ° al 50 ° (inclusi) di una raccolta.
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);
Gli appunti:
-
IllegalArgumentException
viene generato sen
è negativo omaxSize
è negativo - sia
skip(long)
chelimit(long)
sono operazioni intermedie - se un flusso contiene meno di
n
elementi,skip(n)
restituisce un flusso vuoto - sia
skip(long)
chelimit(long)
sono operazioni a basso costo su pipeline sequenziali, ma possono essere piuttosto costose su condotte parallele ordinate
Concatenare flussi
Dichiarazione variabile per esempi:
Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");
Esempio 1 - Concatena due Stream
s
final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
concat1.forEach(System.out::print);
// prints: abc123
Esempio 2: concatena più di due 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
In alternativa, per semplificare il nidificata concat()
Sintassi il Stream
s può anche essere concatenato con 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
Fai attenzione quando costruisci Stream
s dalla concatenazione ripetuta, perché l'accesso a un elemento di un Stream
profondamente concatenato può portare a catene di chiamate profonde o persino a StackOverflowException
.
IntStream a stringa
Java non ha un flusso di caratteri , quindi quando si lavora con String
s e si costruisce un Stream
di Character
s, un'opzione è quella di ottenere un IntStream
di punti di codice usando il metodo String.codePoints()
. Quindi IntStream
può essere ottenuto come di seguito:
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
È un po 'più complicato fare la conversione in un altro modo, cioè IntStreamToString. Questo può essere fatto come segue:
public String intStreamToString(IntStream intStream) {
return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Ordina utilizzando 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);
Produzione:
[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]
È anche possibile utilizzare un meccanismo di confronto diverso in quanto esiste una versione sorted
sovraccarico che accetta come argomento un comparatore.
Inoltre, puoi usare un'espressione lambda per l'ordinamento:
List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());
Ciò produrrebbe [Sydney, New York, Mumbai, London, California, Amsterdam]
Puoi usare Comparator.reverseOrder()
per avere un comparatore che impone il reverse
dell'ordine naturale.
List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
Flussi di primitivi
Java fornisce Stream
specializzati per tre tipi di primitive IntStream
(per int
s), LongStream
(per long
s) e DoubleStream
(per double
s). Oltre ad essere implementazioni ottimizzate per le rispettive primitive, forniscono anche diversi metodi terminali specifici, in genere per operazioni matematiche. Per esempio:
IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0
Raccogli i risultati di un flusso in una matrice
Analogico per ottenere una raccolta per uno Stream
di collect()
possibile ottenere un array con il metodo 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
è un tipo speciale di riferimento al metodo: un riferimento costruttore.
Trovare il primo elemento che corrisponde a un predicato
È possibile trovare il primo elemento di un Stream
che corrisponde a una condizione.
Per questo esempio, troveremo il primo numero Integer
cui quadrato è superiore a 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
Questa espressione restituirà un OptionalInt
con il risultato.
Nota che con un Stream
infinito, Java continuerà a controllare ogni elemento fino a quando non troverà un risultato. Con un Stream
, se Java esaurisce gli elementi ma non riesce a trovare un risultato, restituisce un oggetto OptionalInt
vuoto.
Utilizzo di IntStream per iterare su indici
Stream
elementi di Stream
di solito non consentono l'accesso al valore dell'indice dell'oggetto corrente. Per scorrere su un array o ArrayList
mentre si ha accesso agli indici, utilizzare 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);
Il metodo range(start, endExclusive)
restituisce un altro ÌntStream
e mapToObj(mapper)
restituisce un flusso di String
.
Produzione:
# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc
Questo è molto simile all'uso di un normale for
ciclo con un contatore, ma con il vantaggio di pipelining e parallelizzazione:
for (int i = 0; i < names.length; i++) {
String newName = String.format("#%d %s", i + 1, names[i]);
System.out.println(newName);
}
Flattenare i flussi con flatMap ()
Un Stream
di elementi a sua volta scorrevoli può essere appiattito in un unico Stream
continuo:
La matrice dell'elenco di elementi può essere convertita in una singola 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]
La mappa contenente l'elenco di elementi come valori può essere appiattita in un elenco combinato
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
di Map
può essere appiattito in un singolo Stream
continuo
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]
Crea una mappa basata su un flusso
Caso semplice senza chiavi duplicate
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}
Per rendere le cose più dichiarative, possiamo usare il metodo statico in Function
interface - Function.identity()
. Possiamo sostituire questo element -> element
lambda element -> element
con Function.identity()
.
Caso in cui potrebbero esserci chiavi duplicate
Il javadoc per gli stati Collectors.toMap
:
Se le chiavi mappate contengono duplicati (in base a
Object.equals(Object)
), viene generata unaIllegalStateException
quando viene eseguita l'operazione di raccolta. Se le chiavi mappate possono avere duplicati, utilizzare invecetoMap(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
passato a Collectors.toMap(...)
genera il valore da memorizzare nel caso di una collisione. Può:
- restituisce il vecchio valore, in modo che il primo valore nel flusso abbia la precedenza,
- restituire il nuovo valore, in modo che l'ultimo valore nel flusso abbia la precedenza o
- combinare i valori vecchi e nuovi
Raggruppamento per valore
È possibile utilizzare Collectors.groupingBy
quando è necessario eseguire l'equivalente di un'operazione "raggruppa per" in cascata del database. Per illustrare, il seguente crea una mappa in cui i nomi delle persone sono mappati ai cognomi:
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]}
Generazione di stringhe casuali usando flussi
A volte è utile creare Strings
casuali, magari come ID sessione per un servizio Web o una password iniziale dopo la registrazione per un'applicazione. Questo può essere facilmente ottenuto usando Stream
s.
Per prima cosa dobbiamo inizializzare un generatore di numeri casuali. Per migliorare la sicurezza per le String
generate, è una buona idea usare SecureRandom
.
Nota : la creazione di un SecureRandom
è piuttosto costosa, quindi è consigliabile farlo una volta sola e chiamare uno dei suoi metodi setSeed()
di volta in volta per renderizzarlo.
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
Quando creiamo String
casuali, di solito vogliamo che usino solo determinati caratteri (ad es. Solo lettere e cifre). Pertanto possiamo creare un metodo che restituisce un valore boolean
che può essere successivamente utilizzato per filtrare il 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);
}
Quindi possiamo utilizzare l'RNG per generare una stringa casuale di lunghezza specifica contenente il set di caratteri che supera il nostro controllo useThisCharacter
.
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;
}
Utilizzo degli stream per implementare funzioni matematiche
Stream
IntStream
, e in particolare quelli IntStream
, rappresentano un modo elegante per implementare i termini di somma (Σ). Le gamme del Stream
possono essere utilizzate come limiti della sommatoria.
Ad esempio, l'approssimazione di Pi di Madhava è data dalla formula (Fonte: wikipedia ):
Questo può essere calcolato con una precisione arbitraria. Ad esempio, per 101 termini:
double pi = Math.sqrt(12) *
IntStream.rangeClosed(0, 100)
.mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
.sum();
Nota: con la precisione del double
, selezionare un limite superiore di 29 è sufficiente per ottenere un risultato indistinguibile da Math.Pi
Utilizzo degli stream e dei riferimenti al metodo per scrivere processi di auto-documentazione
I riferimenti ai metodi costituiscono un codice di auto-documentazione eccellente e l'utilizzo dei riferimenti ai metodi con Stream
s semplifica la lettura e la comprensione dei processi complicati. Considera il seguente codice:
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;
}
Quest'ultimo metodo riscritto usando i riferimenti di Stream
s e method è molto più leggibile e ogni fase del processo è facilmente e facilmente comprensibile - non è solo più breve, ma mostra anche a colpo d'occhio quali interfacce e classi sono responsabili del codice in ogni passaggio:
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());
}
Utilizzo degli stream di Map.Entry per preservare i valori iniziali dopo la mappatura
Quando hai uno Stream
devi mappare ma vuoi preservare anche i valori iniziali, puoi mappare lo Stream
su Map.Entry<K,V>
usando un metodo di utilità come il seguente:
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
Quindi è possibile utilizzare il convertitore per elaborare i Stream
che hanno accesso sia ai valori originali sia a quelli mappati:
Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
È quindi possibile continuare a elaborare quel Stream
come di consueto. Ciò evita il sovraccarico di creazione di una raccolta intermedia.
Categorie di operazioni di streaming
Le operazioni di streaming rientrano in due categorie principali, operazioni intermedie e terminali e due sottocategorie, stateless e stateful.
Operazioni intermedie:
Un'operazione intermedia è sempre pigra , ad esempio un semplice Stream.map
. Non è invocato fino a quando il flusso non viene effettivamente consumato. Questo può essere verificato facilmente:
Arrays.asList(1, 2 ,3).stream().map(i -> {
throw new RuntimeException("not gonna happen");
return i;
});
Le operazioni intermedie sono gli elementi costitutivi comuni di un flusso, concatenati dopo l'origine e sono generalmente seguiti da un'operazione terminale che attiva la catena di flusso.
Operazioni terminalistiche
Le operazioni terminal sono ciò che fa scattare il consumo di un flusso. Alcuni dei più comuni sono Stream.forEach
o Stream.collect
. Di solito sono posizionati dopo una catena di operazioni intermedie e sono quasi sempre desiderosi .
Operazioni senza stato
Apolidia significa che ogni oggetto viene elaborato senza il contesto di altri elementi. Le operazioni senza stato consentono l'elaborazione efficiente dei flussi di memoria. Le operazioni come Stream.map
e Stream.filter
che non richiedono informazioni su altri elementi dello stream sono considerate senza stato.
Operazioni stateful
Statefulness significa che l'operazione su ciascun elemento dipende da (alcuni) altri elementi dello stream. Ciò richiede che uno stato sia preservato. Le operazioni di stato possono interrompersi con flussi lunghi o infiniti. Operazioni come Stream.sorted
richiedono che l'intero stream venga elaborato prima che venga emesso qualsiasi elemento che si romperà in un flusso di elementi abbastanza lungo. Questo può essere dimostrato da un lungo flusso ( eseguito a proprio rischio ):
// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);
Ciò causerà una memoria Stream.sorted
a causa dello stato di Stream.sorted
:
// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);
Conversione di un iteratore in un flusso
Utilizzare Spliterators.spliterator()
o Spliterators.spliteratorUnknownSize()
per convertire un iteratore in uno stream:
Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);
Riduzione con stream
La riduzione è il processo di applicazione di un operatore binario a ogni elemento di un flusso per ottenere un valore.
Il metodo sum()
di un IntStream
è un esempio di riduzione; applica l'aggiunta a ogni termine del flusso, determinando un valore finale:
Questo è equivalente a (((1+2)+3)+4)
Il metodo di reduce
di un flusso consente di creare una riduzione personalizzata. È possibile utilizzare il metodo reduce
per implementare il metodo sum()
:
IntStream istr;
//Initialize istr
OptionalInt istr.reduce((a,b)->a+b);
La versione Optional
viene restituita in modo che gli stream vuoti possano essere gestiti in modo appropriato.
Un altro esempio di riduzione è la combinazione di uno Stream<LinkedList<T>>
in una singola 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;
});
Puoi anche fornire un elemento di identità . Ad esempio, l'elemento identità per addizione è 0, come x+0==x
. Per la moltiplicazione, l'elemento identità è 1, come x*1==x
. Nel caso precedente, l'elemento identity è una LinkedList<T>
vuota LinkedList<T>
, perché se aggiungi una lista vuota ad un'altra lista, la lista che stai "aggiungendo" non cambia:
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;
});
Si noti che quando viene fornito un elemento identità, il valore di ritorno non viene incapsulato in un Optional
-if chiamato su un flusso vuoto, reduce()
restituirà l'elemento identità.
Anche l'operatore binario deve essere associativo , ovvero (a+b)+c==a+(b+c)
. Questo perché gli elementi possono essere ridotti in qualsiasi ordine. Ad esempio, la riduzione di cui sopra potrebbe essere eseguita in questo modo:
Questa riduzione equivale a scrivere ((1+2)+(3+4))
. La proprietà di associatività consente inoltre a Java di ridurre il Stream
in parallelo: una porzione del flusso può essere ridotta da ciascun processore, con una riduzione che combina il risultato di ciascun processore alla fine.
Unire uno stream a una singola stringa
Un caso d'uso che si incontra frequentemente, è la creazione di una String
da uno stream, in cui gli elementi di flusso sono separati da un determinato carattere. Il metodo Collectors.joining()
può essere utilizzato per questo, come nell'esempio seguente:
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);
Produzione:
MELA, BANANA, ARANCIA, PERA
Il metodo Collectors.joining()
può anche gestire pre e postfix:
String result = fruitStream.filter(s -> s.contains("e"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", ", "Fruits: ", "."));
System.out.println(result);
Produzione:
Frutta: MELA, ARANCIA, PERA.