Celem laboratorium jest zapoznanie się z mechanizmem interfejsów oraz specjalnym typem struktury danych do przechowywania wielu obiektów - mapą.
Najważniejsze zadania:
- Stworzenie klasy
RectangularMapdo przechowywania wielu zwierząt i ich pozycji z wykorzystaniemHashMap. - Zabezpieczenie kontraktów między mapą a zwierzęciem.
- Konsolowa wizualizacja mapy podczas symulacji.
- Testy integracyjne.
-
Przyjrzyj się interfejsom
WorldMaporazMoveValidator, które znajdują się w katalogu z tym konspektem i skopiuj je do swojego projektu. Skopiuj także klasęMapVisualizer- wykorzystasz ją w dalszych zadaniach. Umieść wszystkie klasy w odpowiednich pakietach, zgodnie z deklaracjami w ich nagłówkach. -
W pakiecie
agh.ics.oop.modelzdefiniuj klasęRectangularMap. -
Podstawowym atrybutem klasy
RectangularMappowinna być struktura przechowująca zwierzęta na ich pozycjach. Do realizacji takiego odwzorowania wykorzystaj mapę (słownik). Z każdą aktualną pozycją zwierzęcia (Vector2d) powiążesz obiektAnimal. Deklaracja typu takiego atrybutu powinna wyglądać tak:Map<Vector2d, Animal> animals = new HashMap<>();. -
Uzupełnij brakującą logikę w
RectangularMapzgodnie z wytycznymi:-
definiuje prostokątną mapę - posiada szerokość oraz wysokość,
-
implementuje interfejs
WorldMap, -
w konstruktorze akceptuje dwa parametry
widthorazheightwskazujące szerokość oraz wysokość mapy (możesz założyć że otrzymane wartości są poprawne), -
umożliwia poruszanie się w obrębie zdefiniowanego prostokąta (jak w laboratorium 3),
-
umożliwia występowanie więcej niż jednego zwierzęcia na mapie,
-
uniemożliwia występowanie więcej niż jednego zwierzęcia na tej samej pozycji,
-
posiada metodę
toStringrysującą aktualną konfigurację mapy (wykorzystaj klasęMapVisualizer, która znajduje się w tym katalogu).Pamiętaj by zaimplementować wszystkie metody narzucone przez interfejs!
-
-
Zmodyfikuj klasę
Animal:- metoda
move()powinna od teraz przyjmowaćMoveValidatori używać go do sprawdzania, czy zwierzę może zmienić swoją pozycję na wcześniej wyliczoną. Wykorzystaj ten argument podczas realizacji ruchu w metodzieRectangularMap.move(). - zmodyfikuj metodę
toStringtak, by zwracała jedynie schematyczną orientację zwierzęcia w postaci łańcucha składającego się z jednego znaku, Np. jeśli zwierzę ma orientację północną, to metodatoString()powinna zwracać łańcuch "N" albo "^".
- metoda
-
Zmodyfikuj klasę
Simulationtak, by przyjmowała w konstruktorze również obiektWorldMap. Następnie popraw realizację metodyrun()tak, by ruch odbywał się za pośrednictwem mapy. Po każdym ruchu wypisz aktualny stan mapy. -
Dodaj testy integracyjne weryfikujące, że implementacja jest poprawna.
Interfejs WorldMap zakłada, że mapa może przechowywać jedynie zwierzęta, a pozycje zawsze wyrażone są jako dwuwymiarowe wektory. Te założenia można poluzować, wprowadzając parametryzację i typy generyczne.
Uwaga: to zadanie najlepiej robić na osobnym branchu i nie scalać go z main - może być trudne w utrzymaniu przy kolejnych laborkach. Najlepiej zacząć realizację zadania od stworzenia brancha lab4-bonus z brancha lab4 (z miejsca, gdzie podstawowa część laborki jest już gotowa). W celu oddania zadania bonusowego wystarczy wtedy utworzyć pull request z lab4-bonus do lab4
- Zmodyfikuj interfejs
WorldMaptak, by mapa mogła przechowywać dowolne obiektyTna pozycjach typuP. Deklaracja typu powinna wyglądać tak:WorldMap<T, P>. Dostosuj do tego założenia wszystkie metody w interfejsie. - Popraw klasę
RectangularMaptak, by implementowała interfejs z odpowiednimi parametrami typów. - Popraw
Simulationtak, by przyjmowało od teraz dowolne mapy obiektów z dowolnymi pozycjami. Uwaga: być może konieczna będzie w tym celu modyfikacja konstruktoraSimulation. Zastanów się, jakie parametry powinien przyjmować ten konstruktor i gdzie powinna znajdować się inicjalizacja zwierzaków po zmianach. - Stwórz dodatkową implementację
TextMap, która będzie przechowywała napisyStringna pozycjach określonych w jednym wymiarze (liczba całkowita). Deklaracja typu dla takiej mapy toWorldMap<String, Integer>. Mapa powinna spełniać założenia:- Mapa nie ma górnej granicy - dokładanie nowego napisu zawsze wstawia go na koniec mapy.
- Przemieszczanie napisu jest możliwe jedynie w obecnych granicach
<0, N-1>(gdzieN- liczba elementów w mapie). Przesuwany napis zamienia się miejscami z sąsiadem - w przypadku ruchu "na wschód" z sąsiadem z prawej (o indeksie o 1 wyższym), a "na zachód" z lewej. Np. dla mapy["Ala", "ma", "sowoniedźwiedzia"]przesunięcie napisu"ma"na wschód powinno dać efekt:["Ala", "sowoniedźwiedzia", "ma"]. Dalsze przemieszczanie wyrazu"ma"w prawo nie jest już możliwe. - Napis może się przemieszczać do przodu i tyłu. Można założyć, że przemieszczenie do przodu następuje po podaniu parametru
FORWARDlubRIGHT, a do tyłu po podaniuBACKWARDlubLEFT.
- Przetestuj nową implementację mapy tworząc dodatkową symulację
Simulation<String, Integer>w metodziemain(). Możesz wykorzystać te same parametry ruchu, co w przypadku symulacji zwierząt oraz dowolnie przyjęty zestaw napisów. - Dodaj do systemu dodatkowy interfejs
WorldNumberPositionMap. Interfejs powinien rozszerzaćWorldMapi tak definiować typy generyczne by obsługiwać obiekty dowolnych typówT, ale na pozycjach wyrażonych dowolnymi typami liczbowymi (Integer,Double,Long, itp). Popraw definicjęTextMaptak by realizowała interfejsWorldNumberPositionMap. Wskazówka: wszystkie liczbowe typy kopertowe rozszerzają klasęNumber.
-
Mechanizm interfejsów pozwala na określenie pewnego zestawu metod, które muszą być implementowane przez określony typ. Interfejs
WorldMapjest tego przykładem - określa on sposób interakcji mapy ze zwierzętami oraz klasąMapVisualizer. Zarówno interfejsy, jak i klasa są do pobrania z folderu z konspektem - wykorzystasz je w swoim projekcie. -
Interfejs określa jedynie, że dana klasa ma posiadać określoną metodę - dlatego w interfejsie nie ma implementacji - wszystkie metody są z założenia abstrakcyjne (można pominąć modyfikator
abstract). -
W interfejsie wszystkie metody są z założenia publiczne, dlatego nie ma potrzeby dodania modyfikatora dostępu
public. Od Javy 9 interfejs może posiadać także metody prywatne. Od Javy 8 interfejsy mogą posiadać metody statyczne (takie same jak metody statyczne w klasach) oraz metody domyślne (oznaczane modyfikatoremdefault), które posiadają implementację. -
Podstawianie obiektów realizujących interfejs pod deklaracje metod i zmiennych wymagających interfejsu jest możliwe, bo w Javie działa polimorfizm. Przykładowo:
MoveValidator validator = new RectangularMap(10, 10); // RectangularMap pośrednio realizuje MoveValidator validator.canMoveTo(new Vector2d(1, 2)); // wywołanie poprawne, Java wywoła implementację metody z RectangularMap // validator.place(animal); wywołanie niepoprawne, kod się NIE skompiluje! MoveValidator nie ma metody place()
-
Klasa deklaruje fakt implementacji interfejsu za pomocą słowa kluczowego
implements, np.class RectangularMap implements WorldMap { }
-
Interfejs
Mapdefiniuje w Javie strukturę słownikową, czyli mapę odwzorowującą klucze na wartości. -
Jedną z najczęściej wykorzystywanych implementacji interfejsu
Mapjest klasaHashMap, przykładowo:Map<Vector2d, Animal> animals = new HashMap<>();
-
Poprawne działanie
HashMapuzależnione jest od implementacji metodequalsorazhashCodew klasie, która stanowi klucze mapy (w ćwiczeniu dotyczy to klasyVector2d). -
Wynik działania metody
hashCodemusi być zgodny z wynikiem działania metodyequals, tzn. jeśli dwa obiekty są równe wedługequals, to ichhashCodemusi być równy. -
Przykładowa implementacja metody
hashCodedla klasyVector2dmoże wyglądać następująco:@Override public int hashCode() { return Objects.hash(this.x, this.y); }
-
Używanie mapy nie wymaga jawnego wywoływania metody
hashCode, ale jest ona używana wewnętrznie dla potrzeb optymalizacji. Istotą funkcji jest fakt, że dla identycznych wartościxiywartość funkcjihashCodebędzie identyczna.