23 czerwca 2009

Dokończenie rozdziału 7. "Working with Collections" z "Programming Groovy"

Kontynuacja relacji z lektury "Programming Groovy: Dynamic Productivity for the Java Developer" Venkata Subramaniama. Tym razem dokończenie rozdziału 7. "Working with Collections" o kolekcjach.

Tworzenie mapy w Groovy przypomina tworzenie listy (patrz Z rozdziału 7. o kolekcjach (listach) z "Programming Groovy") z tym, że elementami są pary - klucz i jego wartość, z dwukropkiem (:) jako separatorem.
 $ groovysh
Groovy Shell (1.6.3, JVM: 1.6.0_14)
Type 'help' or '\h' for help.
-------------------------------------------------------------
groovy:000> mapa = ['klucz1':'wartosc1', 'klucz2':'wartosc2']
===> {klucz1=wartosc1, klucz2=wartosc2}
Dostęp do wartości klucza jest możliwy na dwa sposoby - przez konstrukcję [klucz] lub bezpośrednio przez kropkę z określeniem nazwy klucza.
 groovy:000> mapa['klucz1']
===> wartosc1
groovy:000> mapa.'klucz1'
===> wartosc1
groovy:000> mapa.klucz1
===> wartosc1
Konstrukcja odczytu tablicowego (z []) nie pozwala na określenie nazwy klucza bez pojedynczych cudzysłowów.
 groovy:000> mapa[klucz1]
ERROR groovy.lang.MissingPropertyException: No such property: klucz1 for class: groovysh_evaluate
at groovysh_evaluate.run (groovysh_evaluate:2)
...
W takim przypadku klucz1 traktowane jest jako zmienna.

Jeśli nazwa klucza zawiera zastrzeżone znaki, np. symbole matematyczne, czy spacje, konieczne jest "opakowanie" klucza pojedynczymi cudzysłowami.
 groovy:000> mapa.'klucz ze spacja' = 'pewna wartosc'
===> pewna wartosc
groovy:000> mapa
===> {klucz1=wartosc1, klucz2=wartosc2, klucz ze spacja=pewna wartosc}
groovy:000> mapa.'klucz ze spacja'
===> pewna wartosc
Sprawne oko mogło zauważyć mechanizm dodawania elementów do mapy przez mechanizm "z kropką". Podobnie jak w odczycie, pozbywamy się specjalności niektórych znaków (spacje, symbole matematyczne) przez umieszczenie klucza w pojedynczych cudzysłowach.

Podstawowym wyróżnikiem mapy jako typu w Groovy jest specjalne traktowanie konstrukcji mapa.class. We wszystkich typach w Groovy poza mapą na wyjściu mamy obiekt klasy/typu. W przypadku mapy jest to najzwyklejsza nazwa klucza, więc jeśli go nie ma zwracany jest null.
 groovy:000> mapa.class
===> null
groovy:000> mapa.class = 'Pewna wartosc dla klucza class'
===> Pewna wartosc dla klucza class
groovy:000> mapa.class
===> Pewna wartosc dla klucza class
Wyobraźmy sobie przykładową mapę, w której umieszczamy pary nazwa języka i jego autor. W książce pojawia się mapa z C++, Java i Lisp (pewnie dla dwóch pierwszych znamy autorów, ale dla Lisp już nie, co?).
 groovy:000> jezyki = ['C++':'Stroustrup', 'Java':'Gosling', 'Lisp':'McCarthy']
===> {C++=Stroustrup, Java=Gosling, Lisp=McCarthy}
groovy:000> jezyki.C++
ERROR java.lang.NullPointerException: Cannot invoke method next() on null object
at groovysh_evaluate.run (groovysh_evaluate:2)
...
Próba odczytania wartości dla C++ kończy się NPE (NullPointerException). Już wiemy dlaczego (znaki specjalnego traktowania w Groovy - ++ odpowiadające metodzie next()), ale autor wyjaśnia to w dosyć "interesujący" sposób: "You may discard this example code by saying C++ is always a problem, no matter where you go." (str. 125).

Metoda each() na mapie akceptuje domknięcie, w którym parametrem wejściowym może być pojedynczy element-para mapy (typ MapEntry), lub dwa parametry, które odpowiadają aktualnie przetwarzanemu kluczowi i jego wartości.
 groovy:000> jezyki.each { paraJezykAutor -> println "Jezyk: $paraJezykAutor.key, autor: $paraJezykAutor.value" }
Jezyk: C++, autor: Stroustrup
Jezyk: Java, autor: Gosling
Jezyk: Lisp, autor: McCarthy
===> {C++=Stroustrup, Java=Gosling, Lisp=McCarthy}
groovy:000> jezyki.each { jezyk, autor -> println "Jezyk: $jezyk, autor: $autor" }
Jezyk: C++, autor: Stroustrup
Jezyk: Java, autor: Gosling
Jezyk: Lisp, autor: McCarthy
===> {C++=Stroustrup, Java=Gosling, Lisp=McCarthy}
Ogólnie, wszystkie metody na mapie akceptują pojedynczy parametr wejściowy, dla których przekazywana jest para, a dla dwóch klucz i jego wartość. Tak będzie dla collect(), find(), findAll().

Nowością, w kontekście dostępnych metod w mapie w stosunku do listy, jest metoda any(), która zwraca wartość logiczną true/false, jeśli co najmniej jedna para spełnia warunek zdefiniowany w domknięciu.
 groovy:000> jezyki.any { jezyk, autor -> jezyk =~ "[^A-Za-z]" }
===> true
Z kolei metoda every() sprawdza, czy wszystkie pary spełniają warunek z domknięcia.
 groovy:000> jezyki.every { jezyk, autor -> jezyk =~ "[^A-Za-z]" }
===> false
Interesującą metodą może wydać się metoda groupBy(), która zwraca mapę z parami, gdzie dla zdefiniowanego w domknięciu klucza przypisane są wartości pierwotnej mapy.
 groovy:000> jezyki.groupBy { it }
===> {C++=Stroustrup={C++=Stroustrup}, Java=Gosling={Java=Gosling}, Lisp=McCarthy={Lisp=McCarthy}}
groovy:000> jezyki.groupBy { jezyk, autor -> jezyk.charAt(0) }
===> {C={C++=Stroustrup}, J={Java=Gosling}, L={Lisp=McCarthy}}
groovy:000> jezyki.groupBy { jezyk, autor -> autor.contains('o') }
===> {true={C++=Stroustrup, Java=Gosling}, false={Lisp=McCarthy}}
Gdyby ktoś wpadł na ciekawy pomysł prezentujący cechy groupBy() chętnie opublikuję go w kolejnej relacji.

Na samo zakończenie rozdziału autor trafnie podsumowuje możliwości kolekcji w Groovy: "working with collections is easier and faster, your code is shorter, and it's fun" (str. 130). Szczególnie warto podkreślić owe "it's fun". Trudno się z tym nie zgodzić.