Tijdens mijn zwerftocht langs de verschillende sessies op de JavaOne van 2015 in San Francisco, kwam ik eigenlijk toevallig in een sessie terecht genaamd ‘Kotlin in Anger’. Deze sessie werd gegeven door Justin Lee. Hij werkt bij MongoDB en is onder andere verantwoordelijk voor de Java driver van mongo. In zijn sessie liet hij zien hoe hij de Java driver in zijn geheel in Kotlin had geschreven. Er zijn altijd wel een aantal sessies die bij mij blijven hangen na een conferentie en mij enthousiast maken en dit was er zeer zeker één van. Ik wil mijn enthousiasme graag met jullie delen en in dit artikel ga ik in op een aantal belangrijke punten van Kotlin.
Kotlin is een nieuwe programmeertaal voor het Java platform. Zo nieuw zelfs dat er nog geen officiële release van is. Op het moment van schrijven, is net versie 1.0.0-beta2 beschikbaar. Hopelijk zal tegen de tijd dat jullie dit lezen de 1.0 uitgekomen zijn.
“Pff, alweer een nieuwe taal” zal je misschien denken. Maar als Scala al langere tijd op je bucket list staat en je er nog niet de tijd voor hebt gevonden (of de stap te groot is), dan is dit een mooi alternatief. Ik zeg niet, net als de makers van Kotlin, dat één van de twee beter of beperkter is. Scala is ontstaan vanuit een academisch oogpunt en Kotlin meer vanuit de praktijk. Kotlin is ontwikkeld door Jetbrains, het bedrijf dat we kennen van onder andere IntelliJ en Android Studio. De ondersteuning in deze programma’s is dan ook zeer goed, maar ook voor Eclipse is een plug-in beschikbaar. Het is ook mogelijk om, net als bij Java, een commandline compiler te gebruiken.
Maar goed, wat is Kotlin nou precies en welk gat in het programmeerlandschap wil het vullen? Het is dus een programmeertaal voor de jvm. Kotlin is statically typed, wat wil zeggen dat alle variabelen typen tijdens compileren bekend moeten zijn. De kotlin compiler compileert, net als Java en onder andere Scala en Groovy, naar bytecode. Een groot verschil is echter dat Kotlin al genoegen neemt met een Java 6 jvm. Dit heeft als groot voordeel dat Kotlin programma’s ook prima op een Android device zullen draaien. (In principe wordt op dit moment alleen Java 6 volledig ondersteund op Android). Nu zou je misschien denken dat je allerlei mooie mogelijkheden van Java 7 en 8 mist, maar niets is minder waar. Kotlin bied je ook Generics, Lambda’s en nog veel meer moois, waaronder een aantal concepten die je niet terugvindt in andere jvm talen of die op een veel simpelere manier te realiseren zijn. Daarnaast hebben de makers van Kotlin zich een aantal doelen gesteld:
- Concise (beknopt) – Met minder code tot het zelfde resultaat komen.
- Expressive (expressief) – Duidelijke en gemakkelijk leesbare code.
- Safe (veilig) – Problemen aangeven voordat ze kunnen optreden (o.a. NULL safe).
- Versatile (veelzijdig) – Kotlin compileert naar bytecode, maar naar Javascript kan ook.
- Interoperable (uitwisseling) – Kotlin code kan Java code bevatten en andersom.
- Fast (snel) – Kotlin code moet minstens net zo snel gecompileerd en uitgevoerd worden als Java.
Eerste kennismaking
Voordat je in Java een simpele “Hello world!” applicatie hebt geschreven, ben je al snel 5 regels verder.
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
In kotlin ziet dat er zo uit:
fun main(args: Array<String>): Unit {
println("Hello, World!")
}
Nog niet schokkend minder code, maar toch al een aantal duidelijke verschillen. Er is geen class waar binnen je een static main functie moeten maken. We zouden hier ook nog het return type, Unit, weg kunnen laten. Unit is equivalent aan void in Java. Unit is optioneel, net als een puntkomma na ieder statement. Bij meerdere statements op één lijn is een puntkomma wel verplicht. Kotlin is erg goed in het automatisch bepalen wat het type moet zijn, waardoor wij wederom minder code hoeven te schrijven. Zo kan al aan de hand van de variabele definitie bepaald worden welke type wordt gebruikt.
val s: String = “ABC” // val is een inmutable variabele
var s = “ABC” // var is een mutable variabele
var char = ‘A' // Char object. Numerieke waarde krijg je door: Char.toInt()
var int = 1 // Default naar Int object - 32 bit, maar kan ook een Byte of Short zijn
var long = 0L // Long object - 64 bit
var float = 0F // Float object - 32 bit
var double = 0.0 // Double object - 64 bit
var boolean = true // Boolean object
var foo = MyFooType() // Nieuw MyFooType object. Merk op dat het keyword ‘new’ niet nodig is.
Het bestaat in Kotlin zelfs niet.
Minder boilerplate
Een beter voorbeeld van minder code is het maken van een simpele POJO. Wanneer je in Java een pojo maakt, dan voorzie je die van getters en setters voor iedere property en zal je waarschijnlijk een equals, hashcode en een toString method maken. Meeste IDE’s kunnen dit automatisch voor je genereren, maar dit levert toch veel ruis op in de code en het kost weer extra tijd bij het aanbrengen van wijzigingen. In Kotlin kan je volstaan met de volgende code:
data class Person(val id:Int, var name: String)
That’s it. De magie zit in het keyword data. Dit verteld Kotlin dat we graag alle boilerplate code willen hebben. Deze kan je overigens wel allemaal overriden mocht dit nodig zijn.
Een ander bijkomend voordeel in Kotlin is dat je classes niet ieder in een eigen bestand hoeft op te slaan. Je kan bijvoorbeeld één domain.kt bestand aanmaken en daar al je pojo’s en andere domeinobjecten in opnemen. Of al je service-objecten in één bestand stoppen. Dit reduceert je code base aanzienlijk, wat navigeren door je code prettig maakt.
NULL safety
Ik verwacht niet dat je nu al direct zou willen omschakelen naar Kotlin, maar wacht; er is meer. Veel meer zelfs dan ik kan behandelen in dit artikel. Wat dacht je van NULL Safety? Een groot probleem in Java is dat variabelen de waarde null kunnen krijgen en je niet met zekerheid kunt zeggen wanneer dat gebeurt. Tenzij je heel defensief programmeert, ga je geheid een keer een NullPointerException te zien krijgen, waarna je programma waarschijnlijk niet meer zal doen wat je verwacht. Dit probleem ondervangt Kotlin door het standaard onmogelijk te maken om variabelen null toe te kennen. Het volgende zal direct een compiler error geven:
var str:String = null
Wanneer we een ‘?’ achter het type plaatsen, vertellen we Kotlin dat een null waarde wel toegestaan is. Dit vanwege de mogelijkheid dat we gebruik willen kunnen maken van bestaande Java libraries waarvan we niet met zekerheid kunnen zeggen dat die null safe zijn.
var str: String? = null
println(str.length)
Door het vraagteken kunnen we nu null toekennen aan de variable str. Wanneer we nu de lengte (zoals je ziet een property van String type) zouden willen afdrukken, zal de compiler een error geven. We kunnen dit weer ondervangen door een ‘?’ achter str in de println functie te plaatsen.
var str: String? = null
println(str?.length)
De compiler zal geen error geven en null printen als str null is toegekend of de lengte als str niet null is. Erg veilig programmeren dus. Een ander vaak gebruikte oplossing om een null pointer te omzeilen in Java is het volgende voorbeeld
int length = str != null ? str.length : -1;
In Kotlin bestaat de Elvis operator. Deze kunnen we als volgt gebruiken:
var length = str?.length ?: -1
De Elvis operator, in dit statement ‘?:’, kijkt of de linker zijde niet null oplevert en zal dan deze waarde retourneren. Anders wordt de waarde aan de rechterkant gebruikt.
Smart cast
Al enthousiaster? Nog een mooie feature dan: Smart Casts. Dit zorgt ook weer voor een hoop minder boilerplate code, voordat je iets kan doen met onbekende variabelen. Smart casts in Kotlin zorgen ervoor dat je na een typecheck geen cast meer hoeft uit te voeren om te werken met dat bepaalde type.
val y: Object = doSomething()
if (y is String) {
println(y.length)
}
// of anders
if (y !is String) {
return
}
// vanaf hier is x een String object
print(y.size())
Of we kunnen een when statement gebruiken. Dit is vergelijkbaar met een switch statement in Java:
when(y) {
is Int -> {
// eventueel nog meer statements
print(y + 1)
}
is String -> print(y.length + 1)
is Array<Int> -> return y.sum()
}
Zoals je ziet, geen lelijke instanceof statements en overbodige casts.
Extension functions
Extension functions is nog zo’n ontzettend handige feature in Kotlin. Extension functions kunnen bestaande objecten van extra functies voorzien. Weg met al die StringUtility classes en navenanten. Het volgende voorbeeld spreekt voor zich. De volgende functie retourneert true als de string enkel lowercase karakters heeft en anders false. (In Kotlin is == equivalent met equals in Java. Objectgelijkheid check je met ===)
fun allLowerCase(str:String) : Boolean {
return str == str.toLowerCase()
}
Hoe handig zou het zijn als we dit direct aan het String type zouden kunnen vragen? Met een extension function kan dit.
fun String.allLowerCase() : Boolean {
return this == this.toLowerCase()
}
We kunnen dit op elke type object toepassen. Ook op final classes, want er wordt geen code toegevoegd aan het object zelf.
Objecten
Bij het stukje over pojo’s heb je al een beetje kunnen zien hoe objecten in Kotlin gemaakt worden. Hier gaan we er wat dieper op in:
class Adres constructor(val straat:String, val nummer:Int, val postcode: String, val plaats:String)
Dit is een simpel adresobject met een constructor. We kunnen het keyword constructor weglaten, maar het is wel nodig als we eventuele annotaties willen toevoegen. Tussen Adres en constructor zou dus bijvoorbeeld @Inject kunnen komen. Wanneer we een nieuw adres object willen maken, dan doen we dat als volgt:
var adres = Adres("Straat", 1, "1234AB", "Plaats");
Simpel, maar je hebt misschien niet altijd alle parameters voorhanden. Hiervoor heeft Kotlin default parameters geïntroduceerd. Ons adres object definitie kunnen we aanpassen tot:
class Adres(val straat:String = "", val nummer:Int = 0, val postcode: String = "", val plaats:String = "")
Dat is al wat beter, maar we moeten nog steeds alle parameters opgeven als we bijvoorbeeld alles weten, behalve het huisnummer en de postcode. De oplossing hiervoor zijn named parameters.
var adres = Adres("Straat", plaats = "Plaats");
In Java zal je dit probleem omzeilen door verschillende constructors te definiëren, wat weer ruis veroorzaakt in je code. In sommige gevallen wil je toch graag meerdere constructors definiëren. Dat kan in Kotlin ook. Hieronder een voorbeeld:
class Node(var name:String) {
val children = ArrayList<Node>()
constructor(name:String, parent:Node) : this(name) {
parent.children.add(this)
}
}
We zijn in Kotlin wel verplicht om de primaire constructor aan te roepen. In dit geval is dat de call this(name) achter de 2e constructor. Ik vermoed dat dit komt, omdat in Kotlin in eerste instantie maar sprake zou zijn van enkel primaire constructors.
Inheritance
In Kotlin kunnen we ook overerving toepassen. We zullen dan wel aan de parent class definitie het keyword open moeten toevoegen. Standaard zijn alle classes final, net als alle functies binnen een class. Dit is beter dan de standaard in Java, waarin alles uit te breiden is wanneer final niet aanwezig is. Hieronder een voorbeeld van overerving met een overloaded functie:
open class Basis() {
open fun print() {
println("In basis")
}
}
open class Afgeleid : Basis() {
final override fun print() {
println("In afgeleid")
}
}
Basis is open voor uitbreiding, net als zijn enige methode. De afgeleide class is uit te breiden, er staat immers open in de klasse definitie. Echter de afgeleide methode print is niet verder uit te breiden door het final keyword. Het is je vast opgevallen dat override voor de afgeleide functie staat.
Van Java naar Kotlin en weer terug
Misschien had het je al gezien, maar bij overerving hadden we al gebruik gemaakt van een Java object. Namelijk een ArrayList<Node>(). Niets bijzonders dus, behalve het ontbrekende new keyword. Properties van Java objecten benader je niet via getters en setters, maar gewoon via de property naam. Bijvoorbeeld, de get lengte van de children list is: children.size.
Kotlin code kan je ook in je Java code gebruiken. Dat is niet veel anders, dan hoe je normaal met objecten omgaat. Stel het Kotlin Adres object van hiervoor leeft in een file App.kt en heeft een package statement ‘demo’, dan kunnen we deze instantiëren dmv new demo.Adres().
Zou er nu ook nog een foo() methode in domain.kt staan, dan kunnen we die in Java benaderen dmv: demo.App.foo(). Je ziet dus alle toplevels functies, functies zonder een omsluitende class, als statische methoden van class App. De class naam zal default de naam van de file krijgen. Dit is te overriden door annotaties. Het is absoluut niet moeilijk om een mix van Java en Kotlin te gebruiken. Hierdoor is een vloeiende overgang van Java naar Kotlin haalbaar.
Tot slot
Ik heb in dit artikel geprobeerd om je een beeld van de potentie van Kotlin te geven. Er is nog veel meer in de taal aanwezig dan dat ik hier heb kunnen behandelen. Een goede bron van informatie is de website van Kotlin zelf. Ik verwacht dat Kotlin meer en meer aan populariteit zal winnen. Ook van de compilatie naar javascript verwacht ik veel. Op dit moment geeft Jetbrains het een lage prioriteit, maar dit zou een goede nieuwe manier kunnen worden om statically typed web development te doen.
Mocht je niet direct over willen schakelen naar Kotlin, dan kan je dit ook gefaseerd doen door de goede interoperabaliteit. Al met al een zeer interessante nieuwe taal die ons ontwikkel leven makkelijker kan maken.
Links: