Google collections - ušetřete si práci s kolekcemi

Nedávno mě při poslechu JavaPosse zaujala zmínka o Google Collections. Jedná se o knihovnu doplňující funkcionalitu třídy Collections ze standardní Javy. Knihovna obsahuje řadu utility tříd, které zpříjemňují život s generikami v kolekcích, vytváření kolekcí v kolekcích a další manipulaci dat v kolekcích. Jelikož mě knihovna zaujala hned na první pohled, rozhodl jsem se podívat se jí na zoubek a podělit se s vámi o své zkušenosti.

Zkrácení zápisu pro vytvoření nových kolekcí s generikami

Jedná se možná o drobnost, ale natolik častou, že i drobné vylepšení přinese celkové zpřehlednění zápisu a zrychlení práce. V klasickém Java 1.5 kódu při vytváření generické kolekce typicky píšete např.:


Map bufferIndex
          = new HashMap();

Na pravé straně deklarace tedy opakujete generickou informaci - a zbytečně - tato informace se dá přeci odvodit z levé strany přiřazení. Java bohužel tuto informaci vyžadujete, protože pokud byste zadali jednoduše new HashMap(), vytvoříte obyčejnou mapu bez informace o generikách a Java vás bude při kompilaci otravovat hlášením: Unchecked assignment.

Google collection obsahují řadu statických metod pro vytváření standardních kolekcí, které vás zbaví nutnosti opakovat informaci o generikách při vytváření těchto kolekcí. Stejnou mapu jako v předchozím příkladě vytvoříme napsáním:


Map bufferIndex = Maps.newHashMap();

Podobných metod je ve třídě Maps řada (namátkou třeba newLinkedHashMap, newConcurrentHashMap, newTreeMap). Podpora existuje nejen pro mapy, ale i pro další typy kolekcí:

Konverze polí objektů a primitivních typů

Všichni známe třídu Arrays ze standardní Javy. Ta sice obsahuje velmi užitečné funkce, ale zakrátko člověk zjistí, že si stejně řadu věcí neustále dopisuje sám. Jsou to drobnosti na tři řádky, ale stokrát nic umořilo osla. Google collections nabízí právě tyto drobnosti, které šetří práci.

Práce s poli objektů

Jednoduše je možné slučovat pole objektů. Dříve bylo nutné manipulovat s System.arraycopy - díky knihovně je možné jednoduše zapsat:


Integer[] vysledek = ObjectArrays.concat(
     new Integer[20],
     new Integer[20],
     Integer.class
);

Konverze polí na list a zpět

Pracovat s poli v Javě má řadu výhod. Kód pro práci s nimi je velmi úsporný a přehledný. Pokud mám seznam objektů, který má být od počátku inicializován, rád používám čitelný zápis inicializace seznamu jako pole:


String[] data = new String[] {"jedna", "dvě", "tři"};

Pokud se ovšem stane, že do pole chci přidat další záznam, musím data vložit do listu, jehož inicializace je již na několik řádků a přehlednost se ztrácí. V google collections jsou metody, které tuto práci šetří a zvyšují tak subjektivní použitelnost polí jako takových:


List listJakoPole =
     PrimitiveArrays.asList(new int[] {1,2,3});
int[] poleZListu =
     PrimitiveArrays.toIntArray(listJakoPole);

Pokud by pro mě byla čitelnost zápisu pro vytvoření seznamu jediným měřítkem, vyplatí se mi použít jinou metodu z nabídky google collections:


List poleCisel =
     Lists.newLinkedList(1, 2, 3);

Multimapy

Snad každý z nás potřebuje a často vytváří mapy, které v hodnotách obsahují nikoliv jediný objekt, ale seznam objektů reprezentovaný např. ArrayListem. Já sám to programoval už asi stokrát - není to moc kódu, takže mě nikdy nenapadlo to přesunout do knihovny, ale zase se to používá tak často, že se to ve výsledku opravdu vyplatí. V daném případě musí totiž člověk ošetřit situaci vložení prvního objektu pod daným klíčem - list totiž neexistuje a proto je třeba vytvořit nejdříve list, do něj vložit prvek a teprve tento list vložit pod klíčem do mapy. V google collections lze jednoduše třebas takto:


