Niuans korzystania z funkcji Lists.transform z Google Guava

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.

O autorze

Marek Berkan Marek Berkan: programista, entuzjasta tworzenia oprogramowania, zarządzania zespołami technicznymi. Prywatnie motocyklista, kolarz MTB, biegacz, żeglarz, rekreacyjny wspinacz, zamiłowany turysta. Witryny: , , .

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *