Transformacja Javy do Kotlina – proste przykłady – część trzecia

Simple-Coding-java-kotlin

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, nawet jeżeli 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, za pomocą 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, że 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. Jeżeli chcemy stworzyć listę mutowalną z obiektów to musimy stworzyć np listę za pomocą 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. Jeżeli chodzi o mutowalność i niemutowalność map – możemy to też uzyskać. Jest to odrobinę przyjemniejsze niż z listami chociażby, aczkolwiek nadal musimy trochę kodu wyrzeźbić, a dopiero potem za pomocą 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 za pomocą 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:

Kodu jest wiele mniej! Warto zauważyć, że 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 jest jeszcze protsze – wystarczy, że 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ć, że 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 nawet podpowiada by sekwencji używać. Zwróć też uwagę, że 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! Za pomocą 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, że 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ć za pomocą infixowej funkcji to w notacji first to second. Bardzo czytelne, prawda? Kolejna fajna rzecz – mapę niemutowalną możemy przekonwertować do mutowalnej za pomocą 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 za pomocą 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. Jeżeli chodzi o konwertowanie mapy do listy – także dedykowana metoda – toList. Otrzymujemy wtedy listę typu Pair o którym wspominałem wcześniej. Jeżeli 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ść. Za pomocą 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ę szybko na drodze programowania. Wzorzec ten też jest często implementowany, częściej niż nam się wydaję. Dla niezaznajomionych – chodzi o to, że 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, że counter jest tworzony. Potem to już nie występuję bo jest tworzony tylko raz. Następnie countera inkrementujemy. Nieważne, czy pobieramy wartość za pomocą 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, że wzorzec singleton został zaimplementowany poprawnie. Teraz kontrprzykład w Kotlinie.

Kotlin:

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, że mamy tylko jedną instancję danej klasy. Jeżeli 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, że 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:

Hola, hola! Czy ja wołam funkcję removeVowels() na obiekcie typu String? Tak! W Kotlinie możemy dodawać funkcje do klas, nawet jeżeli 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ć, że 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ę, że 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ę, że zabawa w Świętego Mikołaja będzie jeszcze bardziej przyjemna. Stay tuned! 🙂

Pozostałe części:
Część pierwsza
Część druga

2 thoughts on “Transformacja Javy do Kotlina – proste przykłady – część trzecia

  1. Hej, fajny artykuł entry-level, jednak kilka istotnych uwag:
    1. Operatory Kotlinowe na kolekcjach to nie 1-1 to samo, co oferuje Stream API z Javy – chodzi o lazy evaluation. Z tego powodu – pomimo, że jest to lektura dla nowicjuszy – należałoby choć wspomnieć o sekwencjach.
    2. Pokazana implementacja singletonu w Javie jest niebezpieczna wielowątkowo ( zatem niepoprawna, artykuł wprowadza w błąd) oraz nie jest odpowiednikiem 1-1 tego, co generuje Kotlin. Warto przyjrzeć się, co generuje Kotlin używając IntelliJ lub javap.

    Pozdrawiam

    1. Cześć Maciek,

      wybacz, że tak póżno odpowiadam. Gdzieś mi umknął Twój komentarz…

      Dzięki za opinię o artykule. Propo lazy operatorów – mam tego świadomość, że te operatory, które tu pokazałem nie są “leniwe”. Masz rację, dodałem małą aktualizację do tego punktu zaznaczającą, że to nie jest do końca to samo. Ponadto, skoro Kotlin może kompilować się do bytecodu Javy 6, która Streamów nie posiada to jak byśmy mogli tego używać ktoś mógłby pomyśleć… 🙂

      Tak, z singletonem też oczywiście masz rację. Użyłem tutaj implementacji takiej podstawowej, która wielowątkowo jest niebezpieczna, może nadstąpić nadpisanie jednej instancji drugą przy pierwszy pobieraniu chociażby. Implementacje także zaaktualizowałem i zasugerowałem się tą implementacją, którą wypluwa Kotlin, ma to teraz większy sens.

      Dzięki za merytoryczne uwagi i czujność. Każdemu błędy się zdarzają 🙂

      Pozdrawiam serdecznie!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *