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

simplecoding.pl 6 lat temu

Cześć, dzisiaj kontynuacja wpisu o transformacjach kodu javowego do kodu kotlinowego. Dzisiaj pokażę Ci jak łatwo użyć w Kotlinie rzutowania, różnych “statementów” jako “expressions” oraz ile czasu możemy zaoszczędzić przy tworzeniu klas w Kotlinie.

Rzutowania

Taa… rzutowanie i sprawdzenie typów to jedna z dużych bolączek w Javie. Często jesteśmy w stanie przy dobrze zaprojektowanym kodzie to obejść. Załóżmy jednak, iż musimy się zmierzyć z tym problemem. Jesteś początkującym developerem, tworzysz serwis internetowy do którego zostanie wysłana pewna cyfra. Problem w tym, iż nie mamy pewności w jakiej postaci otrzymamy ten numer. Czy to będzie String, czy Int… Załóżmy, iż chcemy w zależnosci od typu chcemy wywołać pewien blok kodu.

Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SimpleCoding {
public static void main(String[] args) {
Object luckyNumber;
// luckyNumber = 1234;
luckyNumber = "1234";

if (luckyNumber instanceof Integer) {
System.out.println("Lucky decremented Int: " + (((Integer) luckyNumber) - 1));
System.out.println("Normal Int value is: " + luckyNumber);
} else if (luckyNumber instanceof String) {
System.out.println("Lucky number withouth first number: " + ((String) luckyNumber).substring(1));
System.out.println("Normal String value is: " + luckyNumber);
} else {
System.out.println("Different type. Value: " + luckyNumber);
}

}
}

To, co się rzuca w oczy to to, iż choćby o ile wiemy jakiego typu jest zmienna to i tak musimy dbać o odpowiednie je zrzutowanie w bloku co później prowadzi do odpowiedniego nawiasowania itd. Brzydko. Jak wyglądałoby to w Kotlinie?

Kotlin:

fun main(args: Array<String>) { var luckyNumber: Any // luckyNumber = 1234 luckyNumber = "1234" when(luckyNumber) { is Int -> { println("Lucky decremented Int: ${luckyNumber.dec()}") println("Normal Int value is: $luckyNumber") } is String -> { println("Lucky number withouth first number: ${luckyNumber.substring(1)}") println("Normal String value is: $luckyNumber") } else -> { println("Different type. Value: $luckyNumber") } } }

Tutaj do upiększenia kodów nie używam if-else tylko when, który jest zasadniczo czytelniejszym switchem. dzięki słówka is jestem w stanie sprawdzić w Kotlinie czy obiekt jest danego typu. Czytelne, prawda? Ponadto, wykonuję dalej blok kodu, w którym chcę wypisać takie same komunikaty i wykonać takie same operacje. Największa magia tutaj to brak rzutowania! Nigdzie nie ma żadnego (String), (Int)! Dlaczego? W Kotlinie występuje coś takiego jak smart cast! Po krzywopolskiemu “mądre rzutowanie”. o ile mamy warunek typu “x jest obiektem takiej i takiej klasy” to w danym bloku kodu obiekt x zostanie automatycznie zrzutowany. Kompilator o tym wie, my nie musimy się o to martwić. Dzięki temu możemy bezpośrednio wołać funkcje specyficzne dla danej klasy.

Oczywiście, zdarzają się jednak i w Kotlinie sytuacje gdzie musimy wykonać rzutowanie. Kolejny raz przykład aplikacji webowej. Załóżmy, iż dostajemy pod postacią JSONa dane o osobie. Załóżmy, iż nie mamy specjalnej klasy i po deserializacji JSONa otrzymujemy mapę, gdzie klucze są typu String, a wartości typu Object. Typ wartości bierze się stąd, iż możemy tam mieć np imię osoby jako tekst oraz wiek jako liczbę. Imię otrzymujemy jako jedną wartość czyli “Imię nazwisko”. W naszym programie chcemy wypisać całe imię, potem tylko nazwisko (kolejne założenie, mamy imię – nazwisko, dwa słowa), wiek oraz pomiędzy jakimi “okrągłymi” latami użytkownik jest w tej chwili w swoim życiu. Dla przykładu, mam 24 lata więc mam lat pomiędzy 20 a 30.

Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Map;
import java.util.HashMap;

public class SimpleCoding {
public static void main(String[] args) {
final Map<String, Object> person = new HashMap<String, Object>();
person.put("name", "Artur Czopek");
person.put("age", 24);

if (person.get("name") instanceof String) {
System.out.println("Full name: " + person.get("name"));
System.out.println("Surname (if any): " + ((String) person.get("name")).split(" ")[1]);
}

if (person.get("age") instanceof Integer) {
System.out.println("Age: " + person.get("age"));
System.out.println("So, he is between " + (((Integer) person.get("age")) / 10 * 10) + " and " + ((((Integer) person.get("age")) / 10 * 10) + 10));
}
}
}


Po raz kolejny sprawdzamy czy wartość jest danego typu (gdyby to był JSON to tak naprawdę mogłoby być tam cokolwiek). o ile tak, to wykonujemy kod. Po raz kolejny brzydkie rzutowania po drodze i nawiasowania. Odpowiednik w Kotlinie poniżej.

Kotlin:

fun main(args: Array<String>) { val person = mapOf( "name" to "Artur Czopek", "age" to 24 ) if (person["name"] is String) { println("Full name: ${person["name"]}") println("Surname (if any): ${(person["name"] as String).split(" ")[1]}") } if (person["age"] is Int) { println("Age: ${person["age"]}") println("So, he is between ${(person["age"] as Int) / 10 * 10} and ${(person["age"] as Int) / 10 * 10 + 10}") } }


Pierwsza rzecz jaka prawdopodobnie rzuca się w oczy to sposób tworzenia mapy. Kolekcje w Kotlinie mają dużo funkcji ułatwiające ich tworzenie. W poprzednim poście była funkcja listOf, tutaj mamy funkcję mapOf, która otrzymuje pary klucz-wartość jako argument. Dzięki infixowej funkcji to jesteśmy w stanie stworzyć parę poprzez wywołanie “key to value”. Mega czytelne i świetne, ale zagłębimy się w kolekcje i tego typu operacje we właściwym czasie.

Przejdźmy do ifów. Typ sprawdzamy w ten sam sposób. Tutaj widzimy kolejny “syntax sugar” w Kotlinie. Funkcję get(K key) w Kotlinie możemy wywołać poprzez podanie klucza w kwadratowych nawiasach. Możemy takiej notacji użyć na prawie każdym obiekcie z naszą implementacją, ale o tym jak i dlaczego w swoim czasie opowiem. Póki co, o ile nie jesteś aż tak dociekliwy to wystarczy Ci wiedzieć, iż w ten sposób wołamy pobranie obiektu po kluczu z mapy. Niestety, dalej smart casty nie działają. Dlaczego? Problemem jest to, iż pobranie elementu z mapy to jest wynik funkcji. o ile przypisalibyśmy to do zewnętrznego obiektu, a potem go sprawdzili to smart cast by zadziałał. W tym przypadku musimy jednak sami zadbać o rzutowanie.

Boli? W Kotlinie nie aż tak. Rzutowanie odbywa się poprzez zapis “object as class”, czyli na przykład tutaj person[“age”] as Int. Mniej nawiasowania potrzebne do wywołania funkcji na takim obiekcie – (x as y).call() vs ((x) y).call(), a także kolejne bardzo ekspresyjne wywołanie. Myślę, iż choćby osoba nie znająca Kotlina jest w stanie “odszyfrować” co chcemy zrobić.

Expressions (wyrażenia)

Na początek tej części małe ogólnikowe przypomnienie czym jest statement a czym expression w programowaniu (tłumaczenie słowa statement nie przemawia do mnie, stąd w wersji angielskiej). Statement to najmniejszy kawałek kodu w programowaniu imperatywnym, który wyraża akcję do obsłużenia. Expression to kombinacja zmiennych, stałych, wywołań funkcji itd, które produkują inną wartość, czyli chociażby proste 2 + 2. Spróbujmy rozjaśnić to na przykładach.

Tworzymy aplikację turniejową, w której trzeba zdobyc określoną ilośc punktów aby się zakwalifikować. Załóżmy, iż kod sprawdzający czy użytkownik się dostał zwraca tylko wiadomość, którą chcemy potem wyświetlić. W Javie proste porównanie dzięki ifa (zwykłego, nie używam tutaj ternariusza, gdyż chcę jeszcze wykonać inne akcje po drodze).

Java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SimpleCoding {
public static void main(String[] args) {
int pointsToQualify = 10;
int userPoints = 9;
// int userPoints = 11;

String qualifyMessage;

if (userPoints >= pointsToQualify) {
System.out.println("HELL YEAH!");
// do sth...
qualifyMessage = "Congratulations! You have more points than required";
} else {
System.out.println(":(");
// do sth...
qualifyMessage = "Sorry! Not this time";
}

System.out.println(qualifyMessage);
}
}


Prosty kod. o ile mamy wystarzcająca ilość punktów to wypisujemy komunikat, robimy ukryte akcje i przypisujemy do zmiennej wartość wiadomości do wyświetlenia użytkownikowi. W przeciwnym przypadku też wykonamy jakąs operację, a na koniec przypiszemy inną wiadomość. Odpowiadający temu kod w Kotlinie.

Kotlin:

fun main(args: Array<String>) { val pointsToQualify = 10 val userPoints = 9 // val userPoints = 11 val qualifyMessage = if (userPoints >= pointsToQualify) { println("Hell yeah!") // do sth... "Congratulations! You have more points than required" } else { println(":(") // do sth... "Sorry! Not this time" } println(qualifyMessage) }


Hola, hola! Co tu się dzieje! Czemu do wiadomości przypisujemy ifa?! W Kotlinie, w przeciwieństwie do Javy, if jest expression, a nie statement. Podobnie jak blok try, try-catch. Podobnie jak when (odpowiednik javowego swticha). No i po co? Między innymi w takich sytuacjach jak ta nie musimy robić przypisań pod spodem tylko możemy od razu zwrócić wynik takiego wyrażenia. W Kotlinie wynikiem wyrażenia jest wynik “ostatniej linijki” w danym bloku do którego wejdziemy. Nie używamy słówka return.

Rozjaśniając, w kodzie powyżej o ile mamy wystarczającą ilość punktów do zakwalifikowania to wiadomością wynikową będzie “Congratulations! You have more points than required”. Ta wiadomość jest na końcu bloku if zdefiniowana, ale do niczego nie przypisana przez nas, Kotlin robi to za nas o ile dojdziemy do tego etapu. o ile jednak operacja się nie uda to przypiszemy do zmiennej qualifyMessage wartość “Sorry! Not this time”, zdefiniowaną na końcu bloku else. Kolejna czytelna rzecz usprawniająca tworzenie kodu. Zaznaczam, iż w instrukcjach when i try możemy zrobić to samo.

Na koniec jeszcze przekonwertujmy kod z wypisywaniem liczby (pierwszy dzisiejszy przykład) do formy jak powyżej, czyli nasze when niech zwraca wielolinijkową wiadomość, którą na koniec wypiszemy.

Kotlin:

fun main(args: Array<String>) { var luckyNumber: Any // luckyNumber = 1234 luckyNumber = "1234" val message = when(luckyNumber) { is Int -> """ Lucky decremented Int: ${luckyNumber.dec()} Normal Int value is: $luckyNumber """.trimIndent() is String -> """ Lucky number withouth first number: ${luckyNumber.substring(1)} Normal String value is: $luckyNumber """.trimIndent() else -> "Different type. Value: $luckyNumber" } println(message) }


Tutaj chyba widać jeszcze większą moc instrukcji when w Kotlinie. Po notacji “warunek -> kod” gdy kod tylko zwraca wartość możemy go pisać bez “wąsów” i przez cały czas jest to czytelne. W tym przypadku w zależności od odpowiedniego typu na końcu warunku jest zwraca konkretna wiadomość i przypiswana do zmiennej, a następnie na koniec programu wypisana. Kolejna nowinka kotlinowa – wiadomości w potrójnych cudzysłowach. W Kotlinie w ten sposób możemy tworzyć wielolinijkowe łańcuchy tekstowe, bez żadnych “znaczków i szlaczków”, piszemy jak zwykły tekst. Nieraz się jednak zdarza, iż programiści mają różne formatowania w IDE, jakieś wcięcia i tabulatory mogą wystąpić itd. Wtedy wiadomość może nie wyglądać tak, jakbyśmy sobie życzyli. Możemy temu zapobiec chociażby poprzez wywołanie na końcu funkcji trimIndent().

Proste klasy tzw POJO

Ostatni przypadek na dziś. Rozbudujmy przykład człowieka z akapitu o rzutowaniu. Nie chcemy robić tym razem mapy, a klasę, która będzie przechowywać informacje takie jak imię, nazwisko, wiek oraz płeć jako flagę mówiącą, czy użytkownik jest kobietą (zostańmy w świecie gdzie człowiek może być kobietą albo mężczyzną). Klasa powinna zawierać konstruktor przyjmujący wszystkie argumenty, gettery, nadpisane metody equals, hashCode i toString. Takie klasy w Javie zwykle są zwane POJO czyli Plain Old Java Object. Jestem natomiast zwolennikiem obiektów niemutowalnych, tak więc będziemy w stanie podać potrzebne nam wartości podczas tworzenia obiektu, pola są finalne, a settery są niedostępne (i tak by nic nie zrobiły).

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
import java.util.Objects;

final class Person {
private final String firstName;
private final String lastName;
private final int age;
private final boolean female;

public Person(String firstName, String lastName, int age, boolean female) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.female = female;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

public boolean isFemale() {
return female;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
female == person.female &&
Objects.equals(firstName, person.firstName) &&
Objects.equals(lastName, person.lastName);
}

@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age, female);
}

@Override
public String toString() {
return "Person(" +
"firstName=" + firstName +
", lastName=" + lastName +
", age=" + age +
", female=" + female +
')';
}
}


Z pozoru prosta klasa, a jednak dużo tzw “boilerplate’u”, cała ceremonia znana wszystkim Javowcom. Jasne, to wszystko można wygenerować dzięki IDE (co też uczyniłem), natomiast przez cały czas jest dużo kodu przez który w prostej klasie trzeba się przekopywać, a każdą następną zmiana w klasie typu dodanie pola lub zmiana nazwy pola jest stosunkowo kosztowna z perspektywy programisty.

Dobrze, użyjmy tej klasy. Stworzymy dwie osoby o takich samych danych. Upewnimy się, iż są to inne obiekty ze względu na referencję, a zarazem takie same ze względu na zawartość poprzez metodę equals(). Później dojdzie do ciekawej operacji, zmiany płci osoby oraz imienia (Michael dla kobiety nie brzmi zbyt dobrze). W tym celu musimy skopiować obiekt i zmienić wartości dwóch pól, a więc potrzebujemy stworzyć w tym przypadku nowy obiekt. Na koniec wykonamy te same operacje co poprzednio (porównania, hashCode’y itd).

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
67
68
69
70
71
72
73
74
75
76
77
78
79
import java.util.Objects;

final class Person {
private final String firstName;
private final String lastName;
private final int age;
private final boolean female;

public Person(String firstName, String lastName, int age, boolean female) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.female = female;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}

public int getAge() {
return age;
}

public boolean isFemale() {
return female;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
female == person.female &&
Objects.equals(firstName, person.firstName) &&
Objects.equals(lastName, person.lastName);
}

@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age, female);
}

@Override
public String toString() {
return "Person(" +
"firstName=" + firstName +
", lastName=" + lastName +
", age=" + age +
", female=" + female +
')';
}
}



public class SimpleCoding {
public static void main(String[] args) {
Person person = new Person("Michael", "Jackson", 33, false);
Person personDouble = new Person("Michael", "Jackson", 33, false);

System.out.println("Person: " + person);
System.out.println("Person double: " + personDouble);
System.out.println("Comparison by equals: " + person.equals(personDouble)); // true
System.out.println("Comparison by reference: " + (person == personDouble)); // false
System.out.println("Hash codes: " + person.hashCode() + " vs " + personDouble.hashCode()); // true

Person femalePerson = new Person("Anna", person.getLastName(), person.getAge(), true);
System.out.println("Female person: " + femalePerson);

System.out.println("Comparison by equals: " + femalePerson.equals(personDouble)); // false
System.out.println("Comparison by reference: " + (femalePerson == personDouble)); // false
System.out.println("Hash codes: " + femalePerson.hashCode() + " vs " + personDouble.hashCode()); // false
}
}


Bardzo czytelny kod dla wszystkich Javowca. Niby wszystko ok, ale jednak kod klasy nie jest zbyt przyjemny. Jak taką klasę możemy stworzyć w Kotlinie?

Kotlin:

data class Person( val firstName: String, val lastName: String, val age: Int, val female: Boolean )


Tak, to wszystko! Jak to możliwe? Wystarczyło dodać do klasy słówko najważniejsze data. Dzięki temu uzyskujemy m. in. wygenerowane automatycznie metody equals, hashCode i toString, a także metodę copy(), której użycie zaraz zademonstruję. W tym celu jednak musimy zapewnić w konstruktorze przynajmniej jeden parametr, który jest jednocześnie polem w klasie. Mozemy to zrobić np poprzedzając parametr konstruktora słówkiem val lub var, oba są nam już dobrze znane. Dobrze, mamy klasę, możemy stworzyć teraz program w Kotlinie odpowiadający temu powyżej.

Kotlin:

data class Person( val firstName: String, val lastName: String, val age: Int, val female: Boolean ) fun main(args: Array<String>) { val person = Person("Michael", "Jackson", 33, false) val personDouble = Person("Michael", "Jackson", 33, false) println("Person: $person") println("Person double: $personDouble") println("Comparision by equals: ${person == personDouble}") // true println("Comparision by reference: ${person === personDouble}") // false println("Hash codes: ${person.hashCode()} vs ${personDouble.hashCode()}") // true val femalePerson = person.copy(female = true, firstName = "Anna") // WOW println("Female person: $femalePerson") println("Comparison by equals: ${femalePerson == personDouble}") // false println("Comparison by reference: ${femalePerson === personDouble}") // false println("Hash codes: ${femalePerson.hashCode()} vs ${personDouble.hashCode()}") // false }


W samym kodzie dużych różnic nie ma. Olbrzymia redukcja ilości kodu bierze się z samej implementacji klasy. Dwie rzeczy są ważne natomiast. W Kotlinie podwójny znak równości (==) wywołuje pod spodem metodę equals! Potrójny znak równości (===) zachowuje się jak podwójny znak równości w Javie (==), czyli porównuje przez referencje, stąd różnica w kodzie.

Druga rzecz, jeszcze bardziej “wow” (przynajmniej w kodzie) to linijka oznaczona komentarzem “wow”. W tym miejscu tworzymy kobietę dzięki metody copy(), która została wygenerowana przez Kotlina. Wywołanie tej metody bez argumentów zwróci nam nowy obiekt o takich samych wartościach pól. o ile chcemy utworzyć nowy obiekt, który różni się tylko wartością jednego pola to wystarczy jako argumenty podać “nazwaPola = wartość”. Może być więcej zmienionych wartości, wystarczy podać je w takiej samej konwencji oddzielone przecinkami. W tym przypadku tworzymy kopię, która dla flagi female ma inną wartość, a także firstName się zmienia. Kolejna bardzo czytelna funkcjonalność.

Podsumowanie

Na dzisiaj to tyle. Nie powiedziałem jednak ostatniego słowa. Spodziewaj się w najbliższych dniach kolejnego posta, w którym pokażę Ci niektóre transformacje Java -> Kotlin i spróbuję Ci pokazać po raz kolejny dlaczego ten język jest taki niesamowity. Do przeczytania!

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

Idź do oryginalnego materiału