JavaRush /Java Blog /Random-IT /Pausa caffè #177. Una guida dettagliata a Java Stream in ...

Pausa caffè #177. Una guida dettagliata a Java Stream in Java 8

Pubblicato nel gruppo Random-IT
Fonte: Hackernoon Questo post fornisce un tutorial dettagliato su come lavorare con Java Stream insieme ad esempi di codice e spiegazioni. Pausa caffè #177.  Una guida dettagliata a Java Stream in Java 8 - 1

Introduzione ai thread Java in Java 8

Java Streams, introdotti come parte di Java 8, vengono utilizzati per lavorare con raccolte di dati. Non sono di per sé una struttura dati, ma possono essere utilizzati per inserire informazioni da altre strutture dati ordinandole e concatenandole per produrre un risultato finale. Nota: è importante non confondere Stream e Thread, poiché in russo entrambi i termini sono spesso indicati nella stessa traduzione "flusso". Stream denota un oggetto per eseguire operazioni (il più delle volte trasferimento o archiviazione di dati), mentre Thread (traduzione letterale - thread) denota un oggetto che consente l'esecuzione di determinati codici di programma in parallelo con altri rami di codice. Poiché Stream non è una struttura dati separata, non modifica mai l'origine dati. I flussi Java hanno le seguenti funzionalità:
  1. Java Stream può essere utilizzato utilizzando il pacchetto "java.util.stream". Può essere importato in uno script utilizzando il codice:

    import java.util.stream.* ;

    Utilizzando questo codice, possiamo anche implementare facilmente diverse funzioni integrate in Java Stream.

  2. Java Stream può accettare input da raccolte di dati come raccolte e array in Java.

  3. Java Stream non richiede la modifica della struttura dei dati di input.

  4. Java Stream non modifica l'origine. Invece, genera output utilizzando metodi di pipeline appropriati.

  5. I Java Stream sono soggetti a operazioni intermedie e terminali, di cui parleremo nelle sezioni seguenti.

  6. In Java Stream, le operazioni intermedie vengono eseguite in pipeline e si verificano in un formato di valutazione lazy. Terminano con le funzioni del terminale. Questo costituisce il formato di base per l'utilizzo di Java Stream.

Nella sezione successiva, esamineremo i vari metodi utilizzati in Java 8 per creare un Java Stream come e quando richiesto.

Creazione di un flusso Java in Java 8

I thread Java possono essere creati in diversi modi:

1. Creazione di un flusso vuoto utilizzando il metodo Stream.empty()

Puoi creare un flusso vuoto da utilizzare successivamente nel codice. Se utilizzi il metodo Stream.empty() , verrà generato uno stream vuoto, che non contiene valori. Questo flusso vuoto può tornare utile se vogliamo saltare un'eccezione del puntatore nullo in fase di esecuzione. Per fare ciò puoi utilizzare il seguente comando:
Stream<String> str = Stream.empty();
L'istruzione precedente genererà uno stream vuoto denominato str senza elementi al suo interno. Per verificarlo basta controllare il numero o la dimensione dello stream utilizzando il termine str.count() . Per esempio,
System.out.println(str.count());
Di conseguenza, otteniamo 0 in uscita .

2. Crea un flusso utilizzando il metodo Stream.builder() con un'istanza Stream.Builder

Possiamo anche utilizzare Stream Builder per creare uno stream utilizzando il modello di progettazione del builder. È progettato per la costruzione passo-passo di oggetti. Vediamo come possiamo istanziare uno stream utilizzando Stream Builder .
Stream.Builder<Integer> numBuilder = Stream.builder();

numBuilder.add(1).add(2).add( 3);

Stream<Integer> numStream = numBuilder.build();
Utilizzando questo codice, puoi creare uno stream denominato numStream contenente int elementi . Tutto avviene abbastanza velocemente grazie all'istanza Stream.Builder chiamata numBuilder che viene creata per prima.

3. Crea uno stream con i valori specificati utilizzando il metodo Stream.of()

Un altro modo per creare uno stream prevede l'utilizzo del metodo Stream.of() . Questo è un modo semplice per creare uno stream con determinati valori. Dichiara e inizializza anche il thread. Un esempio di utilizzo del metodo Stream.of() per creare uno stream:
Stream<Integer> numStream = Stream.of(1, 2, 3);
Questo codice creerà uno stream contenente elementi int , proprio come abbiamo fatto nel metodo precedente utilizzando Stream.Builder . Qui abbiamo creato direttamente uno stream utilizzando Stream.of() con valori predefiniti [1, 2 e 3] .

4. Creare un flusso da un array esistente utilizzando il metodo Arrays.stream()

Un altro metodo comune per creare un thread prevede l'utilizzo di array in Java. Lo stream qui viene creato da un array esistente utilizzando il metodo Arrays.stream() . Tutti gli elementi dell'array vengono convertiti in elementi del flusso. Ecco un buon esempio:
Integer[] arr = {1, 2, 3, 4, 5};

Stream<Integer> numStream = Arrays.stream(arr);
Questo codice genererà un numStream contenente il contenuto di un array chiamato arr, che è un array di numeri interi.

5. Unione di due flussi esistenti utilizzando il metodo Stream.concat()

Un altro metodo che può essere utilizzato per creare uno stream è il metodo Stream.concat() . Viene utilizzato per combinare due thread per creare un singolo thread. Entrambi i flussi sono combinati in ordine. Ciò significa che il primo thread viene prima, seguito dal secondo thread e così via. Un esempio di tale concatenazione è simile al seguente:
Stream<Integer> numStream1 = Stream.of(1, 2, 3, 4, 5);

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

Stream<Integer> combinedStream = Stream.concat( numStream1, numStream2);
L'istruzione precedente creerà uno stream finale denominato combinedStream contenente gli elementi del primo stream numStream1 e del secondo stream numStream2 uno per uno .

Tipi di operazioni con Java Stream

Come già accennato, con Java Stream in Java 8 è possibile eseguire due tipi di operazioni: intermedie e terminali. Diamo un'occhiata a ciascuno di essi in modo più dettagliato.

Operazioni intermedie

Le operazioni intermedie generano un flusso di output e vengono eseguite solo quando incontrano un'operazione terminale. Ciò significa che le operazioni intermedie vengono eseguite pigramente, in pipeline e possono essere completate solo da un'operazione terminale. Imparerai a conoscere la valutazione pigra e il pipeline un po' più tardi. Esempi di operazioni intermedie sono i seguenti metodi: filter() , map() , Different() , peek() , sorted() e alcuni altri.

Operazioni sui terminali

Le operazioni terminali completano l'esecuzione delle operazioni intermedie e restituiscono anche i risultati finali del flusso di output. Poiché le operazioni terminali segnalano la fine dell'esecuzione lenta e del pipelining, questo thread non può essere utilizzato nuovamente dopo aver subito un'operazione terminale. Esempi di operazioni da terminale sono i seguenti metodi: forEach() , collector() , count() , reduce() e così via.

Esempi di operazioni con Java Stream

Operazioni intermedie

Ecco alcuni esempi di alcune operazioni intermedie che possono essere applicate a un Java Stream:

filtro()

Questo metodo viene utilizzato per filtrare gli elementi da un flusso che corrispondono a un predicato specifico in Java. Questi elementi filtrati costituiscono quindi un nuovo flusso. Facciamo un esempio per capire meglio.
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> even = numStream.filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println(even);
Conclusione:
[98]
Spiegazione: In questo esempio, puoi vedere che anche gli elementi (divisibili per 2) vengono filtrati utilizzando il metodo filter() e memorizzati in un elenco di numeri interi numStream , il cui contenuto viene stampato in seguito. Poiché 98 è l'unico numero intero pari nello stream, viene stampato nell'output.

carta geografica()

Questo metodo viene utilizzato per creare un nuovo flusso eseguendo funzioni mappate sugli elementi del flusso di input originale. Forse il nuovo flusso ha un tipo di dati diverso. L'esempio è simile al seguente:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> d = numStream.map(n -> n*2) .collect(Collectors.toList()); System.out.println(d);
Conclusione:
[86, 130, 2, 196, 126]
Spiegazione: qui vediamo che il metodo map() viene utilizzato per raddoppiare semplicemente ogni elemento dello stream numStream . Come puoi vedere dall'output, ciascuno degli elementi nello stream è stato raddoppiato con successo.

distinto()

Questo metodo viene utilizzato per recuperare solo singoli elementi in uno stream filtrando i duplicati. Un esempio dello stesso assomiglia a questo:
Stream<Integer> numStream = Stream.of(43,65,1,98,63,63,1); List<Integer> numList = numStream.distinct() .collect(Collectors.toList()); System.out.println(numList);
Conclusione:
[43, 65, 1, 98, 63]
Spiegazione: In questo caso, il metodo Different() viene utilizzato per numStream . Recupera tutti i singoli elementi in numList rimuovendo i duplicati dal flusso. Come puoi vedere dall'output, non ci sono duplicati, a differenza del flusso di input, che inizialmente aveva due duplicati (63 e 1).

