Kurs Java

Kolekcje - Sety

Kolejną kolekcją w Javie są Sety, inaczej mówiąc zbiory. Dostarczane są one za pośrednictwem interfejsu Set z pakietu java.util. Sety wyglądają z wierzchu trochę podobnie do list, jednak ich implementacja i podejście do danych jest zupełnie inne. Przyjrzyjmy się teraz najważniejszym cechom zbiorów:
  • Nie są dozwolone duplikaty elementów.

  • Do elementów w secie nie odwołujemy się po pozycji, gdyż nie wiadomo na jakiej pozycji znajduje się dany element.

  • Mogą być sortowalne lub nie - zależy od konkretnej implementacji interfejsu.

Po co mi sety skoro mam listy?

Każdy na początku zadaje sobie pytanie dotyczące użyteczności setów, których konstrukcja wydaje się być bardziej zagmatwana od list i do tego ma swoje ograniczenia. Mimo tego sety są bardzo często używane i nie można ich pominąć w trakcie nauki. Dzieje się tak ze względu na ich specjalną właściwość:

Unikalność elementów

W secie nie mogą istnieć dokładnie dwa takie same obiekty. Zawsze będzie przechowywany tylko jeden. Zatem jeśli mamy ciąg liczb:

7, 2, 9, 1, 5, 7, 6, 3, 9
i taki ciąg wpiszemy do seta, to odczytując wszystkie elementy zbioru otrzymamy tylko pojedyncze wartości:

7, 2, 9, 1, 5, 6, 3

W ten sposób można w łatwy sposób odfiltrować duplikaty. Wystarczy wprowadzić je do seta. Wyciągniemy z niego jedynie niezduplikowane elementy.

Weryfikacja unikalności - kontrakt equals/hashCode

To czy dany element znajduje się w zbiorze jest weryfikowne przy pomocy metod hashCode i equals. Metody te (występujące w każdym obiekcie) wiąże istotny kontrakt (opisujący relację między nimi). Polega on na tym, że jeśli dwa obiekty są równe według metody equals to oznacza, że muszą mieć te same "haszkody" (metoda hashCode zwraca dla nich tę samą wartość). Natomiast jeśli obiekty mają równe "haszkody" niekoniecznie muszą być równe według porówanania metodą equals.

Popularne implementacje zbiorów

Najpopularniejszymi zbiorami są implementacje dostarczane przez klasy:
  • HashSet z pakietu java.util

    Bardzo często stosowana impelmentacja. Elementy są nieposortowane. Ich kolejność nie odpowiada również kolejności wkładania do zbioru. Może przechowywać null-e.

  • LinkedHashSet z pakietu java.util

    Implementacja przechowująca elementy w kolejności ich dodawania. Zatem może być przydatna jeśli zależy nam zarówno na unikalności jak i na tworzeniu historii unikalnych wpisów. Może przechowywać null-e.
  • TreeSet z pakietu java.util

    Przechowuje zbiór pod postacią drzewa. Elementy są poukładane w sposób posortowany (rosnąco). Przydaje się gdy chcemy zapewnić unikalność elementów oraz podstawowe sortowanie. Nie może przechowywać null-i.

Tworzenie zbioru

Dobrą praktyką jest zadeklarowanie instancji zbioru z parametrem typu, na przykład:
Set<Object> listAnything = new HashSet<Object>();
Set<Integer> listNumbers = new HashSet<Integer>();
Set<String> linkedWords = new LinkedHashSet<String>();
Set<String> sortedWords = new TreeSet<String>();
Podobnie jak to miało miejsce w przypadku list, tak samo tutaj możemy określić jakiego typu obiekty mogą być przechowywane w danym zbiorze. Dzięki temu już na etapie kompilacji jesteśmy w stanie się zorientować, czy nie popełniliśmy błędu - na przykład przekazując obiekt klasy String do zbioru, w którym możemy przechowywać tylko obiekty klasy Integer. W takiej sytuacji dowiemy się o tym od razu, gdyż po prostu otrzymamy błąd kompilacji.

Java 7

Od Javy 7 możemy usunąć parametr typu po prawej stronie, co upraszcza zapis do następującej postaci:
Set<Integer> listNumbers = new HashSet<>();
Set<String> linkedWords = new LinkedHashSet<>();
Set<String> sortedWords = new TreeSet<>();

Java 9

Od Javy 9 możemy utworzyć zbiór z ustalonego zestawu elementów:
Set<Integer> listNumbers = Set.of(1, 2, 3, 4, 5, 6);

Java 10

