Guida per Principianti sugli Stream di Java

Ciao there, futuri maghi di Java! Oggi ci imbarcheremo in un viaggio emozionante nel mondo degli Stream di Java. Non preoccuparti se sei nuovo alla programmazione - sarò il tuo guida amichevole, e prenderemo tutto passo per passo. Alla fine di questo tutorial, sarai in grado di gestire i dati come un professionista!

Java - Streams

Cos'è uno Stream in Java?

Immagina di essere in un ristorante sushi a nastro. Le piastre di sushi passano continuamente davanti a te, e puoi scegliere ciò che desideri. Questo è essenzialmente ciò che uno Stream è in Java - una sequenza di elementi che puoi processare uno per uno, senza doverli tutti conservare in memoria contemporaneamente.

Gli Stream sono stati introdotti in Java 8 per rendere più facile ed efficiente lavorare con insiemi di dati. Loro ci permettono di eseguire operazioni sui dati in modo più dichiarativo, dicendo a Java cosa vogliamo fare piuttosto che come farlo.

Creazione di Stream in Java

Iniziamo creando il nostro primo Stream. Ci sono diversi modi per farlo, ma inizieremo con un esempio semplice:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {
public static void main(String[] args) {
// Creazione di uno Stream da una Lista
List<String> fruits = Arrays.asList("apple", "banana", "cherry", "date");
Stream<String> fruitStream = fruits.stream();

// Creazione di uno Stream direttamente
Stream<Integer> numberStream = Stream.of(1, 2, 3, 4, 5);
}
}

In questo esempio, abbiamo creato due Stream. Il primo, fruitStream, è creato da una Lista di frutti. Il secondo, numberStream, è creato direttamente utilizzando il metodo Stream.of().

Operazioni Comuni sugli Stream

Ora che abbiamo i nostri Stream, esaminiamo alcune operazioni comuni che possiamo eseguire su di essi.

Metodo forEach

Il metodo forEach è come un robot amichevole che attraversa ciascun elemento nel tuo Stream e fa qualcosa con esso. Utilizziamolo per stampare i nostri frutti:

fruits.stream().forEach(fruit -> System.out.println(fruit));

Questo stamperà:

apple
banana
cherry
date

Qui, fruit -> System.out.println(fruit) è chiamato una espressione lambda. È un modo conciso per dire a Java cosa fare con ciascun elemento.

Metodo map

Il metodo map è come una bacchetta magica che trasforma ciascun elemento nello Stream. Utilizziamolo per convertire tutti i nostri frutti in maiuscolo:

List<String> upperCaseFruits = fruits.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upperCaseFruits);

Questo stamperà:

[APPLE, BANANA, CHERRY, DATE]

String::toUpperCase è un riferimento a metodo, un'altra feature carina di Java che è come dire "usa il metodo toUpperCase su ciascuna String".

Metodo filter

Il metodo filter è come un bouncer in un club, che lascia passare solo determinati elementi. Utilizziamolo per conservare solo i frutti che iniziano con la lettera 'a':

List<String> aFruits = fruits.stream()
.filter(fruit -> fruit.startsWith("a"))
.collect(Collectors.toList());
System.out.println(aFruits);

Questo stamperà:

[apple]

Metodo limit

Il metodo limit è come dire "Voglio solo questi, grazie!" Limita lo Stream a un certo numero di elementi:

List<String> firstTwoFruits = fruits.stream()
.limit(2)
.collect(Collectors.toList());
System.out.println(firstTwoFruits);

Questo stamperà:

[apple, banana]

Metodo sorted

Il metodo sorted è come ordinare il tuo scaffale di libri. Mette gli elementi in ordine:

List<String> sortedFruits = fruits.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedFruits);

Questo stamperà:

[apple, banana, cherry, date]

Elaborazione Parallela

Una delle cose interessanti sugli Stream è che possono essere facilmente elaborati in parallelo, potenzialmente accelerando le operazioni su grandi insiemi di dati. Puoi creare uno Stream parallelo così:

fruits.parallelStream().forEach(fruit -> System.out.println(fruit + " " + Thread.currentThread().getName()));

Questo potrebbe stampare qualcosa come:

banana ForkJoinPool.commonPool-worker-1
apple main
date ForkJoinPool.commonPool-worker-2
cherry ForkJoinPool.commonPool-worker-3

L'ordine potrebbe essere diverso ogni volta che lo esegui, perché i frutti vengono elaborati in parallelo!

Collectors

I Collectors sono strumenti speciali che ci aiutano a raccogliere i risultati delle operazioni sugli Stream. Abbiamo utilizzato collect(Collectors.toList()) nei nostri esempi per convertire i risultati degli Stream nuovamente in Liste. Ci sono molti altri Collectors utili:

// Unire gli elementi in una Stringa
String fruitString = fruits.stream().collect(Collectors.joining(", "));
System.out.println(fruitString);  // Stampa: apple, banana, cherry, date

// Contare gli elementi
long fruitCount = fruits.stream().collect(Collectors.counting());
System.out.println(fruitCount);  // Stampa: 4

// Raggruppare gli elementi
Map<Character, List<String>> fruitGroups = fruits.stream()
.collect(Collectors.groupingBy(fruit -> fruit.charAt(0)));
System.out.println(fruitGroups);  // Stampa: {a=[apple], b=[banana], c=[cherry], d=[date]}

Statistiche

Gli Stream possono anche aiutarci a calcolare statistiche sui dati numerici:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println("Count: " + stats.getCount());
System.out.println("Sum: " + stats.getSum());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Average: " + stats.getAverage());

Questo stamperà:

Count: 5
Sum: 15
Min: 1
Max: 5
Average: 3.0

Esempio di Stream di Java

Mettiamo tutto insieme con un esempio più complesso. Immagina di avere una lista di studenti con le loro età e voti:

class Student {
String name;
int age;
double grade;

Student(String name, int age, double grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}

public class StreamExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Alice", 22, 90.5),
new Student("Bob", 20, 85.0),
new Student("Charlie", 21, 92.3),
new Student("David", 23, 88.7)
);

// Ottenere la media dei voti degli studenti over 21
double averageGrade = students.stream()
.filter(student -> student.age > 21)
.mapToDouble(student -> student.grade)
.average()
.orElse(0.0);

System.out.println("Media dei voti degli studenti over 21: " + averageGrade);

// Ottenere i nomi dei migliori 2 studenti per voto
List<String> topStudents = students.stream()
.sorted((s1, s2) -> Double.compare(s2.grade, s1.grade))
.limit(2)
.map(student -> student.name)
.collect(Collectors.toList());

System.out.println("Migliori 2 studenti: " + topStudents);
}
}

Questo esempio dimostra come possiamo concatenare più operazioni sugli Stream per eseguire complesse attività di elaborazione dei dati in modo conciso e leggibile.

Ecco fatto, gente! Hai appena fatto i tuoi primi passi nel mondo degli Stream di Java. Ricorda, la pratica fa la perfezione, quindi non aver paura di sperimentare con questi concetti. Prima di sapere, sarai in grado di gestire i dati come un professionista! Buon coding!

Credits: Image by storyset