CodeGym /Kursy /JAVA 25 SELF /Operacje union, intersection, difference

Operacje union, intersection, difference

JAVA 25 SELF
Poziom 32 , Lekcja 2
Dostępny

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.

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