CodeGym /Kursy /JAVA 25 SELF /Podzbiory Stream API: distinct, limit, skip

Podzbiory Stream API: distinct, limit, skip

JAVA 25 SELF
Poziom 30, Lekcja 2
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 30, lekcja 2
Niedostępne
Zarządzanie kolejką na ekskluzywne przyjęcie 🎉
Zarządzanie kolejką na ekskluzywne przyjęcie 🎉
1
Zadanie
JAVA 25 SELF, poziom 30, lekcja 2
Niedostępne
Tworzenie Top-4 unikalnych wskaźników sprzedaży 💰
Tworzenie Top-4 unikalnych wskaźników sprzedaży 💰
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION