1. Metodo distinct: rimuoviamo i duplicati
Nei problemi reali spesso è necessario non solo filtrare e trasformare i dati, ma anche selezionare dalla collezione solo gli elementi unici, limitare la dimensione del risultato oppure, al contrario, saltare i primi elementi. Ad esempio:
- Ottenere l’elenco dei nomi utente univoci.
- Prendere solo le prime 10 voci da visualizzare sulla pagina.
- Saltare i primi 5 elementi (ad esempio nella paginazione — «mostrare dalla 2ª pagina»).
Per questi casi in Stream API ci sono metodi specifici: distinct, limit, skip.
Come funziona distinct?
Il metodo distinct() restituisce un nuovo stream in cui tutti i duplicati degli elementi sono stati rimossi. I duplicati sono determinati tramite i metodi equals e hashCode della classe corrispondente.
Immagina di essere un numismatico, e ti interessa che nella collezione principale non ci siano doppioni. Tutte le monete ripetute andranno nel fondo scambi. Proprio distinct() è in grado di scartare i duplicati dalla collezione principale.
Esempio: nomi utente univoci
List<String> names = List.of(
"Alice", "Bob", "Alice", "Eva", "Bob", "Denis", "Gleb", "Eva"
);
Otteniamo l’elenco dei nomi univoci:
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames);
// Output: [Alice, Bob, Eva, Denis, Gleb]
Esempio: email utente univoche
Supponiamo di avere la classe User:
public class User {
String name;
String email;
// Costruttore, getter, toString() — per comodità
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " <" + email + ">";
}
}
Elenco di utenti con email duplicate:
List<User> users = List.of(
new User("Alice", "alice@mail.com"),
new User("Bob", "bob@mail.com"),
new User("Eva", "eva@mail.com"),
new User("Alice2", "alice@mail.com"), // duplicato dell'email!
new User("Gleb", "gleb@mail.com"),
new User("Eva2", "eva@mail.com") // duplicato dell'email!
);
Se chiami semplicemente users.stream().distinct(), i duplicati non verranno rimossi, perché negli oggetti User per impostazione predefinita i metodi equals e hashCode non sono sovrascritti. In tal caso distinct funziona solo per le coincidenze di riferimento.
Soluzione: Sovrascrivere equals e hashCode in modo che l’unicità sia determinata da email.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(email, user.email);
}
@Override
public int hashCode() {
return Objects.hash(email);
}
Ora:
List<User> uniqueUsers = users.stream()
.distinct()
.collect(Collectors.toList());
uniqueUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
// Gleb <gleb@mail.com>
Importante: Per le tue classi sovrascrivi sempre equals e hashCode se vuoi che distinct funzioni «come si deve»!
2. Metodo limit: limitiamo la quantità di elementi
Il metodo limit(long maxSize) restituisce un nuovo stream che contiene non più dei primi maxSize elementi dello stream originale.
Analogia: Vai in pasticceria e vuoi assaggiare solo 3 pasticcini su 100. limit(3) — e ti danno esattamente i primi tre, gli altri — «non oggi».
Esempio: i primi 3 nomi
List<String> firstThree = names.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(firstThree);
// Output: [Alice, Bob, Alice]
Esempio con la nostra app: top‑3 nuovi utenti
List<User> firstUsers = users.stream()
.limit(3)
.collect(Collectors.toList());
firstUsers.forEach(System.out::println);
// Alice <alice@mail.com>
// Bob <bob@mail.com>
// Eva <eva@mail.com>
Esempio con ordinamento
Si può combinare con l’ordinamento — per esempio, prendere i top‑2 utenti con l’email più corta:
List<User> top2ShortEmail = users.stream()
.sorted(Comparator.comparingInt(u -> u.email.length()))
.limit(2)
.collect(Collectors.toList());
top2ShortEmail.forEach(System.out::println);
// Bob <bob@mail.com>
// Eva <eva@mail.com>
3. Metodo skip: saltiamo i primi elementi
Il metodo skip(long n) restituisce un nuovo stream in cui vengono saltati i primi n elementi dello stream originale.
Esempio: saltare i primi 2 nomi
List<String> afterTwo = names.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(afterTwo);
// Output: [Alice, Eva, Bob, Denis, Gleb, Eva]
Esempio: paginazione (pagination)
Spesso su un sito bisogna mostrare, ad esempio, «3 utenti per pagina». Per la seconda pagina bisogna saltare i primi 3, prendere i successivi 3:
int pageSize = 3;
int pageNumber = 2; // Seconda pagina
List<User> page = users.stream()
.skip(pageSize * (pageNumber - 1))
.limit(pageSize)
.collect(Collectors.toList());
page.forEach(System.out::println);
// Alice2 <alice@mail.com>
// Gleb <gleb@mail.com>
// Eva2 <eva@mail.com>
4. Combinazione di distinct, limit, skip
Questi metodi si possono e si devono combinare — a seconda del compito.
Esempio: ottenere 2 nomi univoci a partire dal terzo in ordine
List<String> result = names.stream()
.distinct() // Rimuoviamo i duplicati: [Alice, Bob, Eva, Denis, Gleb]
.skip(2) // Saltiamo Alice e Bob: [Eva, Denis, Gleb]
.limit(2) // Prendiamo solo due: [Eva, Denis]
.collect(Collectors.toList());
System.out.println(result);
// Output: [Eva, Denis]
Esempio con filtraggio
Supponiamo di dover ottenere le prime 2 email univoche che contengono la lettera «a»:
List<String> emails = users.stream()
.map(user -> user.email)
.filter(email -> email.contains("a"))
.distinct()
.limit(2)
.collect(Collectors.toList());
System.out.println(emails);
// Output: [alice@mail.com, eva@mail.com]
5. Pratica: esercizi di applicazione
Esercizio 1. Ottenere l’elenco dei nomi utente univoci la cui lunghezza è maggiore di 3 caratteri
List<String> longUniqueNames = names.stream()
.filter(name -> name.length() > 3)
.distinct()
.collect(Collectors.toList());
System.out.println(longUniqueNames);
// Ad esempio: [Alice, Denis]
Esercizio 2. Ottenere il 3° e il 4° email univoco dall’elenco degli utenti
List<String> thirdAndFourthEmail = users.stream()
.map(user -> user.email)
.distinct()
.skip(2)
.limit(2)
.collect(Collectors.toList());
System.out.println(thirdAndFourthEmail);
// Ad esempio: [eva@mail.com, gleb@mail.com]
Esercizio 3. Ottenere i primi 5 numeri unici che sono maggiori di 10
List<Integer> numbers = List.of(5, 12, 17, 5, 23, 17, 42, 19, 12, 8);
List<Integer> result = numbers.stream()
.filter(n -> n > 10)
.distinct()
.limit(5)
.collect(Collectors.toList());
System.out.println(result);
// Output: [12, 17, 23, 42, 19]
6. Schema visivo: ordine di applicazione delle operazioni
graph TD
A[Lista di partenza] --> B[filter]
B --> C[distinct]
C --> D[skip]
D --> E[limit]
E --> F[collect]
Commento:
Di solito si applica prima filter, poi si rimuovono i duplicati con distinct, quindi si usano skip e limit e infine — collect. Ma l’ordine può essere cambiato, se il compito lo richiede.
7. Errori tipici nell’uso di distinct, limit, skip
Errore n. 1: aspettarsi che distinct rimuova i duplicati secondo «qualsiasi» criterio.
In realtà, distinct lavora in base al metodo equals dell’oggetto. Se vuoi l’unicità in base a un campo (ad esempio solo su email), e non sull’intero oggetto — devi o sovrascrivere equals/hashCode, oppure usare accorgimenti con Collectors.toMap() o un filtraggio aggiuntivo.
Errore n. 2: ordine delle operazioni errato.
Se applichi prima limit e poi distinct, i duplicati possono rimanere — perché hai limitato lo stream ai primi N elementi, e tra questi potrebbero esserci dei duplicati.
Errore n. 3: saltare con skip più elementi di quanti ce ne siano.
Se provi a saltare più elementi di quelli presenti nello stream, il risultato sarà semplicemente una lista vuota — non ci sarà errore, ma il risultato potrebbe sorprendere.
Errore n. 4: prestazioni non considerate.
I metodi distinct, limit, skip possono essere inefficienti su stream molto grandi, specialmente se lo stream è non ordinato o se gli elementi sono complessi. Nel 99% dei casi d’uso comuni non è un problema, ma se lavori con milioni di record — vale la pena pensarci.
Errore n. 5: dimenticare la logica sovrascritta di equals/hashCode per le proprie classi.
Se lavori con oggetti personalizzati (ad esempio User), senza sovrascrivere questi metodi distinct considererà gli oggetti diversi anche se sono «logicamente» uguali.
GO TO FULL VERSION