Grafika wprowadzająca do lekcji 21 kursu Java "Zbiory". Pod tytułem hasło: "Poznaj Set w Javie i pracuj z unikalnymi danymi!".

Kurs Java #21 Zbiory

Java

Loading

W poprzednich lekcjach omawialiśmy tablice w Javie, które narzucają sztywny rozmiar i wymagają ręcznego zarządzania indeksami. Kolekcje i listy w Javie częściowo rozwiązują ten problem, ale nadal nie kontrolują duplikatów danych. W praktyce szybko pojawia się potrzeba zastosowania struktur, które automatycznie pilnują unikalności.

W tej lekcji poznamy zbiory w Javie (Set), które eliminują powtarzające się elementy i dbają o czystość danych w profesjonalnych systemach. Jeśli nie zapoznaliście się jeszcze z tematem kolekcji i list, warto wrócić do poprzedniej lekcji, ponieważ Set opiera się bezpośrednio na tych samych podstawach i bez tego kontekstu łatwo zgubić logikę działania tej struktury.

Wprowadzenie

Wyobraź sobie, że sporządzasz listę gości na imprezę i używasz do tego zwykłego notatnika. Spisujesz nazwiska jedno po drugim. Jeśli wpiszesz to samo nazwisko dwa razy, lista tego nie wykryje i powstanie bałagan przy wejściu. Musiałbyś za każdym razem sprawdzać całą listę od początku. Przy tysiącu osób zajmie to mnóstwo czasu i energii. Zbiór działa jak inteligentne pudełko z unikalnymi przegródkami. System błyskawicznie weryfikuje obecność elementu przed jego dodaniem. Jeśli spróbujesz wrzucić osobę, która już tam jest, pudełko jej nie przyjmie. Ta analogia unikalnego pudełka idealnie oddaje mechanizm działania zbiorów. Tutaj priorytetem jest brak powtórzeń, a nie kolejność wpisów na kartce.

Zbiory (Set)

Zbiory w Javie (Set) to kolekcje należące do Collections Framework, czyli zestawu gotowych rozwiązań do zarządzania grupami obiektów, które przechowują wyłącznie unikalne elementy. W przeciwieństwie do list nie korzystamy z indeksów. Zamiast pobierać element, sprawdzamy jego obecność w zbiorze.

To zmienia sposób myślenia o danych. W tablicach przechowujesz dane o stałym rozmiarze i pełnej kontroli indeksów, w listach przechowujesz dane w kolejności, a w zbiorach pilnujesz unikalności. W praktyce oznacza to, że nie operujesz na indeksach, ponieważ Set ich nie udostępnia. Nie masz też gwarancji kolejności zwracanych danych w większości implementacji, więc nie możesz zakładać, w jakiej kolejności elementy zostaną odczytane. Zamiast pobierania konkretnych wartości, częściej sprawdzasz ich obecność w zbiorze, ponieważ to jest główna operacja tej struktury.

Set nie zastępuje listy. Rozwiązuje inny problem.

Rodzaje implementacji

Implementujemy różne wersje interfejsu Set w zależności od tego, czy zależy nam na maksymalnej szybkości działania, czy na zachowaniu określonej kolejności danych.

HashSet

Najczęściej używaną implementacją zbioru w Javie jest HashSet. Jego działanie opiera się na mechanizmie tablicy mieszającej (hashing), dzięki czemu bardzo szybko odnajdujemy element lub sprawdzamy jego obecność. Można to porównać do szafki z ponumerowanymi przegródkami. Nie przeszukujemy wszystkiego po kolei, tylko od razu trafiamy do właściwego miejsca.

W praktyce czas sprawdzenia, czy element istnieje w zbiorze, pozostaje taki sam niezależnie od liczby danych. Nie ma znaczenia, czy pracujemy na kilku elementach, czy na milionach rekordów. Każdy obiekt trafia do konkretnego miejsca na podstawie wartości hashCode, co pozwala szybko potwierdzić jego obecność.

HashSet wybieramy wtedy, gdy liczy się wydajność i unikalność danych. W zamian tracimy kontrolę nad kolejnością. Elementy mogą być zwracane w różnej kolejności i nie można na tym polegać przy budowaniu logiki.

Przykład:

Przykładowy kod implementacji zbioru HashSet w Javie.

Na początku implementujemy zbiór Set<String> jako HashSet, który przechowuje unikalne wartości bez zachowania kolejności. Dodajemy elementy takie jak "Kasia", "Adam", "Zosia" oraz "Bartek" metodą add(), w dowolnej kolejności, ale struktura nie gwarantuje żadnego porządku podczas zwracania danych. Jeśli spróbujemy dodać ponownie element "Adam", zostanie on automatycznie zignorowany, ponieważ Set nie dopuszcza duplikatów. Następnie wypisujemy całą zawartość zbioru, gdzie widzimy unikalne dane, ale ich kolejność może być losowa. Na końcu sprawdzamy obecność elementu "Kasia" za pomocą metody contains(), która zwraca true, jeśli element znajduje się w zbiorze.

TreeSet