ArrayListMultimap listMultimap =
     Multimaps.newArrayListMultimap();
listMultimap.put(1, "ahoj");
listMultimap.put(1, "ciao");
List greetings = listMultimap.get(1);

Výsledkem bude očekávaný list stringů obsahující ahoj a ciao. Google collections obsahují řadu implementací pro tyto Multimapy:

Podpora pro Decorator pattern

Google poskytuje základní implementace wrapperů nad standardními kolekcemi a iterátory. Zjednodušují tak práci pro implementaci decorator patternu. Stačí vám jen jednoduše podědit z těchto základních Forwarding class a přetížit konkrétní metody, jejichž chování si přejete změnit. Pro přístup k původnímu (backing) objektu slouží metoda delegate().

Podmínky pro záznamy v kolekcích

Tato podkapitolka a ta následující mi hodně připomíná použití Closures. Umožňuje jednoduše kontrolovat vkládání nových záznamů do kolekcí. Víte, že list vám umožňuje bez problémů vytvořít záznam s null hodnotou. To může vést k chybám, které nejsou na první pohled úplně patrné. S google collection se dá tomuto případu jednoduše zabránit:


List list = Constraints.constrainedList(
		new ArrayList(), Constraints.NOT_NULL
	);
Map map = MapConstraints.constrainedMap(
		new HashMap, MapConstraints.NOT_NULL
	);

Constrainty si samozřejmě můžete implementovat vlastní. Jsou aplikovány před vložením nového záznamu do kolekce a jednoduše tak můžete ohlídat hodnoty, které se v listu nachází. To podstatně zvyšuje míru důvěry, kterou takové kolekci ve svém kódu můžete dát.

Filtrování hodnot v iterátoru

Velmi elegantně a čitelně lze také filtrovat hodnoty, které poskytuje iterátor nad kolekcí.


import static com.google.common.base.Predicates.*;
...
Iterator originalIterator;
Iterator filteredIterator = Iterators.filter(
		originalIterator,
		not(isNull())
);

Predikáty je možné slučovat pomocí and, or nebo negovat jako ve výše uvedeném příkladě. Predikáty je možné volně vytvářet. Čitelnost kódu je daleko lepší než ifování v iteraci. Nyní je predikát definován jako rozhranní s jedinou metodou - ideální adept na zavedení closure.

Třída iterators obsahuje řadu dalších užitečných metod:

  • addAll(Collection collection, Iterator<? extends T> iterator) - vloží všechny prvky iterátoru do kolekce
  • all(Iterator iterator, Predicate<? super T> predicate) - vrátí true, pokud všechny prvky iterátoru odpovídají podmínce
  • any(Iterator iterator, Predicate<? super T> predicate) - vrátí true, pokud alespoň jeden prvek odpovídá podmínce
  • concat(….) - různé metody pro slučování více iterátorů do jediného
  • find(Iterator iterator, Predicate<? super E> predicate) - vrátí první prvek iterátoru odpovídající podmínce
  • a další ;-)

Maven

Bohužel knihovna není buildována Mavenem, takže si budete muset zajistit upload do vaší firemní repository sami. Pozitivní je, že nevyžaduje žádné další dependence, takže se jedná o velmi jednoduchou operaci.

Závěrem

Google collections je knihovna obsahující plno drobných vylepšení, funkcí a utilitek, bez kterých se sice obejdete, ale které vám mohou ušetřit plno řádků kódu (a to nemluvím o kódu testujících váš kód) a které mohou zásadně přispět k zpřehlednění kódu jako takového. Knihovnu využijete až od Javy 1.5, jelikož hlavní podpora je spojená právě s generikami. Knihovnu můžu jen doporučit a díky pánům z JavaPosse za dobrý tip a Googlu za open source.

BTW: nemáte někdo pár akcií na prodej? :-)

Související odkazy