Cześć, dzisiaj zostawiam w twoich rękach trzecią, a zarazem ostatnią część mini cyklu poświęconego transformacjom kodu Javy do kodu Kotlina. Dzisiaj pokażę Ci jak przyjemnie możemy korzystać z kolekcji, jak łatwo utworzyć singleton oraz w jaki sposób można rozszerzać funkcjonalności klas, choćby o ile nie możemy modyfikować ich kodu źródłowego.
Kolekcje
W pierwszej kolejności – kolekcje. Nie takie złe w Javie. Czytelne, dużo implementacji, wiele przydatnych metod…ale czy takie przyjemne w użyciu? Spróbujmy stworzyć tablicę i stworzyć z niej listę. Następnie, dzięki strumieni będziemy trochę obrabiać kolekcje, potem spróbujemy powyciągać niektóre elementy. Jakiś mapping, może grupowanie…a czemu nie, nie są to tak wcale rzadko spotykane sytuacje
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | import
javafx.util.Pair; import java.util.*; import java.util.stream.Collectors; public class SimpleCoding { public static void main(String[] args) { // 1 - tworzenie tablicy, stworzenie z niej listy niemutowalnej i mutowalnej int[] number = new int[] {1,2,3,4}; System.out.println(number); List<Integer> list = Arrays.asList(1, 2, 3, 4); // list.add(5); // exception List<Integer> mutableList = new ArrayList<>(); mutableList.addAll(Arrays.asList(new Integer[] {5,6,7,8})); mutableList.add(9); // 2 - strumieniowe filtrowanie listy i wyswietlanie wynikow System.out.println("Only even from immutable: "); list.stream().filter(num -> num % 2 == 0).forEach(System.out::println); // 3 - pobieranie pierwszego i ostatniego elementu z listy System.out.println("Immutable first element " + list.get(0)); System.out.println("Immutable last element " + list.get(list.size() - 1)); // 4 - laczenie list List<Integer> mergedLists = new ArrayList<>(); mergedLists.addAll(list); mergedLists.addAll(mutableList); // 5 - grupowanie, wyliczanie sredniej, pobieranie elementow z mapy Map<Boolean, List<Integer>> groupedEvenNumber = new HashMap<>(); groupedEvenNumber.put(true, new ArrayList<>()); groupedEvenNumber.put(false, new ArrayList<>()); mergedLists.stream().forEach(num -> groupedEvenNumber.get(num % 2 == 0).add(num)); System.out.println(groupedEvenNumber); System.out.println("Even: " + groupedEvenNumber.get(true)); System.out.print("Even first: "); System.out.println(groupedEvenNumber.get(true) != null ? groupedEvenNumber.get(true).get(0) : "not found"); System.out.print("Even average: "); System.out.println(groupedEvenNumber.get(true) != null ? groupedEvenNumber.get(true).stream().mapToInt(x -> x).summaryStatistics().getAverage() : "not found"); // 6 - mutowalne i niemutowalne mapy Map<String, Object> tmpMap = new HashMap<>(); tmpMap.put("name", "Artur Czopek"); tmpMap.put("age", 24); Map<String, Object> map = Collections.unmodifiableMap(tmpMap); // map.put("male", true); // will not work Map<String, Object> mutableMap = new HashMap<>(); mutableMap.putAll(tmpMap); mutableMap.put("male", true); System.out.println(map); System.out.println(mutableMap); // 7 - odczytywanie par z mapy - rozne operacje na mapach mutableMap.entrySet().stream().forEach(entry -> System.out.println("Key: " + entry.getKey() + ", value: " + entry.getValue())); List<Pair> convertedMap = mutableMap.entrySet().stream().map(entry -> new Pair(entry.getKey(), entry.getValue())).collect(Collectors.toList()); System.out.println(convertedMap); System.out.println("First pair key and value: " + convertedMap.get(0).getKey() + " -> " +convertedMap.get(0).getValue()); Map<String, Boolean> stringCheckMap = new HashMap<>(); mutableMap.entrySet().stream().forEach(entry -> stringCheckMap.put(entry.getKey(), entry.getValue() instanceof String)); System.out.println("Is string: " + stringCheckMap); } |
Krok po kroku wyjaśnijmy co robimy w tym kodzie.
1. Tworzymy tablicę integerów. Bardzo prosto możemy w Javie stowrzyć tablicę typu jakiego byśmy nie chcieli. Stworzenie listy z tablicy jest proste np za pomcą funkcji Arrays.asList(). Problemem (?) jest to, iż lista jest niemutowalna. Efekt zły nie jest, ale może być niepożądany i wiele osób może zaskoczyć wyjątek przy próbie dodania elementu do listy. o ile chcemy stworzyć listę mutowalną z obiektów to musimy stworzyć np listę dzięki metody powyżej (tym razem podaję jako argument tablicę, nie elementy, natomiast musi to być tablica dla jakieś klasy, nie prymitywów!), a następnie dodać ją do innej listy już niemutowalnej. Trochę kodu dla prostych operacji trzeba było napisać…
2. Od Javy 8 mamy streamy i lambdy. Tutaj raczej bez zarzutów, bardzo prosto jesteśmy w stanie wyciągnąc z listy elementy tylko spełniające konkretny warunek, a następnie je wyświetlić.
3. Pobieranie skrajnych elementów z listy jest też całkiem proste. O ile pierwszy element wyciągniemy po indeksie, o tyle do ostatniego już musimy znać rozmiar tablicy bo możemy się narazić na wyjątek. Można to zrobić ładniej, w Kotlinie rzecz jasna
4. Łączenie list – wymagane stworzenie nowej kolekcji wprost i dodanie do niej dopiero tych list. Spoko, ale – tak – da się lepiej
5. Grupowanie kolekcji też może mieć miejsce. Stety niestety, nie mamy dedykowanych metod do tego i musimy trochę kodu naprodukować aby stworzyc kolekcje w mapie, zdefiniować odpowiednie klucze, a dopiero potem grupować. Pobranie elementów z pogrupowanych list naraża nas na nulla dość mocno, tak samo robienie różnych obliczeń, chociażby średniej wartości. Trzeba być ostrożnym.
6. o ile chodzi o mutowalność i niemutowalność map – możemy to też uzyskać. Jest to odrobinę przyjemniejsze niż z listami chociażby, aczkolwiek przez cały czas musimy trochę kodu wyrzeźbić, a dopiero potem dzięki odpowiednich funkcji, przykładowo Collections.unmodifiableMap() stworzyć kolejną kolekcję spełniającą nasze warunki. Łączyć mapy możemy chociażby poprzez wołanie metody putAll() na jednej z nich, która otrzymuje inną mape i dodaje elementy mapy do tej, na której wołamy funkcję.
7. Kilka mniejszych operacjach na mapach na koniec. Odczytywanie wszystkich kluczy i wartości w mapie od Javy 8 jest całkiem przyjemne (tylko te nadmiarowe gettery…). Niedawno zdarzyło mi się w Kotlinie zrobić z mapy listę par. Byłem ciekaw na ile to jest możliwe w Javie. Stety niestety, najlepszy znany mi sposób to było oczywiście użycie streamów, natomiast pary mogłem stworzyc dzięki klasy Pair która pochodzi z JavyFX. No, nie najlepiej. Ostatnia operacja na mapie w tym przykładzie to stworzenie nowej mapy, która przechowuje informacje o tym który element na danej mapie jest typu String a który nie.
Standardowo, kontrprzykład w Kotlinie, z zachowaniem kolejnych punktów.
Kotlin:
fun main(args: Array<String>) { // 1 - tworzenie tablicy, stworzenie z niej listy niemutowalnej i mutowalnej // val numbers = Int(){1,2,3,4 } // will not work val number = intArrayOf(1, 2, 3, 4) println(number) val numberArray = arrayOf(1, 2, 3, 4) println(numberArray) val list = listOf(1, 2, 3, 4) val mutableList = mutableListOf(5, 6, 7, 8) // list.add(5) // will not work // list.remove(5) // will not work mutableList.add(9) // 2 - strumieniowe filtrowanie listy i wyswietlanie wynikow println("Only even from immutable: ${list.filter { it % 2 == 0 }}") // println("Only even from immutable: ${list.asSequence().filter { it % 2 == 0 }.toList()}") // 3 - pobieranie pierwszego i ostatniego elementu z listy println("Immutable first element ${list[0]}") // list.first() println("Immutable last element ${list.last()}") // 4, 5 - laczenie list, grupowanie, wyliczanie sredniej, pobieranie elementow z mapy val groupedEvenNumber = (list + mutableList).groupBy { it % 2 == 0 } println(groupedEvenNumber) println("Even: ${groupedEvenNumber[true]}") println("Even first: ${groupedEvenNumber[true]?.first() ?: "not found"}") println("Even average: ${groupedEvenNumber[true]?.average() ?: "not found"}") // 6 - mutowalne i niemutowalne mapy val map = mapOf( "name" to "Artur Czopek", "age" to 24 ) // map["male"] = true // will not work val mutableMap = map.toMutableMap() mutableMap["male"] = true println(map) println(mutableMap) // 7 - odczytywanie par z mapy - rozne operacje na mapach mutableMap.forEach { key, value -> println("Key: $key, value $value") } val convertedMap = mutableMap.toList() println(convertedMap) println("First pair key and value: ${convertedMap[0].first} -> ${convertedMap[0].second}") val stringCheckMap = mutableMap.mapValues { it.value is String } println("Is string: $stringCheckMap") }Kodu jest wiele mniej! Warto zauważyć, iż nie importuję tutaj żadnych rzeczy dla kolekcji! Kotlin automatycznie dodaje importy powiązane z kolekcjami do plików.
1. Pierwsza ważna różnica, tablice w Kotlinie są tworzone zupełnie inaczej niż w Javie. Tablice w Kotlinie to klasa generyczna! Można to zobaczyć chociażby przy definicji funkcji main – przyjmuje ona jako argument args typu Array. Dla prymitywnych typów mamy jednak dedykowane funkcje do tworzenia tablic, takie jak np intArrayOf. Są one czytelniejsze, a także dzięki takim wywołanim Kotlin optymalizuje działanie kodu dla takich tablic.
2. Filtrowanie kolekcji pozostało protsze – wystarczy, iż użyję na kolekcji metody filter, nie potrzebuje wołać funkcji stream(). Ponadto, podanie warunku jest trochę bardziej przejrzyste. Do funkcji filter podajemy lambdę która używa jednego parametru, więc mogę korzystać ze słówka it, które odwołuje się do obiektu z kolekcji. Ponadto, nie muszę potem wołać funkcji forEach! Funkcja toString dla kolekcji w Kotlinie jest wiele przyjemniej zaimplementowana niż te w Javie, więc mogę po prostu wypisać kolekcję. Warto na tym etapie zaznaczyć, iż nie używamy tutaj streamów z Javy 8, które są leniwe (operacje są wykonywane dopiero, kiedy to “potrzebne”). Jest to de facto w zdekompilowanym kodzie while oraz if. Dobrze mieć tego świadomość. Możemy użyc zakomentowanej linijki do operowania na sekwencjach, które są leniwe. Po więcej szczegółow o różnicach między tymi dwoma rodzajami kolekcji zapraszam do tego artykułu. Od niedawna IntelliJ choćby podpowiada by sekwencji używać. Zwróć też uwagę, iż po operacji na sekwencji wołam jeszcze funkcję toList(). Dlaczego? Usuń to wywołanie, a się przekonasz co innego jest tam zwracane
3. Pobieranie elementów z kolekcji jest także proste i przyjemne. Tam, gdzie mamy funkcję get(), możemy użyć przyjemniejszej notacji, czyli podania argumentu w kwadratowych nawiasach. W podobny sposób robimy to w JSie gdy wyciągamy elementy z JSONa po kluczu. Dla ostatniego i pierwszego elementu w liście mamy też dedykowane metody, takie jak first() i last(), które zwiększają czytelność kodu.
4., 5. – te dwa punkty aż prosiło się połączyć. Listy w Kotlinie implementują funkcję dla operatora plus! dzięki wywołania lista1 + lista2 jesteśmy w stanie stworzyć zupełnie nową listę. Jak przejrzyście! Od razu w tej samej linijce robię grupowanie na nowo utworzonej liście. Do funkcji groupBy podaję jedynie warunek jako lambdę po którym dla elementu ma być tworzony klucz. W tym przypadku kluczem jest true/false. Po odpowiednim kluczu elementy pobieram jak z listy, w kwadratowych nawiasach. Jak to z mapami bywa, mamy ryzyko, iż pod danym kluczem nic się nie ukrywa. Istnieje ryzyko wystąpienia nulla. Kotlin, jako język null safety, wymaga od nas użycia przy takim wywołaniu znaku zapytania. Używając “elvisa” jestem w stanie w łatwy sposób zdefiniować wartość alternatywną w przypadku gdy po lewej stronie operatora wystąpi null.
6. Do tworzenia map mamy również dedykowane funkcje – mapOf() dla map niemutowalnych, a dla mutowalnych mutableMapOf. Funkcje te przyjmują obiekty klasy Pair z Kotlina, nie w JavyFX. Mają one pola first oraz second, nie key i value. Wracając do funkcji, mogą one przyjmować od razu pary jako argumenty, oddzielone przecinkami. Ponadto, pary możemy tworzyć dzięki infixowej funkcji to w notacji first to second. Bardzo czytelne, prawda? Kolejna fajna rzecz – mapę niemutowalną możemy przekonwertować do mutowalnej dzięki metody toMutableMap. Wywołanie metody put w mapie może być w Kotlinie zastąpione np poprzez podanie klucza jak do geta, a następnie dzięki znaku równa się definiujemy nową wartość, jaka powinna być pod podanym kluczem. Metoda toString dla map także jest przyzwoicie zaimplementowana.
7. Operowanie na kluczach i wartościach w mapie jest też przyjemne. Na przykład dla metody forEach występuje destrukturyzacja, od razu możemy odwolać się do klucza i wartości bez odwoływania się wprost do konkretnego obiektu. o ile chodzi o konwertowanie mapy do listy – także dedykowana metoda – toList. Otrzymujemy wtedy listę typu Pair o którym wspominałem wcześniej. o ile chodzi o tworzenie nowej mapy, która przechowuje informacje o tym czy wartość dla danego klucza jest Stringiem czy nie to też mamy usprawnienie – możemy użyć funkcji mapValues, która w odpowiedni sposób skonwertuje nam każdą wartość. dzięki słówka it odwołujemy się do pojedyńczego recordu w mapie.
Niektóre kody, prawda. Postanowiłem to jednak pokazać w ten sposób, gdyż kolekcji używamy bardzo często, a przede wszystkim w bardzo różny sposób.
Singleton
Singleton – jeden ze wzorców projektowych, z którym wiele osób spotyka się gwałtownie na drodze programowania. Wzorzec ten też jest często implementowany, częściej niż nam się wydaję. Dla niezaznajomionych – chodzi o to, iż jesteśmy w stanie stworzyć tylko jedną instancję danego typu. W Javie możemy to zrobić chociażby poprzez zdefiniowane tej instancji jako zmienna statyczną, zdefiniowania wszystkich konstruktorów jako private (tak, aby nikt nie był w stanie ich wywołać), a następnie poprzez zdefiniowanie metody, która pobiera tą jedyną instancję gdy istnieje, w innym przypadku tworzy instancję. W przykładzie poniżej stworzymy counter, którego wartość możemy odczytywać lub inkrementować dedykowaną metodą.
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public
class SimpleCoding { public static void main(String[] args) { System.out.println("Counter amount: " + Counter.INSTANCE.getAmount()); Counter.INSTANCE.inc(); System.out.println("Counter amount: " + Counter.INSTANCE.getAmount()); Counter counter = Counter.INSTANCE; System.out.println("Counter " + Counter.INSTANCE); System.out.println("counter " + counter); counter.inc(); System.out.println("Counter amount: " + Counter.INSTANCE.getAmount()); System.out.println("counter amount: " + counter.getAmount()); } } class Counter { public static final Counter INSTANCE; private int amount = 0; private Counter() { } static { INSTANCE = new Counter(); System.out.println("Creating counter..."); } public int getAmount() { return amount; } public void inc() { System.out.println("Increment"); amount++; } } |
OUTPUT:
1 2 3 4 5 6 7 8 9 | Creating counter... Counter amount: 0 Increment Counter amount: 1 Counter Counter@6d06d69c counter Counter@6d06d69c Increment Counter amount: 2 counter amount: 2 |
Jak możesz zauważyć, przy pierwszym pobraniu countera mamy wypisany komunikat, iż counter jest tworzony. Potem to już nie występuję bo jest tworzony tylko raz. Następnie countera inkrementujemy. Nieważne, czy pobieramy wartość dzięki metody getInstance() czy ze zmiennej do której przypisaliśmy referencje do tego singletonu, zawsze odwołujemy się do tego samego obiektu w pamięci. Adres w pamięci też się zgadza. Wygląda na to, iż wzorzec singleton został zaimplementowany poprawnie. Teraz kontrprzykład w Kotlinie.
Kotlin:
fun main(args: Array<String>) { println("Counter amount: ${Counter.amount}") Counter.inc() println("Counter amount: ${Counter.amount}") val counter = Counter println("Counter $Counter") println("counter $counter") counter.inc() println("Counter amount: ${Counter.amount}") println("counter amount: ${counter.amount}") } object Counter { init { println("Creating counter...") } var amount = 0 private set fun inc() { println("Increment") amount++ } }A cóż to się stało! Gdzie jakieś statyczne instancje, definicje pobierania elementu? No, nie ma. Aby stworzyć singleton w Kotlinie, wystarczy przy definicji klasy zamiast słówka class użyć słówka object, które sprawia, iż mamy tylko jedną instancję danej klasy. o ile chcemy, aby podczas tworzenia instancji wykonała się jakaś logika, na przykład wypisanie komunikatu, możemy to zdefiniować w bloku init. Mamy tak samo gettera (setter zdefiniowany jako prywatny), a także funkcję inc() do inkrementacji. Do instancji singletonu odwołujemy się wprost po nazwie klasy. Możemy to ponownie przypisać do zmiennej, natomiast ponownie będzie to ten sam obiekt, wartości te same, adresy też takie same. Bardzo prosta implementacja jakże popularnego wzorca projektowego!
Rozszerzanie funkcjonalności
Na koniec całego cyklu powiemy sobie trochę o rozszerzaniu klas o nowe funkcjonalności. Załóżmy, iż mamy klasę String. Klasa bardzo często używana, natomiast jest to klasa finalna w Javie. Nie możemy jej ani rozszerzyć, ani modyfikować. Chcielibyśmy jednak mieć dedykowaną metodę, która zwracałaby nowy łańcuch tekstowy składający się tylko ze spółgłosek. Mogą być też inne metody powiązane z tą konkretną klasą. Jakie jest standardowe podejście w Javie? Stworzenie dedykowanej klasy, jak na przykład StringUtils w tej sytuacji bądź też StringHelper. Nazwa jest bardzo wymowna. Następnie, nasze pomocnicze funkcje są definiowane jako statyczne, oraz zawsze jako argument otrzymują obiekt typu dla którego ta klasa pomocnicza jest tworzona. Mogą też być inne dodatkowe argumenty. W przykładzie poniżej, jak wspomniałem, chcemy mieć dodatkową funkcję dla łańcuchów tekstowych, która zwróci nam nowy łańcuch, bez samogłosek.
Java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import
java.util.Arrays; import java.util.List; class StringUtils { public static String removeVowels(String toRemoved) { final List<Character> vowelsList = Arrays.asList(new Character[]{'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}); StringBuilder sb = new StringBuilder(toRemoved); int i = 0; while (i < sb.length()) { if (vowelsList.contains(sb.charAt(i))) { sb.replace(i, i + 1, ""); i--; } i++; } return sb.toString(); } } public class SimpleCoding { public static void main(String[] args) { String artur = "Artur"; String simpleCoding = "simpleCoding.pl"; String zxcvb = "zxcvb"; System.out.println(StringUtils.removeVowels(artur)); // rtr System.out.println(StringUtils.removeVowels(simpleCoding)); // smplCdng.pl System.out.println(StringUtils.removeVowels(zxcvb)); // zxcvb } } |
Zgodnie z tym, co napisałem powyżej – stworzyłem klasę pomocniczą StringUtils, która posiada jedną metodę – removeVowels. Implementacja nie jest w tym przypadku istotna. Zwróćmy uwagę na sposób użycia metody. Wołamy metodę statyczną z tej klasy i tam dopiero podajemy jako argument nasz łańcuch tekstowy. Jak z tego typu sytuacjami radzi sobie Kotlin?
Kotlin:
fun String.removeVowels(): String { val vowelsList = listOf('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U') val sb = StringBuilder(this) var i = 0 while (i < sb.length) { if (vowelsList.contains(sb[i])) { sb.replace(i, i + 1, "") i-- } i++ } return sb.toString() } fun main(args: Array<String>) { val artur = "Artur" val simpleCoding = "simpleCoding.pl" val zxcvb = "zxcvb" println(artur.removeVowels()) println(simpleCoding.removeVowels()) println(zxcvb.removeVowels()) }Hola, hola! Czy ja wołam funkcję removeVowels() na obiekcie typu String? Tak! W Kotlinie możemy dodawać funkcje do klas, choćby o ile one są finalne i nie mamy dostępu do ciała klasy! Funkcje tworzymy jak każdą inną z jedną różnicą – przed nazwą funkcji podajemy nazwę klasy do której dodajemy metodę, a następnie po kropce dopiero definiujemy funkcję. W jej obrębie możemy odwoływać się poprzez słówko this do obiektu – w tym przypadku typu String – na którym funkcja jest wołana. Wywołanie w kodzie jest takie same jak dla każdej innej funkcji z danej klasy.
Warto jednak nadmienić, iż takie funkcje pod spodem są kompilowane bardzo podobnie jak nasz StringUtils w Javie i wołane są jako funkcje statyczne. Nie są to funkcje dodane do tej klasy! Jest to jednak duże uproszenie z punktu czytelności kodu. Po więcej informacji o rozszerzeniach w Kotlinie zapraszam do dokumentacji. Potęzna funkcjonalność.
Podsumowanie
W ten oto sposób dotarliśmy do końca trzyczęściowego cyklu wpisów, w którym pokazywałem transformaty kodu Javy do kodu Kotlina. Wiele rzeczy piszemy w Kotlinie zdecydowanie bardziej zwięźle i czytelniej. Przekłada się to oczywiście na szybsze wytwarzanie kodu po opanowaniu podstaw tego pięknego języka. Mam nadzieję, iż tymi praktycznymi przykładami jeszcze bardziej przekonałem Cię do używania Kotlina, chociażby w prywatnych lub/i eksperymentalnych projektach.
Jeszcze na zakończenie – zachęcam Cię do obserwowania tego, co będzie działo się na blogu. W najbliższych tygodniach wypuszczam pierwszą część mojej małej książki o Kotlinie w kontekście mikroserwisów. Po prawej stronie możesz zapisać się do newslettera, wtedy na pewno książka dotrze na twojego emaila Ponadto, zima idzie, mikołajki nadchodzą…w najbliższym czasie sprawię, iż zabawa w Świętego Mikołaja będzie jeszcze bardziej przyjemna. Stay tuned!
Pozostałe części:
Część pierwsza
Część druga