Najbardziej uporządkowaną implementacją zbioru w Javie jest TreeSet. W przeciwieństwie do HashSet nie opieramy działania na mieszaniu danych, lecz na strukturze drzewa. Dzięki temu elementy nie są rozrzucone w losowy sposób, ale tworzą logiczną, posortowaną całość.

Można to porównać do słownika. Nie przeglądamy wszystkich stron po kolei, tylko zawężamy obszar poszukiwań, aż trafimy na właściwe miejsce.

W praktyce operacje w TreeSet są wolniejsze niż w HashSet, ale nadal przewidywalne. Wraz ze wzrostem liczby danych czas wyszukiwania rośnie, jednak w sposób uporządkowany.

TreeSet wybieramy wtedy, gdy oprócz unikalności zależy nam na zachowaniu kolejności. Dane są zawsze posortowane, więc nie musimy wykonywać dodatkowego sortowania.

W zamian tracimy część wydajności względem HashSet. Dodatkowo elementy muszą być porównywalne, aby struktura wiedziała, jak je uporządkować.

Przykład:

Przykładowy kod implementacji zbioru TreeSet w Javie.

Na początku implementujemy zbiór Set<String> jako TreeSet, który przechowuje wyłącznie unikalne wartości i automatycznie sortuje je według naturalnego porządku. Dodajemy elementy takie jak "Kasia", "Adam" oraz "Zosia" metodą add(), w dowolnej kolejności, ale struktura sama układa je alfabetycznie. Jeśli ponownie dodamy element "Adam", zostanie on automatycznie zignorowany, ponieważ Set nie dopuszcza duplikatów. Następnie wypisujemy całą zawartość zbioru, gdzie widzimy unikalne i automatycznie uporządkowane dane. Na końcu sprawdzamy obecność elementu "Kasia" za pomocą metody contains(), która zwraca true, jeśli element znajduje się w zbiorze.

Operacje CRUD na zbiorach

Praca ze zbiorami również opiera się na schemacie CRUD, ale brak indeksów wymusza inne podejście do zarządzania danymi niż w przypadku list.

Metoda add() w zbiorach zwraca wartość logiczną. Jeśli element już istnieje, metoda zwraca false i system nie modyfikuje kolekcji. Metoda contains() błyskawicznie potwierdza obecność obiektu bez użycia pętli. Ponieważ nie używasz indeksów, nie znajdziesz tu metody get(i) ani set(i). Aby zaktualizować dane, najpierw wycinasz stary obiekt metodą remove(), a następnie wprowadzasz nową wersję.

Iteracje

Pętla for-each

Najprostszy i najczęściej używany sposób przechodzenia po zbiorze ponieważ Set nie posiada indeksów.

Przykład:

for (String imie : imiona) {
    System.out.println(imie);
}

Przechodzimy po każdym elemencie zbioru i wykonujemy operację bez dostępu do pozycji.

Pętla for

Pętla for nie ma zastosowania w klasycznej formie, ponieważ Set nie udostępnia indeksów liczbowych. Nie możemy odwołać się do elementu przez jego pozycję, tak jak w listach, ponieważ zbiór nie przechowuje danych w strukturze opartej na indeksach. Zamiast tego operujemy na bezpośrednich wartościach i sprawdzamy ich obecność w kolekcji.

Iterator

Iterator stosujemy wtedy, gdy chcemy bezpiecznie usuwać elementy podczas iteracji. To jedyny bezpieczny sposób modyfikacji zbioru w trakcie przechodzenia po nim.

Przykład:

Iterator<String> it = imiona.iterator();

while (it.hasNext()) {
    String imie = it.next();

    if (imie.equals("Anna")) {
        it.remove();
    }
}

Iterator przechodzi po zbiorze element po elemencie. Metoda next() zwraca aktualny element, a remove() usuwa go bezpośrednio ze zbioru. Dzięki temu nie dochodzi do błędów modyfikacji struktury podczas iteracji.

Podsumowanie

Zbiory wprowadzają do Twojego kodu automatyczną kontrolę unikalności, której brakuje w listach. Implementując interfejs Set, zyskujesz pewność, że system nie dopuści do powstania duplikatów, co oszczędza Twój czas na ręczne sprawdzanie danych. Najważniejszą różnicą technologiczną jest rezygnacja z indeksów na rzecz błyskawicznego sprawdzania obecności elementu.

HashSet stawia na wydajność, a TreeSet na uporządkowanie danych. Wybór zależy od tego, czy ważniejsza jest szybkość, czy kolejność.

W kolejnej lekcji przechodzimy do Map, gdzie wchodzimy w model klucz–wartość i rozszerzamy pracę z danymi o nowe możliwości.

O autorze

Adam Mingielewicz

Tester oprogramowania z pasją do jakości i technologii. Łączy doświadczenie w testach manualnych i automatycznych, koncentrując się przede wszystkim na aplikacjach webowych oraz usługach SOAP i REST. Testowanie to dla niego nie tylko szukanie błędów, ale przede wszystkim kwestionowanie przyjętych założeń i usprawnianie tego, co nie działa – również w samym procesie testowym. Lubi zmieniać, angażować się i aktywnie budować proces testowy, tak by miał sens, a nie tylko formę. Prywatnie fan rocka, biwakowania, podróży.