sbirciare()

Questo metodo viene utilizzato per tenere traccia delle modifiche intermedie prima di eseguire un'operazione sul terminale. Ciò significa che peek() può essere utilizzato per eseguire un'operazione su ciascun elemento di uno stream per creare uno stream su cui possono essere eseguite ulteriori operazioni intermedie.
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> nList = numStream.map(n -> n*10) .peek(n->System.out.println("Mapped: "+ n)) .collect(Collectors.toList()); System.out.println(nList);
Conclusione:
Mappato: 430 Mappato: 650 Mappato: 10 Mappato: 980 Mappato: 630 [430, 650, 10, 980, 630]
Spiegazione: qui il metodo peek() viene utilizzato per generare risultati intermedi mentre il metodo map() viene applicato agli elementi dello stream. Qui possiamo notare che anche prima di utilizzare l'operazione terminale Collect() per stampare il contenuto finale della lista nell'istruzione print , il risultato per ogni mappatura degli elementi del flusso viene stampato in sequenza in anticipo.

smistato()

Il metodo sorted() viene utilizzato per ordinare gli elementi di uno stream. Per impostazione predefinita, ordina gli elementi in ordine crescente. È inoltre possibile specificare un ordinamento specifico come parametro. Un esempio di implementazione di questo metodo è simile al seguente:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.sorted().forEach(n -> System.out.println(n));
Conclusione:
1 43 ​​63 65 98
Spiegazione: qui, il metodo sorted() viene utilizzato per ordinare gli elementi dello stream in ordine crescente per impostazione predefinita (poiché non è specificato alcun ordine specifico). Puoi vedere che gli elementi stampati nell'output sono disposti in ordine crescente.

Operazioni sui terminali

Ecco alcuni esempi di alcune operazioni del terminale che possono essere applicate ai flussi Java:

per ciascuno()

Il metodo forEach() viene utilizzato per scorrere tutti gli elementi di uno stream ed eseguire la funzione su ciascun elemento uno per uno. Funziona come alternativa alle istruzioni di ciclo come for , while e altre. Esempio:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.forEach(n -> System.out.println(n));
Conclusione:
43 65 1 98 63
Spiegazione: qui il metodo forEach() viene utilizzato per stampare ogni elemento dello stream uno per uno.

contare()

Il metodo count() viene utilizzato per recuperare il numero totale di elementi presenti nello stream. È simile al metodo size() , che viene spesso utilizzato per determinare il numero totale di elementi in una raccolta. Un esempio di utilizzo del metodo count() con Java Stream è il seguente:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());
Conclusione:
5
Spiegazione: poiché numStream contiene 5 elementi interi, utilizzando il metodo count() su di esso verranno restituiti 5.

raccogliere()

Il metodo Collect() viene utilizzato per eseguire riduzioni mutabili di elementi del flusso. Può essere utilizzato per rimuovere contenuto da uno stream al termine dell'elaborazione. Utilizza la classe Collector per eseguire riduzioni .
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> odd = numStream.filter(n -> n % 2 == 1) .collect(Collectors.toList()); System.out.println(odd);
Conclusione:
[43, 65, 1, 63]
Spiegazione: In questo esempio, tutti gli elementi dispari nel flusso vengono filtrati e raccolti/ridotti in un elenco denominato dispari . Alla fine viene stampato l'elenco dei dispari.

minimo() e massimo()

Il metodo min() , come suggerisce il nome, può essere utilizzato su uno stream per trovare l'elemento minimo al suo interno. Allo stesso modo, il metodo max() può essere utilizzato per trovare l'elemento massimo in uno stream. Cerchiamo di capire come possono essere utilizzati con un esempio:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); int smallest = numStream.min((m, n) -> Integer.compare(m, n)).get(); System.out.println("Smallest element: " + smallest);
numStream = Stream.of(43, 65, 1, 98, 63); int largest = numStream.max((m, n) -> Integer.compare(m, n)).get(); System.out.println("Largest element: " + largest);
Conclusione:
Elemento più piccolo: 1 Elemento più grande: 98
Spiegazione: In questo esempio, abbiamo stampato l'elemento più piccolo in numStream utilizzando il metodo min() e l'elemento più grande utilizzando il metodo max() . Tieni presente che qui, prima di utilizzare il metodo max() , abbiamo aggiunto nuovamente elementi a numStream . Questo perché min() è un'operazione da terminale e distrugge il contenuto dello stream originale, restituendo solo il risultato finale (che in questo caso era l'intero “più piccolo”).

