1. Operacja union (suma zbiorów)
W Javie do reprezentowania zbiorów używany jest interfejs Set<T>. W odróżnieniu od list (List) zbiory gwarantują unikalność elementów i z reguły nie dbają o porządek (chyba że użyjesz specjalnych implementacji, np. LinkedHashSet). Najpopularniejsze implementacje — HashSet i TreeSet. Ich główne zadanie — szybko sprawdzać obecność elementu i zapewniać brak duplikatów.
Kiedy potrzebne są zbiory?
- Gdy ważna jest unikalność: na przykład lista wszystkich unikalnych użytkowników, którzy odwiedzili stronę.
- Gdy trzeba szybko sprawdzać obecność elementu: metoda contains w HashSet zwykle działa w czasie stałym.
- Gdy potrzebne są typowe operacje na zbiorach: suma, część wspólna, różnica.
Union — to suma dwóch i więcej zbiorów: wynik zawiera wszystkie elementy z obu zbiorów wejściowych (bez duplikatów).
Przykład w praktyce
Załóżmy, że mamy dwa zbiory uczniów uczęszczających na kółka „Robotyka” i „Programowanie”:
Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");
Chcemy otrzymać zbiór wszystkich uczniów, którzy chodzą przynajmniej na jedno kółko.
Rozwiązanie z użyciem Stream API
Najprostszy sposób — połączyć oba strumienie i zebrać je do Set:
Set<String> all = Stream.concat(
robotics.stream(),
programming.stream()
).collect(Collectors.toSet());
System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha]
Wyjaśnienie:
- Stream.concat łączy dwa strumienie.
- collect(Collectors.toSet()) zbiera elementy do zbioru (automatycznie usuwając duplikaty).
Alternatywa: więcej niż dwa zbiory
Jeżeli mamy trzy i więcej kółek, używamy Stream.of i flatMap:
Set<String> math = Set.of("Zhenya", "Vika", "Boris");
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha, Zhenya]
Dlaczego właśnie Set?
Ponieważ Set automatycznie usuwa duplikaty. Jeśli zbierzesz do List, te same imiona pojawią się wielokrotnie.
2. Operacja intersection (część wspólna zbiorów)
Intersection — to elementy, które występują jednocześnie w obu zbiorach.
Przykład w praktyce
Znaleźć uczniów, którzy chodzą i na „Robotykę”, i na „Programowanie”:
Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");
Rozwiązanie z użyciem Stream API:
Set<String> both = robotics.stream()
.filter(programming::contains)
.collect(Collectors.toSet());
System.out.println(both); // [Vika]
Wyjaśnienie:
Przechodzimy po wszystkich uczestnikach „Robotyki” i filtrujemy tylko tych, którzy są w „Programowaniu”. Efekt — zbiór z imionami, które występują w obu kółkach.
Alternatywny sposób (bez Stream API)
Można użyć wbudowanej metody retainAll (modyfikuje bieżący zbiór):
Set<String> intersection = new HashSet<>(robotics);
intersection.retainAll(programming);
System.out.println(intersection); // [Vika]
Ale w kontekście tematu Stream API skupiamy się na strumieniach.
3. Operacja difference (różnica zbiorów)
Difference — to elementy pierwszego zbioru, których nie ma w drugim.
Przykład w praktyce
Znaleźć uczniów, którzy chodzą tylko na „Robotykę”, ale nie na „Programowanie”:
Set<String> robotics = Set.of("Anya", "Boris", "Vika");
Set<String> programming = Set.of("Vika", "Gleb", "Dasha");
Rozwiązanie z użyciem Stream API:
Set<String> onlyRobotics = robotics.stream()
.filter(name -> !programming.contains(name))
.collect(Collectors.toSet());
System.out.println(onlyRobotics); // [Anya, Boris]
Wyjaśnienie:
Filtrujemy uczestników „Robotyki”, pozostawiając tylko tych, których nie ma w „Programowaniu”.
Alternatywny sposób (bez Stream API)
Set<String> difference = new HashSet<>(robotics);
difference.removeAll(programming);
System.out.println(difference); // [Anya, Boris]
4. Praktyczne zadania: przetwarzanie list użytkowników
Zadanie 1: Znaleźć uczniów, którzy chodzą tylko na jedno kółko
Trzeba ustalić, kto chodzi tylko na „Robotykę” albo tylko na „Programowanie”, ale nie na oba naraz. To różnica symetryczna (xor dla zbiorów):
Set<String> onlyOne = Stream.concat(
robotics.stream().filter(name -> !programming.contains(name)),
programming.stream().filter(name -> !robotics.contains(name))
).collect(Collectors.toSet());
System.out.println(onlyOne); // [Anya, Boris, Gleb, Dasha]
Zadanie 2: Lista wszystkich unikalnych uczniów z kilku kółek
Set<String> all = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
System.out.println(all); // [Anya, Boris, Vika, Gleb, Dasha, Zhenya]
Zadanie 3: Znaleźć uczniów, którzy nie chodzą na żadne kółko
Załóżmy, że mamy listę wszystkich uczniów klasy:
Set<String> allStudents = Set.of("Anya", "Boris", "Vika", "Gleb", "Dasha", "Zhenya", "Igor’", "Katya");
Trzeba ustalić, kto nie chodzi na żadne kółko:
Set<String> attendees = Stream.of(robotics, programming, math)
.flatMap(Set::stream)
.collect(Collectors.toSet());
Set<String> notInAny = allStudents.stream()
.filter(name -> !attendees.contains(name))
.collect(Collectors.toSet());
System.out.println(notInAny); // [Igor’, Katya]
5. Ważne uwagi: equals, hashCode i wydajność
Dlaczego ważna jest poprawna implementacja equals i hashCode?
Wszystkie operacje na zbiorach (Set) zależą od poprawności metod equals i hashCode. Jeśli przechowujesz obiekty własnej klasy (np. Student), koniecznie przesłoń te metody, w przeciwnym razie porównania będą działać niepoprawnie.
Przykład:
class Student {
String name;
int age;
// Pamiętaj o nadpisaniu equals i hashCode!
}
Jeśli tego nie zrobisz, dwóch studentów o tych samych imionach i wieku będzie traktowanych jako różne obiekty przez Set.
Dlaczego lepiej używać Set niż List?
- Operacja contains w Set działa szybko (zwykle w czasie stałym).
- W List wyszukiwanie elementu odbywa się w czasie liniowym, co może być krytyczne dla dużych kolekcji.
- Dla operacji na zbiorach (union, intersection, difference) Set jest znacznie wydajniejszy i logiczniejszy.
6. Typowe błędy przy pracy z operacjami na zbiorach
Błąd nr 1: Używanie List zamiast Set do operacji na zbiorach. Jeśli zbierasz elementy do List, duplikaty nie są usuwane, a operacja contains działa wolno. Dla union/intersection/difference używaj Set.
Błąd nr 2: Brak implementacji equals/hashCode dla obiektów. Jeśli w Set przechowujesz obiekty własnej klasy, ale nie przesłoniłeś metod equals i hashCode, część wspólna i różnica będą działać „dziwnie” — obiekty, które semantycznie są takie same, nie będą uznawane za równe.
Błąd nr 3: Modyfikowanie kolekcji w trakcie działania strumienia. Jeśli bezpośrednio w strumieniu próbujesz modyfikować źródłowy Set (np. dodawać lub usuwać elementy), otrzymasz ConcurrentModificationException. Zawsze pracuj na nowym zbiorze.
Błąd nr 4: Nieoczywista utrata porządku. HashSet nie gwarantuje porządku elementów. Jeśli porządek jest ważny — użyj LinkedHashSet lub TreeSet.
Błąd nr 5: Używanie Stream.concat dla więcej niż dwóch kolekcji. Stream.concat łączy tylko dwa strumienie. Dla większej liczby użyj Stream.of(...) i flatMap.
Błąd nr 6: Problemy z null. Zbiory nie lubią wartości null, zwłaszcza jeśli używasz Set.of(...) — on nie dopuszcza null. Do pracy z null użyj innych implementacji lub filtruj wartości z wyprzedzeniem.
GO TO FULL VERSION