Jestem lokalnym ewangelistą korzystania z biblioteki Google Guava która wprowadza do „nudnej” Javy kilka usprawnień, powodujących że programowanie nie jest takie „drewniane”.
Klasycznym przykładem jest użycie skrótowców do tworzenia kolekcji, gdzie zamiast klasycznego new:
Map<Integer,Map<String,String>> map = new HashMap<Integer,Map<String,String>>();
… robimy krótszą wersję:
Map<Integer,Map<String,String>> map = Maps.newHashMap();
Jednak dużo ciekawszą możliwością jest korzystanie z funkcji transformujących obiekty pomiędzy typami, czy filtrujących kolekcje, bo to wnosi elementy eleganckiego programowania funkcyjnego (dostępne już też w JDK8 dla szczęśliwców…)
Przykład jest taki: mamy listę liczb „1, 2, 3”, którą chcemy przerobić na listę napisów, które mają oryginalną liczbę pomiędzy gwiazdkami:
List<Integer> originalList = Lists.newArrayList(); originalList.add(1); originalList.add(2); originalList.add(3); System.out.println("Original list: " + originalList); List<MyObject> transformedList = Lists.transform(originalList, new Function<Integer, MyObject>() { @Override public MyObject apply(Integer input) { return new MyObject("*" + input + "*"); } }); System.out.println("Transformed list: " + transformedList);
Wynik działania takiego programu wygląda tak:
Original list: [1, 2, 3] Transformed list: [*1*, *2*, *3*]
Wszystko wygląda super. Problem zaczyna się gdy będziemy chcieli do naszej listy transformedList
dodać nowy obiekt:
transformedList.add(new MyObject("*4*"));
Wtedy okaże się że to nie jest zwykła implementacja List, jak np. ArrayList
, tylko Lists$TransformingRandomAccessList
, który uniemożliwia modyfikacje. Wynikiem powyższego polecenia będzie błąd:
Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:131) at java.util.AbstractList.add(AbstractList.java:91)
Taki błąd ma tą zaletę że jest ewidentny, jasny, szybko na niego trafimy i poprawimy kod.
Niestety trafiłem na trudniejszy niuans związany ze zwracanym obiektem klasy Lists$TransformingRandomAccessList
. Spójrzmy na poniższy przykład w którym chcę zmodyfikować stan obiektu „*3*” tak aby zawierał wartość „*4*”:
transformedList.get(2).setMyString("*4*"); System.out.println("Transformed list (2): " + transformedList);
Spodziewamy się że ostatni obiekt z listy zostanie zmodyfikowany i wyświetlimy nową zawartość listy. Jednak tak nie jest:
Transformed list (2): [*1*, *2*, *3*]
Dlaczego? Ponieważ zmienna transformedList
typu Lists$TransformingRandomAccessList
to tak naprawdę nie jest nowa kolekcja po transformacji, tylko obiekt-przelotka wykonujący transformacje na każde żądanie. Więc wykonując „transformedList.get(2)
” transformujemy oryginalną kolekcje „1, 2, 3” na obiekty MyObject
po czym modyfikujemy jeden z nich, a następnie wywołując „System.out.println
” ponownie transformujemy oryginalną kolekcje żeby wyświetlić jej zawartość. Stąd wynik nie zawiera zmiany której się spodziewaliśmy.
Jak poprawić powyższy przykład żeby działał zgodnie z oczekiwaniem? Należy wynik funkcji transformującej przypisać do zupełnie nowej listy:
List<MyObject> transformedList = Lists.newArrayList(Lists.transform(originalList, new Function<Integer, MyObject>() { @Override public MyObject apply(Integer input) { return new MyObject("*" + input + "*"); } })); transformedList.get(2).setMyString("*4*"); System.out.println("Transformed list (3): " + transformedList);
W ten sposób otrzymamy oczekiwany wynik:
Transformed list (3): [*1*, *2*, *4*]
W API Google Guava jest wyraźnie napisane:
The function is applied lazily, invoked when needed. … To avoid lazy evaluation when the returned list doesn’t need to be a view, copy the returned list into a new list of your choosing.
Wniosek z tego jest taki, że wynik funkcji Lists.transform
należy zawsze przepisywać do nowej kolekcji żeby uniknąć takich nieoczywistych zachować naszych aplikacji.