findAny() e findFirst()

findAny() restituisce qualsiasi elemento dello stream come Opzionale . Se il flusso è vuoto, restituirà anche un valore opzionale , che sarà vuoto. findFirst() restituisce il primo elemento dello stream come Opzionale . Come con il metodo findAny() , anche il metodo findFirst() restituisce un parametro Opzionale vuoto se il flusso corrispondente è vuoto. Diamo un'occhiata al seguente esempio basato su questi metodi:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); Optional<Integer> opt = numStream.findFirst();System.out.println(opt); numStream = Stream.empty(); opt = numStream.findAny();System.out.println(opt);
Conclusione:
Opzionale[43] Opzionale.vuoto
Spiegazione: qui, nel primo caso, il metodo findFirst() restituisce il primo elemento dello stream come Opzionale . Quindi, quando il thread viene riassegnato come thread vuoto, il metodo findAny() restituisce un Optional vuoto .

allMatch() , anyMatch() e noneMatch()

Il metodo allMatch() viene utilizzato per verificare se tutti gli elementi in un flusso corrispondono a un determinato predicato e restituisce il valore booleano true se lo fanno, altrimenti restituisce false . Se il flusso è vuoto, restituisce true . Il metodo anyMatch() viene utilizzato per verificare se uno qualsiasi degli elementi in un flusso corrisponde a un determinato predicato. Restituisce vero se è così, falso altrimenti. Se il flusso è vuoto, restituisce false . Il metodo noneMatch() restituisce true se nessun elemento nello stream corrisponde al predicato e false altrimenti. Un esempio per illustrare questo è il seguente:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); boolean flag = numStream.allMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.anyMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.noneMatch(n -> n==1);System.out.println(flag);
Conclusione:
falso vero falso
Spiegazione: Per uno stream numStream contenente 1 come elemento, il metodo allMatch() restituisce false perché tutti gli elementi non sono 1, ma solo uno di essi lo è. Il metodo anyMatch() restituisce true perché almeno uno degli elementi è 1. Il metodo noneMatch() restituisce false perché 1 esiste effettivamente come elemento in questo flusso.

Valutazioni pigre in Java Stream

La valutazione pigra porta a ottimizzazioni quando si lavora con Java Streams in Java 8. Implicano principalmente il ritardo delle operazioni intermedie fino a quando non viene incontrata un'operazione terminale. La valutazione pigra ha il compito di prevenire inutili sprechi di risorse sui calcoli finché il risultato non è effettivamente necessario. Il flusso di output risultante dalle operazioni intermedie viene generato solo dopo il completamento dell'operazione terminale. La valutazione pigra funziona con tutte le operazioni intermedie nei flussi Java. Un uso molto utile della valutazione pigra si verifica quando si lavora con flussi infiniti. In questo modo si evitano molte elaborazioni non necessarie.

Pipeline in Java Stream

Una pipeline in un Java Stream è costituita da un flusso di input, zero o più operazioni intermedie allineate una dopo l'altra e infine un'operazione terminale. Le operazioni intermedie in Java Streams vengono eseguite pigramente. Ciò rende inevitabili le operazioni intermedie in pipeline. Con le pipeline, che sono fondamentalmente operazioni intermedie combinate in ordine, diventa possibile l'esecuzione pigra. Le pipeline aiutano a tenere traccia delle operazioni intermedie che devono essere eseguite dopo che viene finalmente incontrata un'operazione terminale.

Conclusione

Riassumiamo ora ciò che abbiamo imparato oggi. In questo articolo:
  1. Abbiamo dato una rapida occhiata a cosa sono i Java Streams.
  2. Abbiamo quindi imparato molte tecniche diverse per creare thread Java in Java 8.
  3. Abbiamo appreso due tipi principali di operazioni (operazioni intermedie e operazioni terminali) che possono essere eseguite sui flussi Java.
  4. Abbiamo poi esaminato in dettaglio diversi esempi di operazioni sia intermedie che terminali.
  5. Alla fine abbiamo imparato di più sulla valutazione pigra e infine abbiamo imparato a conoscere il pipeline nei thread Java.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION