Tym razem omówimy dwa podstawowe struktury danych w Javie: kolekcje i mapy. Kolekcje służą do gromadzenia obiektów natomiast mapy pozwalają na określenie pewnych relacji miedzy nimi. Obie są częścią frameworku, czyli „platformy programistycznej” o nazwie Collections
ale kolekcje implementują interfejs Collections
, natomiast mapy nie.
Kolekcje
Jak sama nazwa sugeruje framework Collections
służy do tworzenia „kolekcji” czyli struktur umożliwiających grupowanie obiektów i ułatwiający pracę z nimi. Dotychczas kiedy chcieliśmy wspólnie operować jakąś grupą obiektów używaliśmy w tym celu tablic lub tworzyliśmy obiekty o odpowiednich polach. Jak spróbuję pokazać, wykorzystanie klas oferowanych przez Collections
pozwala w wielu przypadkach ułatwić pracę z grupami obiektów, choć nie oznacza to, że należy w ogóle zapomnieć o tablicach.
Zanim przejdę do krótkiego omówienia wybranych klas kolekcji przyjrzyjmy się jednej z nich, klasie ArrayList
.
Rzut oka na ArrayList
Na pytanie czym jest ArrayList
można by odpowiedzieć, że jest to lista, a bardziej kolokwialnie, że to jednowymiarowa tablica na sterydach. Najlepiej zobaczyć parę możliwości takiej struktury przykładzie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
package alista; import java.util.ArrayList; import java.util.Random; public class ALista { public static void main(String[] args) { // Obiekt typu Random będzie potrzebny do losowania liczb // zob: http://ggoralski.pl/?p=1702 Random randGen = new Random(); // Tworzenie obiektu typy ArrayList ArrayList<Integer> dane = new ArrayList<Integer>(); // Dodajemy 9 losowych liczb z zakresu 0-9 for (int i = 0; i < 9; i++) { dane.add(randGen.nextInt(10)); } // Dodajemy jeszcze jedną liczbę na końcu dane.add(5); // Drukujemy rozmiar listy i ostatni indeks System.out.println("Lista posiada " + dane.size() + " elementów "); // Drukujemy zawartość listy System.out.println("Zawartość początkowa : "+ dane); // Dodajemy liczbę (8) w konkrentym miejscu (indeksie = 2) // pozostałe liczby "przesuwają się" dane.add(2, 8); System.out.println("Zawartość po dodaniu : "+ dane); // Wstawiamy liczbę (9) w konkrentym miejscu (indeksie = 2) // wstawiona liczba podmienia tą , która tam jest dane.set(2, 9); System.out.println("Zawartość po wstawieniu : "+ dane); // Usuwanie elementu z konkretnego miejsca (indeks = 0) dane.remove(0); System.out.println("Zawartość po usunięciu : "+ dane); // Szukanie liczby 9 int indeks = dane.indexOf(9); System.out.println("Pierwsza liczba 9 jest pod indeksem: "+ indeks); // Usuwanie pierwszej napotkanej liczby 9 // musimy uzyć obiektu typu Integer, sama liczba byłaby int-em // interpretowanym jako indeks Integer l = 9; dane.remove(l); System.out.println("Zawartość po usunięciu 9: "+ dane); // Pobranie elementu z określonego indeksu System.out.println("Piąty element: " + dane.get(4)); // Tworzymy dodatkową listę ArrayList<Integer> dodatkowe = new ArrayList<Integer>(); dodatkowe.add(7); dodatkowe.add(6); System.out.println("dodatkowe: " + dodatkowe.toString()); // Dodajemy zawartość listy dodatkowe na końcu listy dane dane.addAll(dodatkowe); System.out.println("dane: " + dane); // Przekazanie zawartości do tablicy elementów typu Integer Integer[] tablica = new Integer[dane.size()]; tablica = dane.toArray(tablica); System.out.println("tablica: " + tablica.getClass() + ", 1 element: " + tablica[0]); // Usuwanie wszystkich elementów z listy dane.clear(); System.out.println("Po czyszczeniu: " + dane); } } |
Po uruchomieniu wynik powinien przypominać ten, choć wylosowane liczby będą inne:
Lista posiada 10 elementów
Zawartość początkowa : [4, 2, 6, 1, 6, 5, 1, 7, 0, 5]
Zawartość po dodaniu : [4, 2, 8, 6, 1, 6, 5, 1, 7, 0, 5]
Zawartość po wstawieniu : [4, 2, 9, 6, 1, 6, 5, 1, 7, 0, 5]
Zawartość po usunięciu : [2, 9, 6, 1, 6, 5, 1, 7, 0, 5]
Pierwsza liczba 9 jest pod indeksem: 1
Zawartość po usunięciu 9: [2, 6, 1, 6, 5, 1, 7, 0, 5]
Piąty element: 5
dodatkowe: [7, 6]
dane: [2, 6, 1, 6, 5, 1, 7, 0, 5, 7, 6]
tablica: class [Ljava.lang.Integer;, 1 element: 2
Po czyszczeniu: []
Zapewne najbardziej rzucającą się w oczy różnicą między tablicami i ArrayList
jest „rozciągliwość” tej drugiej. Nie musimy z góry ustalać jej rozmiaru, po prostu w miarę potrzeb dodajemy i usuwamy z niej elementy bez zbędnych ceregieli. Inną różnicą jest to, że odwołujemy się do poszczególnych elementów za pomocą metod, do których przekazujemy indeks, jeśli jest taka potrzeba.
Zwróć też uwagę na sposób w jaki utworzyliśmy listę:
ArrayList
W celu zdefiniowania typu przechowywanych w tablicy obiektów użyliśmy formy
znanej z poprzedniej lekcji, w której omawialiśmy typy generyczne.
Trochę o frameworku Collections
Teraz trochę teorii. Framework Collections
, jak wspomniałem powyżej dostarcza wiele klas umożliwiających tworzenie różnych struktur służących przechowywaniu obiektów. Wszystkie mają pewne cechy wspólne a także każda z nich swoiste, dzięki czemu można wybrać taką, która będzie optymalna dla konkretnego zadania. Tutaj omówię zaledwie niektóre z nich, ale zawsze można zajrzeć do szerszych opracowań, jak np. przewodnik po frameworku Collections
Oracle.
Kolekcje mogą przechowywać wyłącznie odwołania, a więc możemy ich użyć do typów obiektowych. Chcąc przechowywać liczby czy inne typy prymitywne, należy użyć wrappingu. Na szczęście działa tu autoboxing i unboxing, dzięki czemu nie musimy ręcznie przekształcać typów prymitywnych na obiektowe. Pewnym wyjątkiem w powyższym kodzie było ręczne utworzenie obiektu typu Integer
, kiedy chcieliśmy usunąć konkretną liczbę. Gdybym umieścił argument 9
w wywołaniu metody, zostałaby ona zinterpretowana jako indeks komórki.
Wybrane Interfejsy frameworku Collections
Częścią frameworku Collections
są dostarczane przez niego podstawowe interfejsy: Collection
, Deque
, List
, NavigableSet
, Queue
, Set
, SortedSet
oraz kilka dodatkowych, jak Comparator
, Spliteator
, Iterator
czy RandomAccess
. Implementacja tych interfejsów przez różne metody sprawia, że ich „obsługa” ma wiele cech wspólnych, a więc jest łatwiejsza, choć trzeba pamiętać, że klasy mogą różnie implementować poszczególne interfejsy. Wszystkie klasy kolekcji implementują także interfejs Iterable
, co umożliwia np. użycie pętli typu foreach
.
Chyba najbardziej podstawowym interfejsem z wymienionych jest oczywiście interfejs Collections
. Jest on rdzeniem wszystkich kolekcji, deklaruje więc zestaw metod, które wszystkie kolekcje powinny implementować, należą do nich na przykład: add()
, clear()
, contains()
, remove()
czy size()
.
Kolejne interfejsy „doprecyzowują” rodzaj kolekcji. Interfejs List
deklaruje metody charakterystyczne dla kolekcji takich jak ArrayList
przechowujących uporządkowaną sekwencję obiektów. Z kolei Set
opisuje metody dla kolekcji, które nie pozwalają na przechowywanie zduplikowanych elementów. SortedSet
oraz NavigableSet
jak można się domyślić, stanowią rozszerzenia Set
. Metody zadeklarowane w Queue
i Deque
są używane przez kolekcje w formie „kolejki”, porządkując je w kolejności w jakiej będą użyte.
Wybrane klasy frameworku Collections
Krótko opisane powyżej interfejsy wskazują jakich podstawowych rodzajów kolekcji możemy używać wykorzystując framework Collections
, przy czym trzeba pamiętać, że poszczególne klasy mogą implementować wiele interfejsów jednocześnie. Dwa zasadnicze typy kolekcji to List
oraz Set
. Jak można wywnioskować z opisu interfejsów, które implementują, pierwszy typ pozwala gromadzić uporządkowane grupy obiektów, które mogą się powtarzać a drugie nie pozwalają na duplikaty. Przyjrzyjmy się teraz krótko niektórym klasom właściwym dla typów kolekcji:
ArrayList
Implementuje interfejs List
, umożliwia przechowywanie sekwencji obiektów. Przykład użycia podałem powyżej.
LinkedList
Oprócz interfejsu List
implementuje Queue
i Deque
, dzięki czemu korzysta m. in. z takich metod jak addFirst()
, getFirst()
, removeFirst()
i analogicznie: addLast()
, getLast()
, removeLast()
, które jak można wywnioskować z nazw ułatwiają pracę na obu końcach listy.
HashSet
Jak sama nazwa wskazuje implementuje interfejs Set
, z kolei pozostała część nazwy informuje, że mamy do czynienia z „tablicą z haszowaniem” („tablicą mieszającą”) co nie wnikając w szczegóły ma takie znaczenie, że elementy w tablicy są porządkowane wg. specjalnych algorytmów tak aby dostęp do nich był jak najszybszy. W efekcie kolejność elementów w kolekcji nie musi odpowiadać kolejności w jakim je dodawaliśmy. Na przykład po wykonaniu takiego kodu:
1 2 3 4 5 6 |
HashSet<String> aminokwasy = new HashSet<String>(); aminokwasy.add("Ile"); aminokwasy.add("Ala"); aminokwasy.add("Cys"); aminokwasy.add("Arg"); System.out.println("Aminokwasy :" + aminokwasy); |
otrzymałem:
Aminokwasy :[Ile, Ala, Arg, Cys]
TreeSet
Ten typ kolekcji przechowuje dane tak aby dostęp do nich był możliwie szybki. Związane z tym jest sortowanie elementów a także przechowywanie ich w drzewie (niestety nie będę tu wyjaśniał co to dokładnie oznacza).
Przykład:
1 2 3 4 5 6 7 |
TreeSet<String> aminokwasy = new TreeSet<String>(); aminokwasy.add("Ile"); aminokwasy.add("Ala"); aminokwasy.add("Cys"); aminokwasy.add("Arg"); System.out.println("Aminokwasy :" + aminokwasy); |
Wynik:
Aminokwasy :[Ala, Arg, Cys, Ile]
Mapy
Mapy to struktury służące do przechowywania danych oparte na powiązaniu: klucz – wartość. Kluczem może być np. numer PESEL, wartością imię i nazwisko przechowywane w jednym string
-u. Klucze muszą być unikalne, wartości mogą się powtarzać. Mapy są w pewnym stopniu podobne do opisanych powyżej kolekcji, ale w przeciwieństwie do nich nie implementują interfejsu Iterable
, zatem nie możemy na nich stosować, przynajmniej bezpośrednio, pętli typu foreach
, ponadto służą nie tylko gromadzeniu obiektów ale także pozwalają na zachowanie relacji między nimi.
Podobnie jak w przypadku kolekcji mapy implementują charakterystyczne dla nich interfejsy: Map
, Map.Entry
, NavigableMap
, SortedMap
. Najbardziej podstawowym z nich jest oczywiście Map
, który deklaruje m. in. takie podstawowe metody jak put()
, get()
czy remove()
.
Klasy implementujące wspomniane interfejsy to: HashMap
, EnumMap
, TreeMap
, WeakHashMap
, LinkedHashMap
oraz IdentityHashMap
. Ponadto istnieje AbstractMap
, która jest „szkieletem implementacji” interfejsu Map
i jest klasą nadrzędną wobec poprzednio wymienionych.
Ogólny sposób działania map pokażę na przykładzie HashMap
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import java.util.*; public class Mapy { public static void main(String[] args) { // Tworzę HashMap, kluczami i wartościami bedą string-i HashMap<String, String> kodony = new HashMap<>(); // Wprowadzam kilka par klucz-wartość // kluczami są kodony, wartościami aminokwasy, które kodony kodują kodony.put("UUU", "Phe"); kodony.put("UUC", "Phe"); kodony.put("UUA", "Leu"); kodony.put("CUU", "Leu"); kodony.put("AGG", "Arg"); // Drukowanie zawartości mapy System.out.println("kodony: " + kodony); // Pobieranie wartości odpowiadającej kluczowi "UUA" System.out.println("UUA znaczy: " + kodony.get("UUA")); // Możemy zmodyfikować wartość przypisaną do klucza, jeśli klucza nie ma // zostanie dołączona nowa para: kodony.put("UUA", "błędny kodon"); System.out.println("kodony: " + kodony); // Jak wyżej, ale jeśli klucza nie ma, nie zostanie dołączona para kodony.replace("AAA", "Lys"); System.out.println("kodony: " + kodony); // Usuwanie kodony.remove("UUA"); System.out.println("kodony: " + kodony); // Sprawdzamy, czy mapa zawiera dany klucz System.out.println("Czy mapa zawiera klucz \"UUA\"? " + kodony.containsKey("UUA")); // Sprawdzamy, czy mapa zawiera daną wartość System.out.println("Czy mapa zawiera wartość \"Leu\"? " + kodony.containsValue("Leu")); // Teraz spróbujemy pobrać kolejne pary klucz-wartość // Ponieważ Map nie implementuje interfejsy Iterable, nie możemy // bezpośrednio uzyć pętli foreach, ale obejdziemy to // Tworzymy kolekcję typu Set przechowujące obiekty Map.Entry, // które przechowują pary klucz-wartość // Metoda entrySet() zwraca cały sestaw tych par z mapy Set<Map.Entry<String, String>> pary = kodony.entrySet(); // iterujemy po obekcie typu Set System.out.println("\nIteracja:"); for (Map.Entry<String, String> para: pary) { System.out.println("klucz: " + para.getKey() + " wartość: " + para.getValue()); } } } |
Działanie podstawowych metod i technik opisane jest w komentarzach kodu.
Leave a Reply
You must be logged in to post a comment.