I den förra artikeln visade jag ett antal exempel där Kotlin antingen förkortar eller förfinar vanliga uppgifter i Java samt ett par av skillnaderna mellan språken. I denna artikel dyker vi ned i den djupa delen av bassängen och tittar på ett par områden där Kotlin utmärker sig jämfört med sin föregångare.
Null-hantering
Det första området värt att uppmärksamma är Kotlins hantering av variabler som kan innehålla null. Här finns en rad verktyg vi kan använda för att skydda oss mot den i Java alltför vanliga NullPointerException.
Deklarerar man en variabel med nyckelorden var eller val så ger Kotlin oss ett kompileringsfel om vi försöker tilldela den ett värde som är null:
val myString: String = null //Error: Null can not be a value of a non-null type String
Om vi av något skäl måste tillåta null-värden i variabeln myString så måste vi explicit deklarera att datatypen tillåter null genom att använda nullable type-operatorn (representerad av frågetecknet efter String):
val myString: String? = null
Om vi däremot försöker att anropa en metod på myString variabeln direkt efter deklaration så stöter vi på ett annat kompileringsfel:
val myString: String? = nullprintln(myString.length) //Error: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
Felmeddelandet ger oss en handledning om hur vi kan lösa problemet genom att använda safe call-operatorn, som i exemplet nedan:
val myString: String? = null val length = myString?.length println("Length of myString: $length")
På den rad 2 använder vi safe call-operatorn (?.) för att läsa fältet length på strängen myString (men vi hade även kunnat anropa metoder i String-klassen.) och kan på så sätt undvika kompileringsfel. Safe call-operatorn skyddar oss mot NullPointerException orsakat av att anropa metoder eller läsa fält på null-referenser genom att i sin tur returnera null. Om man har flera kedjade anrop där en eller flera länkar i kedjan kan vara null så skulle man i Java kunnat skriva något i denna stil:
if (person != null && person.getPersonId() != null && person.getPersonId().getId() != null && person.getPersonId().getId().getIntValue() != null) { … } else { return null; }
I Kotlin kan vi undvika denna genom att kedja anrop med safe call-operatorn, som kommer returnera null vid första null-referensen. Vi kan då skriva om ovanstående kod till detta:
return person.?getPersonId().?getId().?getIntvalue()
Men i det fallet då man verkligen vill undvika null? I Java hade det kunnat se ut såhär:
String myString = null; int length = myString != null ? myString.length() : 0; System.out.println("Length of myString: " + length);
Vi använder den ternära operatorn och tittar på returvärdet innan vi gör en tilldelning och i det fallet då det är null returnerar vi ett default-värde. I Kotlin så har detta förenklats i form av den så kallade Elvis-operatorn:
var myString: String? = null val length = myString?.length ?: 0 println("Length of myString: $length")
På detta sätt kan vi försäkra oss om att variabeln length aldrig kommer att få ett null-värde, inte ens om myString är null. Vi kan till och med använda oss av Elvis-operatorn för att kasta ett Exception istället för att returnera ett värde:
val length = myString?.length ?: throw IllegalStateException("myString is empty!")
Förstaklass- och lambda-funktioner
Som jag visade i den förra artikeln har Kotlin stöd för funktioner som är oberoende av klasser, lambda-funktioner och att skicka funktioner som argument till andra funktioner. Men inte nog med det: Kotlin stöder även single expression-funktioner och lambda-funktioner med muterbara värden. Nedan har vi ett exempel på en vanlig funktion:
fun double(x: Int): Int { return x * 2 }
Funktionen tar ett heltal som input, dubblerar värdet och returnerar resultatet. Denna funktion kan skrivas om som en single expression-funktion:
fun double(x: Int): Int = x * 2
En annan typ av funktion med speciella egenskaper i Kotlin är dess lambda-funktioner. Dessa har funnits i Java sedan version 8, men med begränsningen att variabler som deklarerats utanför lambda-funktionen måste vara final för att kunna användas inuti lambda-funktionen. I Kotlin finns inte denna begränsning och följande kod är därför giltig:
var sum = 0 listOf(-1,0,1,2,3,4,5).filter { it > 0 }.forEach { sum += it } print(sum)
Destructuring
En annan användbar egenskap hos Kotlin som inte finns i Java men återfinns i dynamiska språk som Javascript och Python är destructuring. Nedan följer ett exempel på hur destructuring kan användas för listor:
val myList = listOf(1,2,3,4,5) val (first, second) = myList println(first) println(second)
I detta exempel skriver de två println-satserna ut 1 och 2. Destructuring kan även användas för maps:
val myMap = mapOf("alfa" to 123, "beta" to 456, "gamma" to 789) for ((key, value) in myMap) { print(“$key: $value”) }
Utöver listor och maps så kan destructuring användas för att dela upp fält i objekt och tilldela dem till variabler:
data class Result(val result: Int, val status: String) fun function(): Result { return Result(200, “OK”) } val (result, status) = function() println("$result $status")
I exemplet ovan returnerar funktionen ett objekt av klassen Result som vi sedan delar upp i de två variablerna result och status. Utskriften blir då: “200 OK”.
Intelligent type casting
En egenskap hos Kotlin som inte ofta lyfts fram är dess smarta sätt att förstå vilken datatyp en variabel har och komma ihåg det inuti exempelvis if-satser. Nedan ett exempel på type casting i Java:
if (maybeNumber instanceof Integer) { maybeNumber = ((Integer) maybeNumber).plus(1); }
I följande exempel kan vi använda Kotlins smarta type casting för att korta ner koden:
if (maybeNumber is Number) { maybeNumber = maybeNumber.plus(2) }
I detta exempel gör vi först en kontroll för att se om variabeln maybeNumber faktiskt är av datatypen Number och om det villkoret uppfylls så betraktar Kotlin variabeln som ett nummer och vi får i IDE:n förslag på de fält och metoder som finns tillgängliga för klassen Number.
Som exemplen ovan visar är Kotlin en mycket modern Java-dialekt som inte drar sig från att verka främmande för de som är vana vid Java. Paralleller kan dras mellan Java och Kotlin å ena sidan samt C och Go å andra sidan. Go lyftes fram av Google som en modern tolkning av C med en Python-inspirerad syntax, inbyggt stöd för resurssnål parallellism och rejält standardbibliotek, men efter att ha stötts och blötts bland både praktiskt- och teoretiskt lagda utvecklare så började ett återkommande mönster skönjas i kritiken: Go hade många bra och praktiska idéer, men språkets hela design kändes väldigt föråldrad.
Efter att ha experimenterat med Kotlin så känner jag att Jetbrains här har lyckats undvika den fallgropen. Språket känns modernt på ett sätt som Java inte alltid känns och har gjort många modiga val som trots att de kanske gör det främmande från Javautvecklare samtidigt gör att det kan dra nytta av de decennier av programmeringsspråksteori som Google till synes förbisåg när de skapade Go.
Vid den här punkten måste jag lägga alla korten på bordet: finns det saker jag saknar i Kotlin? Finns det områden där jag tycker att man inte gick tillräckligt långt i moderniseringen?
Svaret på båda frågorna är: Ja!
När det gäller saker som jag saknar så är pattern matching den främsta kandidaten. Denna feature finns såväl i Scala som Vavr-biblioteket (tidigare kallat Java Slang) för Java och är ett utmärkt verktyg för att förenkla de stora if-else if-else-satser som ofta förekommer i Java. Med pattern matching hade vi istället kunnat skriva om dem som funktionella uttryck med värden och predikat. Exempel på pattern-matching finns i Vavr-dokumentationen.
När det gäller områden där jag tycker att Kotlin inte gått tillräckligt långt i sin modernisering av Java finns bland annat immutability. Jag hade önskat mig ett const eller constant nyckelord som gör att inte bara referenser till en variabel utan även dess innehåll blir skyddat mot ändringar. Exempelvis kan Javas och Kotlins final-nyckelord ge oss en lista som inte kan ersättas med en annan lista, men vi kan fortfarande ändra på listans innehåll. En sådan form av immutability skulle visserligen orsaka en större overhead då bland annat fler minnesallokeringar skulle krävas, men vi som utvecklar skulle få det lättare att resonera om hur ett program fungerar.
Sammanfattningsvis är jag trots detta oerhört imponerad av vad Kotlin erbjuder. Utvecklingsteamet har lagat eller kompenserat för många av de dåliga delarna av Java samtidigt som man lyckats behålla direkt kompatibilitet med Java. För Android är Kotlin ett enormt steg framåt, men även för utvecklare som använder Java för backendutveckling så erbjuder Kotlin ett språk som är lättare att lära sig än andra funktionella JVM-språk samtidigt som det är mer kortfattat och typsäkert än vanlig Java.