1 Programowanie w Javie - wykład 13 Kolekcje c.d. ( mapy) Treści prezentowane w wykładzie zostały oparte o: Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014 http://docs.oracle.com/javase/8/docs/ http://docs.oracle.com/javase/9/docs/ C. S. Horstmann, G. Cornell, Java. Podstawy, Helion, Gliwice 2013 C. S. Horstmann, Java, Techniki zaawansowane(wydanie10), Helion, Gliwice 2017
Mapy (interfejs Map) 7 Tablica asocjacyjna jest zestawem elementów, do których zapewniono swobodny, bezpośredni (czyli bez konieczności przeglądania elementów zestawu) dostęp za pomocą kluczy. Można sobie wyobrażać, że jest to uogólnienie zwykłej tablicy: w zwykłej tablicy kluczami są indeksy całkowite, a w tablicy asocjacyjnej kluczami mogą być dowolne obiekty. Efektywne realizacje tablic asocjacyjnych opierają się na odpowiedniej implementacji słowników (odwzorowujących klucze w odpowiadające im wartości). W Javie słownikowe implementacje tablic asocjacyjnych określane są słowem map, pochodzącym od terminu mapping, oznaczającego jednoznaczne odwzorowanie (w tym przypadku zbioru kluczy w zbiór wartości). Również po polsku krótko będziemy nazywać tablice asocjacyjne mapami.
Mapy Mapa jest jednoznacznym odwzorowaniem zbioru kluczy w zbiór wartości. O mapach możemy myśleć jako o takich kolekcjach par: klucz - wartość, które zapewniają odnajdywanie wartości związanej z podanym kluczem. Zarówno klucze, jak i wartości mogą być referencjami do dowolnych obiektów (jak również wartościami null). Oczywiście, wartości kluczy nie mogą się powtarzać (odwzorowanie musi być jednoznaczne). Natomiast pod różnymi kluczami można zapisać te same wartości (odwzorowanie nie musi być wzajemnie jednoznaczne). Przykłady: Zestaw danych zawierający: Klucz: NIP ->Wartość : Dane o podatniku Klucz: Kraj ->Wartość : Stolica Klucz: Nazwa kontaktu ->Wartość : Numer telefonu i pozwalający na odnajdywanie poszukiwanej wartości po kluczu. Istotą zastosowania map jest możliwość łatwego i jednocześnie szybkiego odnajdywania informacji w powiązanych zestawach danych. 8
Mapy - interfejs Map<K,V> 9 W JCF ze względu na specyfikę działania na mapach (jako zestawach par klucze-wartości) implementują one interfejs Map, a nie Collection. Podstawowe operacje na mapie (niezależnie od jej implementacji) są określone przez metody interfejsu Map<K,V>, gdzie K typ kluczy, V typ wartości. Należą do nich: void clear() - usuwa wszystkie elementy mapy (operacja opcjonalna). boolean containskey(object key) - zwraca true jeżeli mapa zawiera podany klucz (i związaną z nim wartość). boolean containsvalue(object value) - zwraca true jeśli mapa zawiera podaną wartość (do ktorej może prowadzić wiele kluczy). V get(object key) - zwraca wartość dla danego klucza. V put(k key, V value) - dodaje parę klucz-wartość do mapy (lub zastępuje poprzednie odwzorowanie tego klucza podaną wartością), zwraca obiekt, który uprzednio znajdował się pod danym kluczem lub null. void putall(map<...> t)- dodaje do mapy wszystkie elementy mapy t. V remove(object key) - usuwa odwzorowanie: klucz-wartość z tej mapy (operacja opcjonalna). Zwraca usuniętą wartość. int size() - zwraca liczbę par klucz-wartość w mapie Uwaga. Metody put..., ze względu na jednoznaczność, powodują zastąpienie istniejących w mapie odwzorowań, które mają takie same klucze jak dodawane odwzorowania.
Mapy - interfejs Map<K,V> Metoda containskey(..) wydaje się duplikować jakby działanie metody get(..). Ale pamietajmy, że mapy mogą zawierać pod danym kluczem wartość null. Wtedy metoda get zwraca null i nie wiemy czy oznacza to brak odwzorowania w mapie, czy też odwzorowanie klucza w wartość null. Metoda containskey pozwala to rozstrzygnąć: zwróci true jeśli odwzorowanie jest (nawet jeśli wartością "pod" kluczem jest null), natomiast zwrócona wartość false świadczy na pewno o tym, że w mapie nie ma poszukiwanego odwzorowania. Ponadto w interfejsie Map istnieją metody : Set<K> keyset() - zwraca widok na zbiór kluczy. Collection<V> values() - zwraca widok na kolekcję wartości. Set<Map.Entry<K,V>> entryset() - zwraca widok na zbiór par kluczewartości jako zbiór obiektów typu Map.Entry<K,V>. K jest typem klucza, V - typem wartości. Ze względu na to, że mapy nie implementują interfejsu Collection, nie można od nich uzyskać bezpośrednio iteratorów. Można natomiast uzyskać kolekcje, które są widokami na zestawy kluczy (Set, bo bez powtórzeń), zestawy wartości (Collection, bo bez porządku i z możliwością powtórzeń elementów), oraz par klucze-wartośći (Set, bo bez powtórzeń). Od tych kolekcji - naturalnie - możemy uzyskać iteratory. 10
Mapy konkretne implementacje W mapach istotne jest szybkie odnajdywanie kluczy. Klucze (poprzez umieszczenie ich razem z wartościami w odpowiednich strukturach danych) związane są już bezpośrednio i niejako natychmiastowo z wartościami. Podobnie zatem jak w przypadku zbiorów istnieją dwie podstawowe implementacje, pozwalające na szybkie wyszukiwanie kluczy - implementacja oparta na tablicy mieszającej (HashMap) oraz na drzewie czerwono-czarnym (TreeMap). Niejako ubocznym skutkiem zastosowania tej ostatniej jest możliwość przeglądania kluczy mapy w naturalnym porządku rosnącym lub w porządku, określanym przez komparator podany przy konstrukcji mapy. Reguły stosowania komparatorów są takie same jak w przypadku zbiorów uporządkowanych. Mówimy, że mamy do czynienia z mapą uporządkowaną, a zatem mamy - tak jak w przypadku zbiorów uporządkowanych - dodatkowe właściwości takie jak "pierwszy" i "ostatni" klucz lub ich podzbiór "od" - "do" (określane przez metody interfejsu SortedMap) oraz operacje znajdowania bliskich kluczy (z interfejsu NavigableMap) Implementacja LinkedHashMap pozwala na odtworzenie kolejności dodawania elementów do mapy. 12
Mapy HashMap przykład 1 Przykład. Wyobraźmy sobie, że w pliku znajdują się nazwy i adresy firm. Nasz program po wczytaniu pliku ma za zadanie dostarczenie prostego interfejsu wyszukiwania adresu dla podanej nazwy firmy. Zauważmy, że proste sposoby rozwiązania tego problemu (np. prowadzenie dwóch list - nazw i adresów - i liniowe wyszukiwanie nazwy na liście nazw po to by otrzymać pozycję adresu na liście adresów) są bardzo nieefektywne. Klasa HashMap, która reprezentuje zestaw par klucz-wartość, efektywnie pozwala rozwiązać ten problem. Zatem jeśli postać pliku wejściowego jest następująca: nazwa_firmy1 adres nazwa_firmy2 adres... nazwa_firmyn adres to problem wyszukiwania adresów dla firm podawanych w dialogach wejściowych można oprogramować w następujący sposób: 13
import java.util.*; import java.io.*; import javax.swing.*; Mapy HashMap przykład 1 class TestMap1 { public static void main(string args[]) throws IOException { // mapa odwzorowań : nazwa -> adres // argumenty typu są dwa: dla klucza(nazwy) i wartości(adresu) Map<String, String> map = new HashMap<>(); // Wczytywanie danych Scanner scan = new Scanner(new File("firmsAddr.txt")); String firmname; String address; while (scan.hasnextline()) { firmname = scan.nextline(); address = scan.hasnextline()? scan.nextline() : ""; } //nazwa firmy to klucz pod którym w mapie będzie jej adres map.put(firmname, address); //dodanie pary klucz-wartość do mapy 14
Mapy HashMap przykład 1 } } // Interakcyjna część programu: dla podanej w dialogu // nazwy firmy pokazywany jest jej adres while ((firmname = JOptionPane.showInputDialog ("Nazwa firmy"))!= null) { address = map.get(firmname); if (address == null) address = "?"; JOptionPane.showMessageDialog(null, "Firma: " + firmname + '\n' + "Adres: " + address); } w Javie 8 dla map dostępna jest metoda: getordefault(key, value), która zwraca wartość pod kluczem key, a jeśli jej nie ma (w mapie brak klucza i wartości z nim związanej), to zwraca domyślną wartość value. Używając tej metody powyższy fragment programu mógłby wyglądać następująco: while ((firmname = JOptionPane.showInputDialog ("Nazwa firmy"))!= null) { address = map.getordefault(firmname, "?"); JOptionPane.showMessageDialog(null, "Firma: " + firmname + '\n' + "Adres: " + address); } 15
Mapy keyset() i values() przykład 1 16 Metody keyset() i values() zwracają odpowiednio zbiór kluczy i kolekcję wartości mapy. Iterując po kluczach możemy sięgać po kolejne pary klucz-> wartość. Dla mapy map firma-> adres: System.out.println(map); Set<String> keys = map.keyset(); System.out.println("Klucze: " + keys); Collection<String> vals = map.values(); System.out.println("Wartości: " + vals); Set<Map.Entry<String,String>> entries = map.entryset(); System.out.println("Pary: " + entries); for (String name : map.keyset()){ String addr = map.get(name); System.out.println(name + " - adres: " + addr); } Uzyskana poprzez keyset() kolekcja kluczy jest widokiem (a nie jakimś nowym obiektem-zbiorem) na klucze mapy, co oznacza, że operując na elementach tego zbioru faktycznie operujemy na kluczach mapy. Podobne widoki są zwracane poprzez values() i entryset(). Widok na klucze jest typu wyznaczanego przez klasę implementującą interfejs Set, ale jest to inna klasa niż znane nam HashSet i TreeSet
Mapy TreeMap przykład 1 Parę klucz-> wartość w mapie można usunąć za pomocą metody remove(key) albo za pomocą metody remove() iteratora zbioru kluczy. System.out.println(map); map.remove("microsoft"); System.out.println(map); map.put("microsoft", "Redmont"); System.out.println(map); for (Iterator<String> it = map.keyset().iterator(); it.hasnext(); ) { if(it.next().equals("microsoft")) it.remove(); } Jeśli chcemy mieć posortowane klucze tworzymy TreeMap //mapa w porządku naturalnym (klucze alfabetycznie) Map<String, String> mapsort = new TreeMap<>(map); // Pusta mapa z komparatorem odwracającym // naturalny porządek Map<String, String> mapro = new TreeMap<>(Collections.reverseOrder()); // Wpisujemy do niej pary z mapy map mapro.putall(map); System.out.println(mapRo); 17
Mapy entryset - przykład 2 Efektywniejszym od keyset()(gdyż keyset wymaga każdorazowego pobierania wartości spod klucza) sposobem przeglądania map jest użycie metody entryset. Uzyskana kolekcja par klucze-wartości (widok na klucze-wartości) jest typu wyznaczanego przez klasę implementującą interfejs Map.Entry (wewnętrzny interfejs interfejsu Map). Dzięki temu - ale tylko w trakcie iteracji po elementach tej kolekcji - mamy dodatkowe możliwości działania, a mianowicie: uzyskanie klucza (metoda getkey()), wartości dla danej pary (metoda getvalue()) oraz zmianę wartości dla danej pary (metoda setvalue(object). Przykład: Map<String, Integer> map = new HashMap<>(); for(char c = 'a'; c <= 'd'; c++) map.put("" + c, (int) c); Set<Map.Entry<String, Integer>> entries = map.entryset(); System.out.println(entries);//[a=97, b=98, c=99, d=100] //Efektywny sposób przeglądania map for(map.entry<string, Integer> e : entries) { Integer val = e.getvalue(); System.out.print(e.getKey() + " " + val + "' " ); //a 97, b 98, c 99, d 100, e.setvalue(val + 1000); } System.out.println(entries);//[a=1097, b=1098, c=1099, d=1100] 18
Mapy przykład 3 Rozważmy napisanie programu diagnostycznego (np. jakichś napraw), który na podstawie wprowadzonego przez użytkownika symptomu podaje niezbędną akcję do wykonania. Możemy to zrobić np. przy pomocy instrukcji switch z ilością case odpowiadającą ilości symptomów (np. 100). Takie rozwiązanie jest niefektywne, wszelkie zmiany powodują konieczność modyfikacji kodu i ponownej kompilacji. Lepiej będzie oddzielić dane od kodu źródłowego (zapisać je w pliku np. w kolejnych wierszach symptom-akcja) i zastosować mapę. HashMap<String, String> diagmap = new HashMap<>(); for (Scanner sc = new Scanner(new File("diagnose.txt")); sc.hasnextline(); ) { diagmap.put(sc.nextline(), sc.nextline()); } String symptom = showinputdialog("podaj symptom"); String act = diagmap.containskey(symptom)? diagmap.get(symptom) : "Nieznany symptom"; showmessagedialog(null, act); Zamiast wielu linijek kodu mamy kilka, a program jest niezależny od zestawu symptomów i odpowiednich akcji. Istotą zastosowania map jest możliwość szybkiego odnajdywania informacji w powiązanych zestawach danych, a także tworzenia zwięzłego i elastycznego kodu. 19
Klasa Collections i Arrays JCF zawiera dwie użyteczne klasy, wspomagające działania na kolekcjach (a także tablicach) - klasę Collections i klasę Arrays. Klasa Collections dostarcza statycznych metod, operujących na kolekcjach lub zwracających kolekcje. Do metod tych należą: algorytmy kolekcyjne, metody zwracające widoki kolekcji, metody tworzące specjalne kolekcje. Wybrane algorytmy kolekcyjne: static int binarysearch(list<... > list, K key)- wyszukiwanie binarne na liście. Zwraca indeks elementu, który jest równy podanemu obiektowi key (wedle metody compareto z klas(y) elementów listy, przy czym zarówno elementy listy, jak i obiekt key muszą być porównywalne). Lista musi być najpierw posortowana według naturalnego porządku np. metodą Collections.sort(List l). Przy braku elementu zwraca wartość < 0 (a nie -1!) static int binarysearch(list<...> list, K key, Comparator<...> c) - wyszukiwanie binarne według podanego komparatora (porównywanie obiektu key z elementami listy następuje za pomocą metody compare komparatora). Lista musi być posortowana zgodnie z porządkiem definiowanym przez komparator np. metodą Collections.sort(List l, Comparator c). Zwraca indeks odnalezionego elementu lub wartość < 0, gdy brak jest elementu. 20
Klasa Collections i Arrays 21 static void sort(list<t> list)- sortowanie listy według naturalengo porządku elementów (rosnąco) static void sort(list<t> list, Comparator<...> c) - sortowanie listy wedle porządku określanego przez komparator. W klasie Arrays dostarczono statycznych metod, m.in. implementujących algorytmy sortowania i binarnego wyszukiwania dla tablic zarówno typów pierwotnych, jak i obiektów (w tym ostatnim przypadku również z możliwością użycia komparatorów). Metody klasy Collections zwracające widoki kolekcji nakładają na kolekcje "opakowania" w celu zmiany ich niektórych właściwości. W szczególności możemy uzyskać niemodyfikowalne oraz synchronizowane widoki dowolnej kolekcji. Przypomnijmy, że niemodyfikowalność kolekcji oznacza niemożliwość wszelkich modyfikacji (zarówno strukturalnych - dodawanie usuwanie elementów, jak i zmian wartości elementów). Czasami jest to potrzebne, gdy chcemy zagwarantować, by po utworzeniu kolekcja w ogóle nie mogła podlegać zmianom, lub gdy chcemy zróżnicować prawa dostępu do kolekcji dla różnych grup użytkowników naszej aplikacji (ktoś może zmieniać kolekcje, kto inny - tylko przeglądać). Gotowe implementacje kolekcji w JCF są modyfikowalne.
Klasa Collections i Arrays 22 Niemodyfikowalne widoki kolekcji możemy uzyskać za pomocą metod (dla ułatwienia pominięto parametry typu): public static Collection unmodifiablecollection(collection c); public static Set unmodifiableset(set s); public static List unmodifiablelist(list list); public static Map unmodifiablemap(map m); public static SortedSet unmodifiablesortedset(sortedset s); public static SortedMap unmodifiablesortedmap(sortedmap m); Szczególnym rodzajem niemodyfikowalności jest brak możliwości zmian rozmiaru kolekcji. Takich kolekcji (których elementy możemy zmieniać, ale nie możemy do nich dodawać nowych elementów ani usuwać istniejących) dostarcza statyczna metoda aslist z klasy Arrays, zwracająca listę, elementami której są elementy tablicy przekazanej jako argument. Uwaga: lista ta jest widokiem na tablicę! Metoda ta może służyć nie tylko do łatwego tworzenia list z istniejących tablic, ale również do tworzenia nowych, pustych list o niezmiennych rozmiarach: // Tworzy listę o ustalonym (i niezmiennym) // rozmiarze 1000 elementów List l = Arrays.asList(new Object[1000]);
Przetwarzanie list i zbiorów -uwagi W klasie Collections dostępne są metody statyczne, ułatwiające wykonywanie różnych operacji na kolekcjach. Oprócz wspomnianych wcześniej metod dostępne są dodatkowo metody odwracania list Collections.reverse(lista), wypełniania kolekcji i wiele innych. W trakcie przeglądania kolekcji za pomocą iteratora nie wolno jej modyfikować innymi środkami niż metody stosowanego właśnie do iterowania. Zatem // coll dowolna kolekcja napisów Iterator<String> it1=coll.iterator(); Iterator<String> it2=coll.iterator(); while(it1.hasnext()){ it1.next(); it2.next(); it2.remove(); // BŁĄD FAZY WYKONANIA wyjątek // ConcurrentModificationException } Dotyczy to również pętli for-each dla kolekcji (stosowany jest w niej "pod spodem" iterator) for (String e : coll) coll.add(e+"1");//spowoduje ConcurrentModificationException 23
Java 9 metoda of dla List, Set i Map 24 W Java 9 została dostarczona nowa statyczna metoda fabrykująca ( factory methods ) of tworząca instancje kolekcji (list i zbiorów) oraz map o małej liczbie elementów. // for empty list static <E> List<E> of() // for list containing one element static <E> List<E> of(e e1) // for list containing two element static <E> List<E> of(e e1, E e2) // // for list containing an arbitrary number of elements static <E> List<E> of(e... elements); Przykład: List<String> immutablelist = List.of(); immutablelist = List.of("one", "two", "three"); //jeśli dodamy null do listy to będzie wyrzucony wyjątek //NullPointerException //List<String> immutablelist = List.of("one", "two", null);
Java 9 metoda of dla List, Set i Map 25 Lista stworzona przy pomocy metody of jest niezmienialna (immutable), dlatego jeśli spróbujemy dodać element do listy zostanie wyrzucony wyjątek java.lang.unsupportedoperationexception List<String> immutablelist = List.of("one", "two", "three"); immutablelist.add("four"); //wyjatek Rozwiązanie problemu: List<String> mutablelist = new ArrayList<>(List.of("one", "two", "three")); mutablelist.add("four"); mutablelist.add(null); // Result: [one, two, three, four, null] Set // for empty Set static <E> Set<E> of() // for Set containing one element static <E> Set<E> of(e e1) // for Set containing two element static <E> Set<E> of(e e1, E e2) // // for Set containing an arbitrary number of elements static <E> Set<E> of(e... elements)
Java 9 metoda of dla List, Set i Map 26 Map Map.of() Aby stworzyć mapę korzystamy z metod statycznych: // for empty Map static <K, V> Map<K, V> of() // for Map containing a single mapping static <K, V> Map<K, V> of(k k1, V v1) // for Map containing two mappings static <K, V> Map<K, V> of(k k1, V v1, K k2, V v2) // // for Map containing up to ten mappings static <K, V> Map<K, V> of(k k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) Przykład: Map<Integer, String> immutablemap = Map.of(); immutablemap = Map.of(1, "one", 2, "two", 3, "three");
Java 9 metoda of dla List, Set i Map 27 Map.ofEntries() Jeśli chcemy stworzyć mapę z więcej niż 10 mapowaniami korzystamy z metody Map.ofEntries(). static <K, V> Map<K, V> ofentries( Entry<? extends K,? extends V>... entries) Aby z niej skorzystać pary klucz- wartość (entry) można stworzyć metodą static <K, V> Entry<K, V> entry(k k, V v) Np.: Map<Integer, String> newimmutablemap = Map.ofEntries(Map.entry(1, "one"), Map.entry(2, "two"), Map.entry(3, "three")); System.out.println(newImmutableMap);//{1=one, 3=three, 2=two} Map<Integer, String> newimmutablemap1 = Map.ofEntries(); System.out.println(newImmutableMap1);//{}