Mariusz Prowaźnik

o programowaniu w Javie, Scali i Clojure.


Apache Commons: Collections

W tym poście dowiesz się do czego i jak można użyć komponentu Collections, zawartego w Apache Commons.

Dobry programista, zanim zacznie rozwiązywać problem, sprawdzi, czy nie można użyć gotowego rozwiązania. A zwykle można. I naprawdę wiele z nich zostało zawartych w bibliotekach Apache Commons (dawniej Jakarta Commons).

Myślałeś kiedyś o tym, że przydałaby się klasa działająca jak HashMap przyjmująca kilka kluczy, albo zachowująca kolejność, iterator dla HashMap czy kolejka z priorytetem? Jeśli tak, to nie jesteś pierwszy - o tym samym pomyśleli autorzy komponentu Collections.

Dodanie zależności
Aby móc używać commons-collections dodaj do pom.xml zależność:
<dependency>
 <groupId>commons-collections</groupId>
 <artifactId>commons-collections</artifactId>
 <version>3.2.1</version>
</dependency>
A najlepiej wyszukaj tę zależność na http://mvnrepository.com bo możliwe, że teraz gdy to czytasz jest już wersja nowsza niż 3.2.1

Poniżej zaprezentuję wybrane klasy i interfejsy z commons-collection.
MultiKeyMap
Jest to mapa, w której kluczem może być od 1 do 5 zmiennych. Jako przykład posłużyć może mapa zawierająca oceny, w której kluczami są nazwa klasy i nr w dzienniku
MultiKeyMap mapaOcen = new MultiKeyMap();

  mapaOcen.put("IIc", 14, "bardzo dobry");
  mapaOcen.put("IIc", 13, "dobry");
  mapaOcen.put("Ic", 13, "dostateczny");
  mapaOcen.get("IIc", 14);
  mapaOcen.get("IIc", 13);
  mapaOcen.get("Ic", 13);
IterableMap, MapIterator
Przeiterowanie mapy z java.util nie jest tak wygodne jak w przypadku setów i list. Nie są dostępne zarówno konstrukcje typu:
HashSet<String> set = new HashSet<String>();
  for (String string : set) {
   System.out.println(string);
  }
jak i
HashSet<String> set = new HashSet<String>();
  while (set.iterator().hasNext())
   System.out.println(set.iterator().next());
Oczywiście, mapę można przeiterować po EntrySet:
HashMap<String, String> mapa = new HashMap<String, String>();
  for (Entry<String, String> entry : mapa.entrySet()) {
   entry.getKey();
   entry.getValue();
  }
(btw, iterowanie po keySet, by potem w pętli wyciągać elementy po kluczu jest błędem) Ale można również użyć odpowiednika z Commons Colections implementującego interfejs IterableMap - klasę HashedMap:
HashedMap mapa = new HashedMap();
  mapa.put("klucz", "wartosc");
  MapIterator iterator = mapa.mapIterator();
  while (iterator.hasNext()){
   String klucz = (String) iterator.next();
   String wartosc = (String) iterator.getValue();
  }