Od Javy 10 można jeszcze bardziej skrócić tworzenie kolekcji używając słowa kluczowego var:
var items = new HashSet<Item>();
Niemniej to rozwiązanie ma pewne ograniczenia, a także wady, dlatego warto jest wstrzymać się z jego masowym użyciem (będziemy jeszcze o tym pisać w przyszłości).

Podstawowe operacje na zbiorach

Podstawowymi operacjami jakie możemy wykonywać na zbiorach jest dodawanie oraz usuwanie elementów. Operacje te są realizowane przez następujące metody:
  • add(<obiekt>)

    Umożliwia ona dodanie elementu do seta. Co ważne - wymagane jest dodanie elementów tego samego typu (lub podtypu) co parametr typu zadeklarowanego przez seta.
    Set<String> strings = new HashSet<String>();
    // Poniższe elementy zostaną dodane do seta - wszystkie są typu String:
    strings.add("One");
    strings.add("Two");
    strings.add("Three");
    // Poniższy element nie zostanie dodany do seta - kompilator zgłosi błąd:
    strings.add(123);
    
    Dodawanie elementów podtypów zadeklarowanego typu:
    Set<Number> linkedNumbers = new LinkedHashSet<>();
    linkedNumbers.add(new Integer(123));
    linkedNumbers.add(new Float(3.1415));
    linkedNumbers.add(new Double(299.988));
    linkedNumbers.add(new Long(67000));
    
    Pamiętajmy, że w przypadku setów nie możemy mówić o indeksie elementu. W przypadku LinkedHashSet mamy jedynie zapewnioną kolejność, czyli podczas przeglądania zbioru otrzymamy elementy zgodne z kolejnością ich dodawania.

  • remove(<obiekt>)

    Metoda usuwa element ze zbioru.
    strings.remove("One");
    
Wymieniliśmy tutaj zaledwie kilka podstawowych metod, które umożliwiają pracę ze zbiorami. Z biegiem czasu należy zapoznać się również z innymi metodami. Uruchamiając mechanizm podpowiedzi w IDE - na przykład w IntelliJ - można łatwo poznać jakie metody są dostępne (obrazek zawiera podpowiedź dla seta linkedNumbers):
Lista metod seta
Po tym wstępie z łatwością zrozumiecie jak działają najważniejsze z metod. Natomiast do trudniejszych będziemy jeszcze wracać w naszym kursie.

Przeglądanie zawartości seta

Zbiory implementują interfejs Iterable, co umożliwia przeglądanie ich element po elemencie. W tym celu wykorzystujemy interfejs Iterator. Jego działanie polega na przeglądaniu kolekcji dopóki po danym elemencie występuje kolejny element. Pobranie bieżącego elementu i przejście do następnego wykonywane jest za pomocą metody next.
Set<Number> someNumbers = new HashSet<>();
someNumbers.add(new Integer(123));
someNumbers.add(new Float(3.1415));
someNumbers.add(new Double(299.988));
someNumbers.add(new Long(67000));

Iterator<Number> someNumbersIterator = someNumbers.iterator();

while(someNumbersIterator.hasNext()) {
    System.out.println(someNumbersIterator.next());
}
Przekazanie zadania iteracji zewnętrznemu mechanizmowi (obiektowi klasy Iterator) powoduje, że nie zależymy od implementacji naszej struktury danych. Trzeba bowiem pamiętać, że nie tylko sety możemy iterować w Javie (na przykład w poprzednim rozdziale iterowaliśmy po liście).

Natomiast bardzo często stosowanymi rozwiązaniami do przeglądania zbiorów są udoskonalone pętle for (for-each):
Set<Number> someNumbers = new HashSet<>();
someNumbers.add(new Integer(123));
someNumbers.add(new Float(3.1415));
someNumbers.add(new Double(299.988));
someNumbers.add(new Long(67000));

for (Number number : someNumbers) {
    System.out.println(number);
}
albo też zupełna nowość od Javy w wersji 8, a więc strumienie. To jednak jest już zupełnie inne, obszerne zagadnienie, które poruszymy omawiając kompleksowo nowości wprowadzone w Javie 8.

Jak modyfikować obiekty w setach?

Omawiając listy wspomnieliśmy o metodzie set, która pozwalała nam na aktualizację obiektu na danej pozycji. W przypadku setów nie mamy indeksów i nie mamy metody set. Co nam zatem pozostaje? Otóż możemy iterować po kolejnych obiektach seta i porównywać je po wybranym polu w celu znalezienia właściwego obiektu, a gdy już to zrobimy możemy wtedy zaktualizować jego dane.

Inną opcją jest usunięcie obiektu z seta metodą remove i dodanie go na nowo w zaktualizowanej wersji za pomocą metody add.
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Masz pytanie dotyczące tego rozdziału? Zadaj je nam!
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.
kursjava@javappa.com

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE