1. Metoda distinct: usuwamy duplikaty
W realnych zadaniach często trzeba nie tylko filtrować i przekształcać dane, lecz także wybierać z kolekcji wyłącznie unikalne elementy, ograniczać rozmiar wyniku lub, przeciwnie, pomijać pierwsze kilka elementów. Na przykład:
- Uzyskać listę unikalnych imion użytkowników.
- Wziąć tylko pierwsze 10 wpisów do wyświetlenia na stronie.
- Pominąć pierwsze 5 elementów (np. przy implementacji paginacji — „pokaż od 2. strony”).
Do takich zadań w Stream API są specjalne metody: distinct, limit, skip.
Jak działa distinct?
Metoda distinct() zwraca nowy strumień, w którym usunięto wszystkie duplikaty elementów. Duplikaty określane są za pomocą metod equals i hashCode odpowiedniej klasy.
Wyobraź sobie, że jesteś numizmatykiem i zależy ci, aby w głównej kolekcji nie było duplikatów. Wszystkie powtarzające się monety trafią do funduszu wymiany. Właśnie distinct() potrafi odsiać duplikaty z głównej kolekcji.
Przykład: unikalne imiona użytkowników
List<String> names = List.of(
"Alicja", "Bob", "Alicja", "Ewa", "Bob", "Denis", "Gleb", "Ewa"
);
Uzyskamy listę unikalnych imion:
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames);
// Wynik: [Alicja, Bob, Ewa, Denis, Gleb]
Przykład: unikalne adresy email użytkowników
Załóżmy, że mamy klasę User:
public class User {
String name;
String email;
// Konstruktor, gettery, toString() — dla wygody
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return name + " <" + email + ">";
}
}
Lista użytkowników z duplikatami email:
List<User> users = List.of(
new User("Alicja", "alice@mail.com"),
new User("Bob", "bob@mail.com"),
new User("Ewa", "eva@mail.com"),
new User("Alicja2", "alice@mail.com"), // duplikat email!
new User("Gleb", "gleb@mail.com"),
new User("Ewa2", "eva@mail.com") // duplikat email!
);
Jeśli wywołasz po prostu users.stream().distinct(), to duplikaty nie zostaną usunięte, ponieważ w obiektach User domyślnie metody equals i hashCode nie są nadpisane. W takim przypadku distinct działa tylko dla referencyjnych porównań.
Rozwiązanie: Nadpisać equals i hashCode tak, aby unikalność była określana po 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);
}
Teraz:
List<User> uniqueUsers = users.stream()
.distinct()
.collect(Collectors.toList());
uniqueUsers.forEach(System.out::println);
// Alicja <alice@mail.com>
// Bob <bob@mail.com>
// Ewa <eva@mail.com>
// Gleb <gleb@mail.com>
Ważne: Dla własnych klas zawsze nadpisuj equals i hashCode, jeśli chcesz, aby distinct działał „po ludzku”!
2. Metoda limit: ograniczamy liczbę elementów
Metoda limit(long maxSize) zwraca nowy strumień, który zawiera nie więcej niż maxSize pierwszych elementów strumienia źródłowego.
Analogicznie: Przychodzisz do cukierni i chcesz spróbować tylko 3 ciastka ze 100. limit(3) — i dostajesz dokładnie trzy pierwsze, pozostałe — „niestety, nie dziś”.
Przykład: pierwsze 3 imiona
List<String> firstThree = names.stream()
.limit(3)
.collect(Collectors.toList());
System.out.println(firstThree);
// Wynik: [Alicja, Bob, Alicja]
Przykład z naszą aplikacją: top‑3 nowych użytkowników
List<User> firstUsers = users.stream()
.limit(3)
.collect(Collectors.toList());
firstUsers.forEach(System.out::println);
// Alicja <alice@mail.com>
// Bob <bob@mail.com>
// Ewa <eva@mail.com>
Przykład z sortowaniem
Można to połączyć z sortowaniem — na przykład wziąć top‑2 użytkowników z najkrótszym adresem e‑mail:
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>
// Ewa <eva@mail.com>
3. Metoda skip: pomijamy pierwsze elementy
Metoda skip(long n) zwraca nowy strumień, w którym pominięto pierwsze n elementów strumienia źródłowego.
Przykład: pominąć pierwsze 2 imiona
List<String> afterTwo = names.stream()
.skip(2)
.collect(Collectors.toList());
System.out.println(afterTwo);
// Wynik: [Alicja, Ewa, Bob, Denis, Gleb, Ewa]
Przykład: paginacja (pagination)
Często na stronie trzeba wyświetlać np. „po 3 użytkowników na stronę”. Dla drugiej strony trzeba pominąć pierwsze 3, a wziąć kolejne 3:
int pageSize = 3;
int pageNumber = 2; // Druga strona
List<User> page = users.stream()
.skip(pageSize * (pageNumber - 1))
.limit(pageSize)
.collect(Collectors.toList());
page.forEach(System.out::println);
// Alicja2 <alice@mail.com>
// Gleb <gleb@mail.com>
// Ewa2 <eva@mail.com>
4. Łączenie distinct, limit, skip
Te metody można i warto łączyć — w zależności od zadania.
Przykład: uzyskać 2 unikalne imiona, zaczynając od trzeciego w kolejności
List<String> result = names.stream()
.distinct() // Usuwamy duplikaty: [Alicja, Bob, Ewa, Denis, Gleb]
.skip(2) // Pomijamy Alicję i Boba: [Ewa, Denis, Gleb]
.limit(2) // Bierzemy tylko dwóch: [Ewa, Denis]
.collect(Collectors.toList());
System.out.println(result);
// Wynik: [Ewa, Denis]
Przykład z filtrowaniem
Załóżmy, że trzeba uzyskać pierwsze 2 unikalne adresy email, które zawierają literę „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);
// Wynik: [alice@mail.com, eva@mail.com]
5. Praktyka: zadania do zastosowania
Zadanie 1. Uzyskać listę unikalnych imion użytkowników, których długość jest większa niż 3 znaki
List<String> longUniqueNames = names.stream()
.filter(name -> name.length() > 3)
.distinct()
.collect(Collectors.toList());
System.out.println(longUniqueNames);
// Na przykład: [Alicja, Denis]
Zadanie 2. Uzyskać 3. i 4. unikalny email z listy użytkowników
List<String> thirdAndFourthEmail = users.stream()
.map(user -> user.email)
.distinct()
.skip(2)
.limit(2)
.collect(Collectors.toList());
System.out.println(thirdAndFourthEmail);
// Na przykład: [eva@mail.com, gleb@mail.com]
Zadanie 3. Uzyskać pierwsze 5 unikalnych liczb większych niż 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);
// Wynik: [12, 17, 23, 42, 19]
6. Schemat wizualny: kolejność stosowania operacji
graph TD
A[Lista wejściowa] --> B[filter]
B --> C[distinct]
C --> D[skip]
D --> E[limit]
E --> F[collect]
Komentarz:
Zwykle najpierw stosuje się filter, potem usuwa duplikaty distinct, następnie używa skip i limit, a na końcu — collect. Jednak kolejność można zmieniać, jeśli tego wymaga zadanie.
7. Typowe błędy przy pracy z distinct, limit, skip
Błąd nr 1: Oczekiwanie, że distinct usuwa duplikaty po „dowolnej” cesze.
W rzeczywistości distinct działa według metody equals obiektu. Jeśli chcesz unikalność po jakimś polu (np. tylko po email), a nie po całym obiekcie — trzeba albo nadpisać equals/hashCode, albo użyć trików z Collectors.toMap() lub dodatkowego filtrowania.
Błąd nr 2: Naruszenie kolejności operacji.
Jeśli najpierw zastosujesz limit, a potem distinct, to duplikaty mogą pozostać — w końcu ograniczyłeś strumień do pierwszych N elementów, a wśród nich mogą być takie same.
Błąd nr 3: Pomijanie przez skip więcej elementów, niż jest dostępnych.
Jeśli spróbujesz pominąć więcej elementów, niż jest w strumieniu, wynik będzie po prostu pustą listą — błędu nie będzie, ale rezultat może zaskoczyć.
Błąd nr 4: Nieuwzględniona wydajność.
Metody distinct, limit, skip mogą być nieefektywne na bardzo dużych strumieniach, szczególnie jeśli strumień jest nieuporządkowany lub elementy są złożone. W 99 % codziennych zadań to nie problem, ale jeśli pracujesz z milionami rekordów — warto się nad tym zastanowić.
Błąd nr 5: Zapomniana nadpisana logika equals/hashCode dla własnych klas.
Jeśli pracujesz z obiektami domenowymi (np. User), to bez nadpisania tych metod distinct uzna obiekty za różne, nawet jeśli są „logicznie” takie same.
GO TO FULL VERSION