Benvenuti al tutorial sull’API Stream di Java 8. Negli ultimi post su Java 8, abbiamo esaminato Modifiche all’interfaccia Java 8 e Interfacce funzionali ed espressioni lambda. Oggi esamineremo una delle principali API introdotte in Java 8 – Java Stream.
Java 8 Stream
- Java 8 Stream
- Collezioni e Java Stream
- Interfacce funzionali in Java 8 Stream
- java.util.Optional
- java.util.Spliterator
- Operazioni intermedie e terminali dello Stream Java
- Operazioni di interruzione dello Stream Java
- Esempi di Stream Java
- Limitazioni dell’API Stream Java 8
Stream Java
Prima di esaminare gli esempi dell’API Stream Java, vediamo perché è stato necessario. Supponiamo di voler iterare su una lista di interi e trovare la somma di tutti gli interi maggiori di 10. Prima di Java 8, il modo per farlo sarebbe stato:
private static int sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int sum = 0;
while (it.hasNext()) {
int num = it.next();
if (num > 10) {
sum += num;
}
}
return sum;
}
Ci sono tre problemi principali con l’approccio sopra descritto:
- Vogliamo solo conoscere la somma dei numeri interi ma dovremmo anche specificare come avverrà l’iterazione, questo viene anche chiamato iterazione esterna perché il programma client gestisce l’algoritmo per iterare sulla lista.
- Il programma è di natura sequenziale, non c’è modo di farlo facilmente in parallelo.
- C’è molto codice da scrivere anche per un compito semplice.
Per superare tutte le limitazioni sopra descritte, è stata introdotta Java 8 Stream API. Possiamo utilizzare Java Stream API per implementare iterazione interna, che è migliore perché il framework Java controlla l’iterazione. L’iterazione interna offre diverse funzionalità come l’esecuzione sequenziale e parallela, il filtraggio in base ai criteri forniti, la mappatura, ecc. La maggior parte degli argomenti dei metodi di Java 8 Stream API sono interfacce funzionali, quindi le espressioni lambda funzionano molto bene con esse. Vediamo come possiamo scrivere la logica sopra in una singola istruzione utilizzando Java Streams.
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
Si noti che il programma sopra utilizza la strategia di iterazione, il filtraggio e i metodi di mappatura del framework Java e aumenterebbe l’efficienza. Prima di tutto esamineremo i concetti fondamentali di Java 8 Stream API e poi passeremo attraverso alcuni esempi per capire i metodi più comunemente utilizzati.
Collezioni e Java Stream
A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream
package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream
, LongStream
and DoubleStream
.
Interfacce funzionali nello Stream di Java 8
Alcune delle interfacce funzionali comunemente utilizzate nei metodi dell’API Stream di Java 8 sono:
- Function e BiFunction: Function rappresenta una funzione che prende un tipo di argomento e restituisce un altro tipo di argomento.
Function<T, R>
è la forma generica dove T è il tipo dell’input della funzione e R è il tipo del risultato della funzione. Per gestire i tipi primitivi, ci sono specifiche interfacce Function –ToIntFunction
,ToLongFunction
,ToDoubleFunction
,ToIntBiFunction
,ToLongBiFunction
,ToDoubleBiFunction
,LongToIntFunction
,LongToDoubleFunction
,IntToLongFunction
,IntToDoubleFunction
etc. Alcuni dei metodi Stream in cui viene utilizzataFunction
o le sue specializzazioni primitive sono:- <R> Stream<R> map(Function<? super T, ? extends R> mapper)
- IntStream mapToInt(ToIntFunction<? super T> mapper) – analogamente per flussi specifici di ritorno long e double.
- IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – analogamente per long e double
- <A> A[] toArray(IntFunction<A[]> generator)
- <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- Predicate e BiPredicate: Rappresenta un predicato contro cui gli elementi dello stream vengono testati. Viene utilizzato per filtrare gli elementi dallo stream di Java. Proprio come
Function
, esistono interfacce specifiche per int, long e double. Alcuni dei metodi di Stream in cui vengono utilizzate specializzazioni diPredicate
oBiPredicate
sono:- Stream<T> filter(Predicate<? super T> predicate)
- boolean anyMatch(Predicate<? super T> predicate)
- boolean allMatch(Predicate<? super T> predicate)
- boolean noneMatch(Predicate<? super T> predicate)
- Consumer e BiConsumer: Rappresentano un’operazione che accetta un singolo argomento di input e non restituisce alcun risultato. Possono essere utilizzati per eseguire qualche azione su tutti gli elementi dello stream di Java. Alcuni dei metodi dello Stream di Java 8 in cui vengono utilizzate le interfacce
Consumer
,BiConsumer
o le loro specializzazioni primitive sono:- Stream<T> peek(Consumer<? super T> azione)
- void forEach(Consumer<? super T> azione)
- void forEachOrdered(Consumer<? super T> azione)
- Fornitore: Il fornitore rappresenta un’operazione attraverso la quale possiamo generare nuovi valori nello stream. Alcuni dei metodi in Stream che prendono un argomento
Fornitore
sono:- public static<T> Stream<T> generate(Fornitore<T> s)
- <R> R collect(Fornitore<R> fornitore, BiConsumer<R, ? super T> accumulatore, BiConsumer<R, R> combinatore)
java.util.Optional
Java Optional è un oggetto contenitore che può o non può contenere un valore non nullo. Se un valore è presente, isPresent()
restituirà true e get()
restituirà il valore. Le operazioni terminali dello stream restituiscono un oggetto Optional. Alcuni di questi metodi sono:
- Optional<T> reduce(BinaryOperator<T> accumulator)
- Optional<T> min(Comparator<? super T> comparator)
- Optional<T> max(Comparator<? super T> comparator)
- Optional<T> findFirst()
- Optional<T> findAny()
java.util.Spliterator
Per supportare l’esecuzione parallela nell’API Stream di Java 8, viene utilizzata l’interfaccia Spliterator
. Il metodo trySplit
di Spliterator restituisce un nuovo Spliterator che gestisce un sottoinsieme degli elementi dello Spliterator originale.
Operazioni Intermedie e Terminali di Java Stream
Le operazioni della API Java Stream che restituiscono un nuovo Stream sono chiamate operazioni intermedie. Nella maggior parte dei casi, queste operazioni sono pigre per natura, quindi iniziano a produrre nuovi elementi di stream e li inviano all’operazione successiva. Le operazioni intermedie non sono mai le operazioni finali di produzione di risultati. Le operazioni intermedie comunemente utilizzate sono filter
e map
. Le operazioni della API Stream Java 8 che restituiscono un risultato o producono un effetto collaterale. Una volta chiamato il metodo terminale su uno stream, consuma lo stream e dopo di ciò non possiamo utilizzare lo stream. Le operazioni terminali sono avidi per natura, ovvero elaborano tutti gli elementi nello stream prima di restituire il risultato. I metodi terminali comunemente utilizzati sono forEach
, toArray
, min
, max
, findFirst
, anyMatch
, allMatch
ecc. È possibile identificare i metodi terminali dal tipo di ritorno, non restituiranno mai uno Stream.
Operazioni di Short Circuiting dello Stream Java
Un’operazione intermedia è chiamata short circuiting, se può produrre uno stream finito per uno stream infinito. Ad esempio limit()
e skip()
sono due operazioni intermedie di short circuiting. Un’operazione terminale è chiamata short circuiting, se può terminare in tempo finito per uno stream infinito. Ad esempio anyMatch
, allMatch
, noneMatch
, findFirst
e findAny
sono operazioni terminali di short circuiting.
Esempi di Java Stream
I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.
Creazione di Java Streams
Esistono diversi modi per creare uno stream in Java da array e collezioni. Esaminiamoli con esempi semplici.
-
Possiamo utilizzare
Stream.of()
per creare uno stream da dati di tipo simile. Ad esempio, possiamo creare uno stream di interi in Java da un gruppo di int o oggetti Integer.Stream<Integer> stream = Stream.of(1,2,3,4);
-
Possiamo utilizzare
Stream.of()
con un array di oggetti per restituire lo stream. Nota che non supporta l’autoboxing, quindi non possiamo passare un array di tipo primitivo.Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); //funziona bene Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); //Errore di compilazione, Tipo non corrispondente: impossibile convertire da Stream<int[]> a Stream<Integer>
-
Possiamo utilizzare la raccolta
stream()
per creare uno stream sequenziale eparallelStream()
per creare uno stream parallelo.List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); // stream sequenziale Stream<Integer> sequentialStream = myList.stream(); // stream parallelo Stream<Integer> parallelStream = myList.parallelStream();
-
Possiamo utilizzare i metodi
Stream.generate()
eStream.iterate()
per creare uno Stream.Stream<String> stream1 = Stream.generate(() -> {return "abc";}); Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
-
Utilizzando i metodi
Arrays.stream()
eString.chars()
.LongStream is = Arrays.stream(new long[]{1,2,3,4}); IntStream is2 = "abc".chars();
Convertire lo Stream Java in una Collezione o un Array
Esistono diversi modi per ottenere una Collezione o un Array da uno stream Java.
-
Possiamo usare il metodo
collect()
di java Stream per ottenere una Lista, una Mappa o un Insieme dallo stream.Stream<Integer> intStream = Stream.of(1,2,3,4); List<Integer> intList = intStream.collect(Collectors.toList()); System.out.println(intList); //stamperà [1, 2, 3, 4] intStream = Stream.of(1,2,3,4); //lo stream è chiuso, quindi dobbiamo crearlo di nuovo Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10)); System.out.println(intMap); //stamperà {1=11, 2=12, 3=13, 4=14}
-
Possiamo usare il metodo
toArray()
dello stream per creare un array dallo stream.Stream<Integer> intStream = Stream.of(1,2,3,4); Integer[] intArray = intStream.toArray(Integer[]::new); System.out.println(Arrays.toString(intArray)); //stamperà [1, 2, 3, 4]
Operazioni Intermedie di Java Stream
Diamo un’occhiata ad un esempio di operazioni intermedie dello stream di Java comunemente usate.
-
Esempio di filtro di stream: Possiamo utilizzare il metodo filter() per testare gli elementi dello stream per una condizione e generare una lista filtrata.
List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); Stream<Integer> sequentialStream = myList.stream(); Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); // filtra i numeri superiori a 90 System.out.print("Numeri alti superiori a 90="); highNums.forEach(p -> System.out.print(p+" ")); // stampa "Numeri alti superiori a 90=91 92 93 94 95 96 97 98 99 "
-
Esempio di mappatura di stream: Possiamo utilizzare map() per applicare funzioni a uno stream. Vediamo come possiamo utilizzarlo per applicare la funzione di maiuscolo a una lista di stringhe.
Stream<String> names = Stream.of("aBc", "d", "ef"); System.out.println(names.map(s -> { return s.toUpperCase(); }).collect(Collectors.toList())); // stampa [ABC, D, EF]
-
Esempio di sorted() dello Stream: Possiamo usare sorted() per ordinare gli elementi dello stream passando un argomento Comparator.
Stream<String> nomi2 = Stream.of("aBc", "d", "ef", "123456"); List<String> ordinatoInverso = nomi2.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); System.out.println(ordinatoInverso); // [ef, d, aBc, 123456] Stream<String> nomi3 = Stream.of("aBc", "d", "ef", "123456"); List<String> ordinatoNaturale = nomi3.sorted().collect(Collectors.toList()); System.out.println(ordinatoNaturale); //[123456, aBc, d, ef]
-
Esempio di flatMap() dello Stream: Possiamo usare flatMap() per creare uno stream dallo stream di liste. Vediamo un esempio semplice per chiarire questo dubbio.
Stream<List<String>> listaOriginaleNomi = Stream.of( Arrays.asList("Pankaj"), Arrays.asList("David", "Lisa"), Arrays.asList("Amit")); // Appiattire lo stream da List<String> a stream di stringhe Stream<String> streamPiatto = listaOriginaleNomi .flatMap(listaStr -> listaStr.stream()); streamPiatto.forEach(System.out::println);
Operazioni terminali dello Stream di Java
Diamo un’occhiata ad alcuni esempi di operazioni terminali dello stream di Java.
-
Esempio di reduce() dello Stream: Possiamo utilizzare reduce() per eseguire una riduzione sugli elementi dello stream, utilizzando una funzione di accumulo associativa, e restituire un Optional. Vediamo come possiamo utilizzarlo per moltiplicare gli interi in uno stream.
Stream<Integer> numbers = Stream.of(1,2,3,4,5); Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;}); if(intOptional.isPresent()) System.out.println("Moltiplicazione = "+intOptional.get()); //120
-
Esempio di count() dello Stream: Possiamo utilizzare questa operazione terminale per contare il numero di elementi nello stream.
Stream<Integer> numbers1 = Stream.of(1,2,3,4,5); System.out.println("Numero di elementi nello stream="+numbers1.count()); //5
-
Esempio di forEach() dello Stream: Questo può essere utilizzato per iterare sullo stream. Possiamo usarlo al posto dell’iteratore. Vediamo come utilizzarlo per stampare tutti gli elementi dello stream.
Stream<Integer> numeri2 = Stream.of(1,2,3,4,5); numeri2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
-
Esempi di match() dello Stream: Vediamo alcuni esempi dei metodi di matching nell’API dello Stream.
Stream<Integer> numeri3 = Stream.of(1,2,3,4,5); System.out.println("Lo Stream contiene 4? "+numeri3.anyMatch(i -> i==4)); //Lo Stream contiene 4? true Stream<Integer> numeri4 = Stream.of(1,2,3,4,5); System.out.println("Lo Stream contiene tutti gli elementi minori di 10? "+numeri4.allMatch(i -> i<10)); //Lo Stream contiene tutti gli elementi minori di 10? true Stream<Integer> numeri5 = Stream.of(1,2,3,4,5); System.out.println("Lo Stream non contiene 10? "+numeri5.noneMatch(i -> i==10)); //Lo Stream non contiene 10? true
-
Stream findFirst() esempio: Questa è un’operazione terminale di interruzione rapida, vediamo come possiamo usarla per trovare la prima stringa da uno stream che inizia con D.
Stream<String> names4 = Stream.of("Pankaj","Amit","David", "Lisa"); Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst(); if(firstNameWithD.isPresent()){ System.out.println("Primo nome che inizia con D="+firstNameWithD.get()); //David }
Limitazioni dell’API Stream di Java 8
L’API Stream di Java 8 porta molte novità per lavorare con liste e array, ma ha anche alcune limitazioni.
-
Espressioni lambda senza stato: Se stai utilizzando uno stream parallelo e le espressioni lambda sono stateful, ciò può risultare in risposte casuali. Vediamolo con un semplice programma.
StatefulParallelStream.java
package com.journaldev.java8.stream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class StatefulParallelStream { public static void main(String[] args) { List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); List<Integer> result = new ArrayList<Integer>(); Stream<Integer> stream = ss.parallelStream(); stream.map(s -> { synchronized (result) { if (result.size() < 10) { result.add(s); } } return s; }).forEach( e -> {}); System.out.println(result); } }
Se esegui il programma sopra, otterrai risultati diversi perché dipende dal modo in cui lo stream viene iterato e non abbiamo alcun ordine definito per il processing parallelo. Se usiamo uno stream sequenziale, questo problema non si presenterà.
-
Una volta che uno Stream è consumato, non può essere utilizzato in seguito. Come puoi vedere negli esempi sopra, ogni volta sto creando uno stream.
-
Ci sono molti metodi nell’API Stream e la parte più confusa sono i metodi sovraccaricati. Rende il tempo di curva di apprendimento lungo.
E questo è tutto per il tutorial sull’esempio di Java 8 Stream. Non vedo l’ora di utilizzare questa funzionalità e rendere il codice leggibile con migliori prestazioni attraverso l’elaborazione parallela. Riferimento: Documento API Stream di Java
Source:
https://www.digitalocean.com/community/tutorials/java-8-stream