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>>();
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:
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);
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.
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ę:
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:
Wszystko wygląda super. Problem zaczyna się gdy będziemy chcieli do naszej listy
transformedListdodać nowy obiekt:Wtedy okaże się że to nie jest zwykła implementacja List, jak np.
ArrayList, tylkoLists$TransformingRandomAccessList, który uniemożliwia modyfikacje. Wynikiem powyższego polecenia będzie błąd: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*”:Spodziewamy się że ostatni obiekt z listy zostanie zmodyfikowany i wyświetlimy nową zawartość listy. Jednak tak nie jest:
Dlaczego? Ponieważ zmienna
transformedListtypuLists$TransformingRandomAccessListto 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 obiektyMyObjectpo 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:
W API Google Guava jest wyraźnie napisane:
Wniosek z tego jest taki, że wynik funkcji
Lists.transformnależy zawsze przepisywać do nowej kolekcji żeby uniknąć takich nieoczywistych zachować naszych aplikacji.Archiwa
Kategorie
Ostatnie posty
AsciiDoctor – dokumentacja techniczna “na poważnie”
25 września 2022Podsumowanie kursu “Droga Nowoczesnego Architekta”
16 stycznia 2021Recenzja książki “Projekt Feniks”
21 sierpnia 2020Kalendarz