Geschreven door Bart van Dijk

De dagelijkse voordelen van Kotlin

Development11 minuten leestijd

Sinds ongeveer een jaar werk ik met Kotlin, een taal die ik persoonlijk zie als de spirituele opvolger van Java. Het wordt ontwikkeld door JetBrains, de ontwikkelaars van de populaire editor IntelliJ, en heeft een hoop voordelen ten opzichte van Java. Deze voordelen zijn ook niet dusdanig obscuur dat ze niet opvallen - ik gebruik ze bijna dagelijks. In deze blogpost wil ik een blik werpen op welke onderdelen van Kotlin mij helpen, en in sommige gevallen hoe ze zich verhouden tot Java.

Java interoperability

Het belangrijkste voordeel wat Kotlin tegenover Java heeft is misschien een valse start, maar het is de reden dat Kotlin zo groot aan het worden is denk ik, en dat is de Java interoperability. Dit houdt in dat Kotlin vlekkeloos samen kan werken met Java in dezelfde codebase. Nog beter, wij kunnen Java-code aanroepen vanuit Kotlin en omgekeerd Kotlin code aanroepen vanuit Java. De compiler heeft hier geen problemen mee en zal zijn werk doen zoals altijd.

Het grote voordeel hieraan is dat Kotlin overal geadopteert kan worden waar Java wordt geschreven. Als een team niet comfortabel is met Kotlin kunnen zij kiezen om kleine stukken van de code te pakken en die om te schrijven naar Kotlin, om zo gaandeweg meer ervaring op te doen met Kotlin en comfortabeler te worden met de taal.

Voortbouwend hierop is dat de ontwikkelaars bij JetBrains er alles aan hebben gedaan om de conversie van Java naar Kotlin zo makkelijk mogelijk te maken. In hun IDE, IntelliJ, hebben zij de functionaliteit ingebouwd om Java-code in een Kotlin bestand te plakken (via copy-paste) om een popup te tonen die vraagt “Wil je deze code omzetten naar Kotlin?”. Als er akkoord wordt gegeven zal IntelliJ zelf de Java-code converteren naar Kotlin code dat in 99% van de gevallen klaar is voor productie. Dit kan een software engineer helpen om bekende Java-code automatisch te converteren en het resultaat te bestuderen om Kotlin beter te leren begrijpen. Er wordt immers vanuit een bekende basis gewerkt in dat geval.

Data classes

Nu voor de echte start, en mijn persoonlijke favoriet: data classes. In Java bestaan de zogeheten POJO’s, of Plain Old Java Objects. Het zijn simpele objecten met een aantal velden, en methodes om de waardes van die velden op te halen of te veranderen. Een voorbeeld hiervan voor een boek klasse is als volgt:

public class Book {
     private String title;
     private String author;
     private int amountOfPages;
 
     public Book(
         String title,
         String author,
         int amountOfPages
     ) {
         this.title = title;
         this.author = author;
         this.amountOfPages = amountOfPages;
     }
 
     public String getTitle() {
         return this.title;
     }
 
     public String getAuthor() {
         return this.author;
     }
 
     public int getAmountOfPages() {
         return this.amountOfPages;
     }
 
     public void setTitle(String newTitle) {
         this.title = newTitle;
    }
 
     public void setAuthor(String newAuthor) {
         this.author = newAuthor;
     }
 
     public void setAmountOfPages(int newAmountOfPages) {
         this.amountOfPages = newAmountOfPages;
     }
 }

De bovenstaande klasse bestaat uit 38 regels code en is erg simpel. Er zijn drie velden, title, author, en amountOfPages, een enkele constructor, en de getters en setters. Kotlin kan dezelfde code genereren door een data class aan te maken. In het geval van een Book klasse gaat dit als volgt:

data class Book(
     val title: String,
     val author: String,
     val amountOfPages: Int
 )

Dat is alles! Vijf regels code en hetzelfde resultaat is bereikt als de 38 regels van Java code. De Kotlin code beschrijft een constructor tussen de gewone haakjes, en daarbinnen de drie datavelden die wij willen. De compiler zal begrijpen wat nodig is voor de data class en daarmee alle getters en setters genereren.

Het bestaan van de data class maakt het zoveel eenvoudiger om een POJO (of eigenlijk POKO in dit geval) te maken dat het erg aantrekkelijk is om “even gauw” een data class ergens voor aan te maken omdat dit simpelweg eenvoudiger is dan ingewikkeldere constructen zoals een tuple of pair.

Sealed classes

De data class is niet de enige soort nieuwe klasse. Kotlin kent voor de visibility van classes naast de private, protected, en public types ook de types sealed en internal.

Een sealed class is een klasse of een interface waarbij alle subklassen en implementaties bekend moeten zijn tijdens het compileren van de code. Dit zorgt ervoor dat een derde partij niet nog een extra subklasse later toe kan voegen als er bijvoorbeeld een library geschreven is (de klasse is “sealed” voor verdere subklassen). Sealed classes hebben hun weg wel gevonden naar Java als een preview sinds Java 15, en als volwaardig onderdeel sinds Java 17. Echter, ze zijn langer aanwezig in Kotlin en hebben betere support. In Kotlin is het bijvoorbeeld mogelijk om een sealed class in een switch statement te gebruiken - Java staat dit als preview functie toe in Java 17, maar er is nog geen officiële support. Ook is Java stricter in het gebruik van sealed classes. De sealed class zelf moet expliciet aangeven welke subklassen toestemming hebben om een extension te maken via het keyword permits.

In mijn optiek zorgt dit voor meer verbose code en ligt mijn voorkeur bij de aanpak van Kotlin. Daarbij mag elke klasse een subklasse zijn, maar de subklassen moeten in dezelfde file als de sealed class staan. Een voorbeeld daarvan staat hieronder. Het werken met sealed classes vind ik zelf erg fijn in Kotlin doordat de code compact is, en ik weet dat ik klassen kan schrijven die alleen de subklassen heeft die ik wil, wanneer dan ook. Dit kan recente versies van Java inmiddels ook.

sealed interface Error
 
sealed class IOError(): Error
class CustomError(): Error

Companion objects

Een companion object is een manier in Kotlin om statische variabelen te maken die onderdeel zijn van een klasse, maar gebruikt kunnen worden zonder de klasse te instantiëren. Doorgaans gebruik ik een companion object zelf om statische variabelen aan een klasse toe te wijzen zoals normaal in Java gedaan zou worden. Dit gaat heel eenvoudig op de volgende manier:

class Coffee {
     companion object {
         const val optimalTemperatureInCelsius: Int = 140
     }
 }

Belangrijk hierbij is dat er een annotatie extra nodig is voor goede Java interoperability. Door @JvmStatic boven de companion object variabele te plaatsen kan dit bereikt worden. Dit is nodig omdat een companion object in Kotlin kan overerven van klassen of interfaces, maar in Java is dit niet mogelijk.

Er zijn nog andere manieren waarop nuttig gebruik gemaakt kan worden van een companion object. Een voorbeeld hiervan is het Factory design pattern. Door gebruik te maken van een named companion object kan een Factory heel eenvoudig opgezet worden:

class Coffee {
     companion object Brewer {
         fun brew(): Coffee = Coffee()
     }
 }

De factory is Brewer genoemd in dit voorbeeld, en de gebruikelijke createInstance is nu brew genoemd om in het thema van de klasse te blijven. Vervolgens kan de factory gebruikt worden op de volgende manier:

val blackCoffee = Coffee.Brewer.brew()

Null safety

Ik vind persoonlijk null safety erg belangrijk in code, en ik ben erg blij met het type system in Kotlin. Het type system probeert namelijk om null referenties te voorkomen. Dit wordt gedaan door variabelen niet standaard een null mogelijkheid te geven, maar dit expliciet af te laten dwingen. Bijvoorbeeld, bij het aanmaken van een String moet de String een waarde toegewezen krijgen, anders compileert de code niet. Door gebruik te maken van een ? bij de variabele vertel je de compiler dat null toegewezen mag worden aan de variabele.

val someString: String = "Hello There" // mag geen null zijn
val anotherString: String? = null      // mag null zijn

Helaas zie ik in mijn dagelijks programmeerwerk een hoop mensen die denk ik uit gemak toch vaak met null willen werken. Ik zie dit als een soort “Java-isme” wat over is gekomen naar Kotlin. Hierdoor is de code al vrij snel bezaaid met ?s en de bijbehorende if-statements die op null controleren. Gelukkig is Kotlin hier op voorbereid en is er een shorthand voor de null checks in de vorm van, alweer, een vraagteken ?. Dit werkt als volgt:

anotherString?.let { println(it) }

Kotlin controleert hier of de waarde van anotherString null is of niet. Als er null geconstateerd wordt dan gebeurt er verder niks - de check heeft gefaald. Als er geen null wordt geconstateerd dan heeft anotherString dus een waarde gekregen. In dat geval voert Kotlin het let deel uit en wordt de waarde van anotherString geprint.