Inne klasy implementujące interfejs IterableMap to np: MultiKeyMap, IdentityMap, LinkedMap, LRUMap, TreeBidiMap
OrderedMap i OrderedMapIterator
Kolekcje implementujące ten interfejs charakteryzują się ustaloną kolejnością elementów, a dzięki OrderedMapIterator można iterować mapę w obie strony.
Zatem kolekcja LinkedMap przechowuje obiekty w takiej kolejności, w jakiej zostały dodane:
OrderedMap mapa = new LinkedMap();
  mapa.put("tomek", "wartosc1");
  mapa.put("ada", "wartosc2");
  OrderedMapIterator iterator = mapa.orderedMapIterator();
  while (iterator.hasNext()) {
   System.out.println((String) iterator.next() + " = "
     + (String) iterator.getValue());
  }
  while (iterator.hasPrevious()) {
   System.out.println((String) iterator.previous() + " = "
     + (String) iterator.getValue());
  
tomek = wartosc1
ada = wartosc2
ada = wartosc2
tomek = wartosc1
a klasa TreeBidiMap w kolejności posortowanej:
OrderedMap mapa = new TreeBidiMap();
  mapa.put("tomek", "wartosc1");
  mapa.put("ada", "wartosc2");
  OrderedMapIterator iterator = mapa.orderedMapIterator();
  while (iterator.hasNext()) {
   System.out.println((String) iterator.next() + " = "
     + (String) iterator.getValue());
  }
  while (iterator.hasPrevious()) {
   System.out.println((String) iterator.previous() + " = "
     + (String) iterator.getValue());
  
ada = wartosc2
tomek = wartosc
tomek = wartosc1
ada = wartosc2
Klucze używane w TreeBidiMap muszą implementować interfejs Comparable.
Kolekcja LinkedMap jest odpowiednikiem java.util.LinkedHashMap i w zasadzie różni się tym, że jest dostępny dla niej iterator. W java.util jest też kolekcja TreeMap w której elementy są posortowane i nawet ma jedną przewagę nad TreeBidiMap, tj przyjmuje w konstruktorze Comparator co pozwala na większą swobodę w określania sposobu porównywania elementów (można np zmienić sposób porównania String'ów), ale TreeBidiMap dysponuje inną ciekawą cechą, dzięki zaimplementowaniu interfejsu BidiMap
BidiMap
W mapach dwukierunkowych można nie tylko wyszukiwać wartość po kluczu, ale również wyszukać klucz po wartości, albo stworzyć mapę odwrotną - w której wartości stają się kluczami, a klucze wartościami:
BidiMap mapa = new TreeBidiMap();
  mapa.put("klucz", "wartosc");
  mapa.put("inny klucz", "inna wartosc");
  mapa.get("klucz");
  mapa.getKey("inna wartosc");
  mapa.inverseBidiMap();  
Jako, że wartości w TreeBidiMap są unikalne, włożenie do mapy elementu o istniejącej wartości nadpisze istniejący klucz:
BidiMap mapa = new TreeBidiMap();
  mapa.put("inny klucz", "inna wartosc");
  mapa.put("cos", "inna wartosc");
  System.out.println(mapa);
{cos=inna wartosc}
Bag
Bag to kolekcja z licznikiem wystąpień elementu. Podstawowe implementacje to HashBag i TreeBag. Przykład:
Bag bag = new HashBag();
  bag.add("element", 16); // dodaje 16 elementów
  bag.remove("element", 2); // zdejmu 2 elementy
  bag.getCount("element"); // zwraca licznik elementów, w tym przypadku 14

To oczywiście tylko kilka z wielu klas i interfejsów z Apache Commons. Więcej informacji znajdziesz tutaj: http://commons.apache.org/collections/api-release/index.html

Obsługa wyjątków: Finally

Słowo kluczowe finally stosujemy, gdy chcemy, by jakaś część kodu wykonała się niezależnie od tego, czy w bloku try-catch zostanie wyrzucony wyjątek. Zatem po wykonaniu takiego kodu:

public class Test {
 public static void main(String[] args) {
  try {
   String a = null;
   System.out.println(a.charAt(0));
  } catch (NullPointerException ex) {
   System.out.println("Null pointer!");
  } finally {
   System.out.println("Hello world!");
  }
 }
}
otrzymamy:
Null pointer!
Hello world!
Gdyby zmienić String a = null na String a ="arbuz" otrzymamy:
a
Hello world!
Wydawałoby się, że na tym można skończyć pisać o try-finally, jednak istnieje kilka ciekawych przypadków, o których do niedawna nie wiedziałem. Np. kod w bloku finally wykona się nawet jeśli w bloku try zostanie zamieszczona instrukcja kończąca wykonanie metody i zwracająca wartość return:
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) {
  try {
   return;
  } finally {
   System.out.println("Hello world!");
  }
 }
}
Wynik:
Hello world!
Podobnie jest w przypadku instrukcji typu break, continue:
package com.blogspot.evojava;

public class App {
 public static void main(String[] args) {
  for (int i = 0; i < 5; i++) {
   System.out.println("Hello World! " + i);
   try {
    break;
   } finally {
    continue;
   }
  }
 }
Hello World! 0
Hello World! 1
Hello World! 2
Hello World! 3
Hello World! 4
Jedynym sposobem, żeby "powstrzymać" finally jest zamknięcie JVM za pomocą System.exit(0):
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) {
  try {
   System.exit(0);
  } finally {
   System.out.println("Hello world!");
  }
 }

Należy pamiętać też o tym, że jeśli w bloku finally zostanie wyrzucony wyjątek, to dalsze przetwarzanie zostanie przerwane:
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) throws Exception {
  String str = null;
  try {
  } finally {
   str.compareTo("a");
   System.out.println(" world!");
  }
 }
Exception in thread "main" java.lang.NullPointerException
 at com.blogspot.evojava.Test.main(Test.java:8)
I pozostaje jeszcze inna ważna sprawa: niepoprawne skonstruowanie bloku try-finally może skutkować bardzo niebezpiecznym dla programu błędem. Przykład podany jest poniżej:
package com.blogspot.evojava;

public class Test {
 public static void main(String[] args) throws Exception {
  try {
   throw new Exception("Ważny wyjątek");
  } finally {
   throw new Exception("Nieistotny wyjątek");
  }
 }
}
Exception in thread "main" java.lang.Exception: Nieistotny wyjątek
 at com.blogspot.evojava.Test.main(Test.java:8)
W kodzie tym jeden z wyjątków w ogóle nie został obsłużony, ani nigdzie nie został przekazany. Mógłby być powodem złego działania programu, a programista w ogóle by się o nim nie dowiedział, bo wszelki ślad po wyjątku zaginął. W bardziej skomplikowanym kodzie taki błąd może być bardzo kosztowny i trudny do odnalezienia. Dlatego warto wiedzieć o takim przypadku.