Kolekcje - Java
Kolekcja to struktura danych — w praktyce obiekt — przechowująca referencje do innych obiektów. Zazwyczaj kolekcje zawierają referencje do obiektów dowolnego typu, który znajduje się w związku „jest” z typem wskazanym w kolekcji. Interfejsy frameworku kolekcji deklasują operacje, które można wykonywać w sposób uogólniony na różnych rodzajach kolekcji. Rysunek 16.1 przedstawia
niektóre interfejsy frameworku kolekcji. Kilka implementacji tych interfejsów znajduje się we frameworku, ale można również zapewnić własne implementacje.
Interfejs | Opis |
Collection | Główny interfejs hierarchii kolekcji, na podstawie którego powstały interfejsy Set, Queue i List. |
Set | Kolekcja, która nie zawiera duplikatów. |
List | Uporządkowana kolekcja, która może zawierać duplikaty. |
Map | Kolekcja, która wiąże klucze z wartościami i nie może zawierać duplikatów kluczy. Ten interfejs nie dziedziczy po Collection. |
Queue | Najczęściej kolekcja typu pierwsze weszło, pierwsze wychodzi, która modeluje kolejkę oczekujących; można jednak zdefiniować inny porządek kolekcji. |
Rysunek 16.1. Wybrane interfejsy frameworku kolekcji
Kolekcje bazujące na typie Object
Klasy i interfejsy frameworku kolekcji stanowią część pakietu java.util. We wczesnych wersjach języka Java klasy frameworku przechowywały i modyfikowały jedynie referencje typu Object, co umożliwiało przechowywanie w kolekcji dowolnego obiektu, ponieważ wszystkie klasy bezpośrednio lub pośrednio dziedziczą po Object. W praktyce w większości sytuacji programy przetwarzają konkretne typy obiektów. Oznaczało to konieczność rzutowania w dół referencji Object na odpowiedni typ, aby przetworzyć obiekt. Jak wspomnieliśmy w rozdziale 10., rzutowania w dół najlepiej unikać.
Kolekcje uogólnione
Aby wyeliminować opisany problem, framework kolekcji został wzbogacony o mechanizm typów generycznych (uogólnionych).
Mechanizm uogólnienia pozwala określić konkretny typ przechowywany w kolekcji, dzięki czemu możliwe jest wykorzystanie mechanizmów sprawdzania typów na etapie kompilacji — kompilator zgłosi błąd w przypadku próby przypisania do kolekcji nieodpowiedniego typu. Gdy określimy typ przechowywany w kolekcji, wszystkie referencje pobierane z kolekcji będą dokładnie tego typu. Eliminuje to potrzebę jawnego rzutowania, które mogłoby zgłosić wyjątek ClassCastExceptions, jeśli referencja do obiektu nie jest odpowiedniego typu. Co ważne, kolekcje uogólnione są zgodne wstecz z kodem Javy, który był pisany przed wprowadzeniem tego udogodnienia.
Dobra praktyka programistyczna
Nie wynajduj koła na nowo — zamiast tworzyć własne struktury danych, użyj interfejsów i kolekcji z frameworku kolekcji Javy, które zostały dobrze przetestowane i dostosowane do wymagań większości aplikacji.
Wybór kolekcji
Dokumentacja każdej kolekcji opisuje wymagania pamięciowe i charakterystykę wydajnościową metod obsługujących operacje takie jak dodawanie i usuwanie elementów, wyszukiwanie elementów, sortowanie itp. Przed wyborem kolekcji przeczytaj dokumentację związaną z kategorią kolekcji (Set, List, Map, Queue itd.), a dopiero później zdecyduj się na konkretną implementację.
Klasy otoczkowe
Każdy typ podstawowy (dodatek D) ma odpowiadającą mu klasę otoczkową (z pakietu java.lang). Klasy te noszą nazwy Boolean, Byte, Character, Double, Float, Integer, Long i Short. Dzięki nim typy podstawowe mogą być traktowane jak obiekty. Każda z liczbowych klas otoczkowych — Byte, Short, Integer, Long, Float i Double — rozszerza klasę Number. Wszystkie wymienione klasy otoczkowe są finalne, więc nie można ich rozszerzać. Typy podstawowe nie mają żadnych metod, więc związane z nimi metody trafiły do klas otoczkowych (np. metoda parseInt zamieniająca tekst na typ int znajduje się w klasie Integer).
Automatyczne pakowanie i rozpakowywanie
Java zapewnia mechanizm, który automatycznie pakuje i rozpakowuje typy podstawowe, czyli konwertuje je do wersji otoczkowej i z wersji otoczkowej. Pakowanie zamienia wartość typu podstawowego na odpowiedni obiekt typu otoczkowego. Rozpakowanie zamienia obiekt klasy otoczkowej na typ podstawowy. Konwersje te — czyli automatyczne pakowanie i rozpakowanie — zachodzą
w pełni automatycznie. Przyjrzyjmy się następującym instrukcjom:
integerArray[0] = 10; // Przypisz obiekt Integer z wartością 10 do integerArray[0]
int value = integerArray[0]; // Pobierz obiekt Integer jako typ int
W tym przypadku automatyczne pakowanie ma miejsce, gdy przypisujemy wartość typu int (10) do integerArray[0], ponieważ tablica integerArray przechowuje referencje do obiektów Integer, a nie wartości typu int. Automatyczne rozpakowywanie
zachodzi, gdy wydobywamy wartość integerArray[0] do zmiennej value typu int, ponieważ value przechowuje wartość jako liczbę całkowitą, a nie referencję do obiektu typu Integer. Konwersja będzie zachodziła również w przypadku, kiedy zamiast typu boolean użyjemy obiektu typu Boolean.
Interfejs Collection i klasa Collections
Interfejs Collection zawiera operacje zbiorcze (czyli operacje wykonywane na całej kolekcji) dla operacji takich jak dodawanie, czyszczenie i porównywanie obiektów (lub elementów) z kolekcji. Obiekt Collection można zamienić na tablicę. Dodatkowo interfejs Collection zapewnia metody zwracające obiekt Iterator, co pozwala na przejście przez kolekcję i usuwanie jej elementów w trakcie iteracji. Klasę Iterator opisujemy w punkcie 16.6.1. Inne metody interfejsu umożliwiają określenie rozmiaru kolekcji i sprawdzenie, czy jest pusta.
Obserwacja z poziomu inżynierii oprogramowania
Interfejsu Collection używa się najczęściej jako typu dla parametrów metod, które umożliwiają polimorficzne przetwarzanie wszystkich obiektów implementujących interfejs Collection.
Obserwacja z poziomu inżynierii oprogramowania
Większość implementacji kolekcji zapewnia konstruktor, który przyjmuje argument typu Collection, a zatem umożliwia konstrukcję nowej kolekcji na podstawie elementów istniejącej kolekcji.
Klasa Collections zapewnia przydatne metody statyczne do wyszukiwania, sortowania i wykonywania innych operacji na kolekcjach.
Listy
Interfejs List (nazywany również sekwencją) to kolekcja elementów ułożona w pewnym porządku, która może zawierać duplikaty. Podobnie jak w przypadku tablic, obiekty List zaczynają swoje indeksy od 0 (czyli pierwszy element ma indeks o numerze 0). Poza metodami odziedziczonymi z interfejsu Collection interfejs List zapewnia metody do modyfikowania elementów na podstawie ich
indeksów, modyfikowania zestawu elementów na podstawie zakresu indeksów, wyszukiwania elementów i pobierania ListIterator w celu dostępu do elementów.
Interfejs List jest implementowany przez kilka klas, w tym klasy ArrayList i LinkedList. Automatyczne pakowanie ma miejsce, gdy dodaje się wartości typów podstawowych do tych klas, ponieważ mogą one przechowywać tylko obiekty. Klasa ArrayList to implementacja List jako tablicy o zmiennej długości. Wstawianie elementów między istniejące elementy ArrayList jest operacją niewydajną — wszystkie elementy po nowym muszą zostać przesunięte, co jest kosztowne, jeśli tablica jest długa. Klasa LinkedList zapewnia wydajne wstawianie (lub usuwanie) elementów ze środka kolekcji, ale jest znacznie mniej wydajna od ArrayList
w kwestii losowego dostępu do elementów ze środka kolekcji.
Programowanie w Javie. Solidna wiedza w praktyce. Wydanie XI, Autorzy: Paul Deitel, Harvey Deitel, Wydawnictwo: Helion