Een andere manier van nullable variabelen gebruiken is minder veilig dan de ?, maar kan soms zijn nut hebben. Het kan namelijk gebeuren dat Kotlin een variabele als een mogelijke null ziet, maar als software engineer is duidelijk in de code te zien dat dit nooit kan gebeuren. In zulke gevallen kan gebruik gemaakt worden van een dubbele bang (!!). Dit dwingt af dat de waarde van de variabele gebruikt wordt - ook als dit alsnog null zou zijn. Het gebruik van !! moet dan ook, vind ik, minimaal blijven en het liefste voorkomen worden. Helaas heeft het vele gebruik van nullable variabelen met ? als gevolg dat de code ook al snel bezaaid is met !!s, waardoor de null safety van Kotlin in mijn ogen onderuit wordt gehaald. Dit neemt alleen niet weg dat de null safety van Kotlin een welkome functionaliteit is die goed werkt.

Named arguments

Named arguments neem ik tegenwoordig dusdanig voor lief dat ik verbaast was toen ik (her)ontdekte dat Java deze feature niet ondersteunt. Het is een kleine toevoeging in de code, maar het maakt (in ieder geval voor mij) echt een wereld van verschil. Bij het gebruiken van een functie met argumenten kunnen de argumenten namelijk een toegewezen naam krijgen, zoals gedefinieerd in de functie zelf, maar nog belangrijker is dat ze ook een eigen plek toegewezen kunnen krijgen.

Normaal gezien zou een functie in Java gedefinieerd en gebruikt worden als volgt:

public static Coffee create(String name, int temperature, BeanType beanType) { ... }
 
Coffee.create("Cappucino", 95, BeanType.DARK_ROAST);

In bovenstaand voorbeeld is impliciet duidelijk waar de argumenten voor staan, maar om zeker te zijn moet toch naar de functie definitie gekeken worden. Dit kan nog omzeild worden door de argumenten eerst te definiëren:

String name = "Cappucino";
int temperatureInCelsius = 95;
BeanType beanType = BeanType.DARK_ROAST;
 
Coffee.create(name, temperatureInCelsius, beanType);

Echter, dit zorgt voor meer regels code en is een manier van werken die ik tegenwoordig niet mooi meer vind. De zojuist gegeven voorbeelden kunnen met behulp van named arguments in Kotlin geschreven worden op een mooiere manier.

class Coffee(name: String, temperature: Int, beanType: BeanType)
 
Coffee(name = "Cappucino", temperature = 95, beanType = BeanType.DARK_ROAST)

Nu is direct bij het aanmaken van een Coffee object te zien waar de argumenten voor staan. In dit geval staan de argumenten nog in dezelfde volgorde, maar stel dat we de argumenten in een andere volgorde willen, bijvoorbeeld om extra focus te leggen op het type boon. Dit kan eenvoudig opgelost worden door beanType naar voren te plaatsen. Of stel dat name een standaard waarde heeft die wij willen gebruiken. In dat geval slaan wij het hele argument simpelweg over.

// Andere volgorde
Coffee(beanType = BeanType.DARK_ROAST, temperature = 95, name = "Cappucino")
 
// Argument overslaan. Eerst wordt een klasse definieerd met een standaard waarde voor 'name'.
class Coffee(name: String = "Black", temperature: Int, beanType: BeanType)
 
Coffee(beanType = BeanType.DARK_ROAST, temperature = 95)

String templates

Als laatste functionaliteit wil ik de string templates van Kotlin in het licht zetten. Strings zijn in Java lastig om mee te werken, of in ieder geval om te manipuleren. In Kotlin kunnen argumenten direct in de String zelf worden geplaatst. Deze functionaliteit is ontzettend eenvoudig maar voegt zoveel plezier in het programmeren toe, simpelweg doordat een complexe String opbouwen zo makkelijk gaat.

Als voorbeeld, stel dat wij een rekensom uitgeprint willen hebben als String. Dit is in Kotlin direct uit te voeren:

val firstNumber = 25
val secondNumber = 5
 
println("First number is $firstNumber. Second number is $secondNumber.\n$firstNumber / $secondNumber = ${firstNumber / secondNumber}")

De argumenten kunnen direct in de String gebruikt worden door er een $ voor te plaatsen. Bij complexe onderdelen van de String kunnen simpele berekeningen gedaan worden door de berekening te plaatsen binnen haakjes {}. [String templates zijn in versie 19 ook aan Java toegevoegd - red.]

Conclusie

Ik hoop dat deze blogpost meer was dan een simpele vergelijking tussen Java en Kotlin. Kotlin is een programmeertaal die dagelijks het programmeren makkelijker en leuker maakt voor mij dankzij het gemak van de features die het heeft. Het voelt op zijn minst als een goede toevoeging aan het JVM-ecosysteem, en uiteindelijk als een oprechte opvolger voor Java. Als je Kotlin nog nooit hebt gebruikt, geef het een keer een kans!