CodeGym /Corsi /JAVA 25 SELF /Stream primitivi e costo del boxing

Stream primitivi e costo del boxing

JAVA 25 SELF
Livello 33 , Lezione 0
Disponibile

1. Problema: perché gli Stream normali non sono sempre efficienti

Quando si lavora con collezioni di numeri in Java e si usano gli stream normali (Stream<Integer>, Stream<Double>), sotto il cofano avviene il “boxing” e l’“unboxing” dei valori primitivi in oggetti wrapper (Integer, Double ecc.). È comodo, ma non sempre efficiente:

  • Boxing — trasformazione di un primitivo (int) in un oggetto (Integer).
  • Unboxing — operazione inversa: da oggetto a primitivo.

Problema:
Il boxing/unboxing comporta operazioni extra e consumo di memoria. Nei “punti caldi” (invocati spesso) dell’applicazione ciò può portare a un calo sensibile delle prestazioni, soprattutto se si elaborano grandi array di numeri.

Esempio:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);

Qui ogni numero è un oggetto Integer, non un primitivo int.

2. Stream primitivi: IntStream, LongStream, DoubleStream

Per evitare il boxing/unboxing superfluo, in Java esistono stream primitivi:

  • IntStream — per int
  • LongStream — per long
  • DoubleStream — per double

Operano esclusivamente su primitivi e non creano wrapper superflui.

Come creare uno stream primitivo?

Da un array:

int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);

Con range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 inclusi

Generazione di numeri casuali:

new Random().ints(5, 0, 100)    // 5 int casuali da 0 a 99

Esempio: somma degli elementi di un array

int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15

3. Conversione tra Stream<T> e stream primitivi

A volte si ha uno stream normale, altre volte uno primitivo. Per convertire si usano metodi specifici:

  • mapToInt, mapToLong, mapToDouble — convertono uno stream normale in uno primitivo.
  • boxed() — converte uno stream primitivo di nuovo in uno stream di oggetti.

Esempio:

List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3

Al contrario:

IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();

Attenzione:
boxed() è l’operazione inversa: crea di nuovo oggetti wrapper (Integer, Double ecc.). Se puntate alla massima prestazione — evitatelo nei “punti caldi”.

4. Somma e statistiche: sum, average, min, max, summaryStatistics

Gli stream primitivi offrono metodi comodi per l’aggregazione:

  • sum() — somma di tutti gli elementi
  • average() — media (restituisce OptionalDouble)
  • min(), max() — minimo e massimo (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — restituisce un oggetto con statistiche complete (somma, media, minimo, massimo, conteggio)

Esempio:

int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum());      // 15
System.out.println(stats.getAverage());  // 3.0
System.out.println(stats.getMin());      // 1
System.out.println(stats.getMax());      // 5
System.out.println(stats.getCount());    // 5

Confronto con Collectors:
Per gli stream normali si può usare Collectors.summarizingInt, ma spesso è meno efficiente dei metodi degli stream primitivi:

List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));

5. Evitare l’autoboxing: dove è importante e come misurarlo

Dove è critico?

  • In cicli e stream che elaborano grandi volumi di numeri (ad esempio gestione di array, statistiche, matematica, parsing dei dati).
  • Nei “punti caldi” — codice invocato spesso e determinante per le prestazioni.

Perché è importante?

  • Ogni volta che avviene il boxing si crea un nuovo oggetto wrapper (Integer, Double ecc.).
  • Questo aumenta il carico sul garbage collector (GC).
  • Nei microbenchmark la differenza può essere di ordine multiplo!

Come misurare?
Per un confronto accurato si usa il framework JMH (Java Microbenchmark Harness). Consente di confrontare in modo affidabile le prestazioni del codice con e senza boxing.

Esempio (pseudocodice):

@Benchmark
public int sumIntStream() {
    return IntStream.range(0, 1_000_000).sum();
}

@Benchmark
public int sumStreamInteger() {
    return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}

La seconda variante sarà più lenta a causa della creazione continua di oggetti Integer.

Conclusione:
Se vi interessa la performance — usate gli stream primitivi!

6. Quando gli stream primitivi aiutano davvero e quando non conviene complicare

Usate gli stream primitivi se:

  • lavorate con grandi array/collezioni di numeri.
  • dovete calcolare rapidamente somma, media, minimo, massimo.
  • scrivete codice in cui ogni millisecondo conta (per esempio elaborazione dati in tempo reale).

Potete non preoccuparvene se:

  • la collezione è piccola (poche decine o meno di elementi).
  • il codice diventa troppo complesso a causa delle conversioni tra tipi.
  • le prestazioni non sono critiche (per esempio l’elaborazione dell’input utente).

Esempio:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Si può fare, ma va bene anche un normale reduce

Consiglio: non trasformate il codice in una “giungla” per un’ottimizzazione microscopica. Usate gli stream primitivi dove è davvero giustificato.

7. OptionalInt, OptionalDouble: estrarre i risultati in modo sicuro

I metodi min(), max(), average() degli stream primitivi non restituiscono un semplice numero, ma un “contenitore” — OptionalInt, OptionalDouble ecc. Serve per gestire in sicurezza gli stream vuoti (ad esempio se l’array è vuoto).

Esempio:

int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
    System.out.println("Minimo: " + min.getAsInt());
} else {
    System.out.println("L'array è vuoto!");
}

Confronto con Optional standard:

  • OptionalInt — per int
  • OptionalDouble — per double
  • OptionalLong — per long

Perché non restituire semplicemente 0?
Perché 0 può essere un valore valido, mentre uno stream vuoto è un caso a parte.

Esempio con average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // se vuoto — restituirà NaN

8. Pratica: esempi d’uso degli stream primitivi

Esempio 1: somma dei quadrati dei numeri da 1 a 1000

int sum = IntStream.rangeClosed(1, 1000)
                   .map(x -> x * x)
                   .sum();
System.out.println(sum);

Esempio 2: filtraggio e conteggio dei numeri pari

int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
                   .filter(x -> x % 2 == 0)
                   .count();
System.out.println("Numeri pari: " + count);

Esempio 3: confronto con un normale Stream<Integer>

List<Integer> list = IntStream.range(0, 1_000_000)
                              .boxed()
                              .collect(Collectors.toList());

long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");

t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");

Su grandi volumi la differenza sarà evidente.

9. Errori tipici nell’uso degli stream primitivi

Errore n. 1: dimenticare il boxing durante la conversione dei tipi.
Se usate boxed(), ricordate che crea di nuovo oggetti wrapper. Non usatelo se non necessario.

Errore n. 2: usare stream primitivi per oggetti.
IntStream funziona solo con int, non con oggetti. Se dovete lavorare con oggetti — usate un normale Stream<T>.

Errore n. 3: ignorare OptionalInt/OptionalDouble.
Se chiamate min(), max(), average() — verificate sempre che il risultato sia presente (isPresent()), altrimenti otterrete un’eccezione.

Errore n. 4: conversioni troppo complesse tra Stream<T> e IntStream.
Se il codice diventa illeggibile a causa dei continui mapToInt()boxed()mapToDouble() — forse conviene semplificare la logica.

Errore n. 5: aspettarsi un’accelerazione “magica” su collezioni piccole.
Per liste piccole la differenza tra Stream e IntStream è minima. Non conviene complicare il codice per un risparmio microscopico.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION