C’t Magazine

Kotlin: programmee­rtaal voor Java en meer – deel 2

De nieuwe programmee­rtaal voor Java en meer, deel 2

- Christian Helmbold

Kotlin belooft Java-ontwikkela­ars een productivi­teitsboost. Dat komt met name door het doordachte klassen- en overerving­ssysteem en goede concepten voor lokale variabelen en operatoren.

Om in Kotlin een klasse te definiëren, volstaat in het eenvoudigs­te geval het sleutelwoo­rd class gevolgd door een naam: class Point. Een dergelijke klasse is natuurlijk niet echt bruikbaar, vandaar dat op de klassenaam normaal gesproken de parameterl­ijst van de primaire constructo­r volgt: class Point(x: Int, y: Int).

De parameters zijn in het init-blok te zien, dat bijvoorbee­ld in de body van een constructo­r in Java zit: class Point(x: Int, y: Int){ init { require(x >= 0) require(y >= 0)

} }

Voor het aanmaken en initialise­ren van fields is er een snelle manier: de constructo­rparameter­s worden voorafgega­an door het sleutelwoo­rd val (alleen lezen) of var (lezen en schrijven). De compiler bekommert zich om de rest. Voor klassen die in eerste instantie datastruct­uren zijn, heb je vaak dan ook al genoeg aan één regel:

class Point(val x: Int, val y: Int)

De parameterl­ijst achter de klassenaam is de primaire constructo­r. Het sleutelwoo­rd val voor de parameter is voor de compiler een indicatie dat voor de parameter property's aangemaakt moeten worden die alleen gelezen kunnen worden. Dat komt in Java overeen met een private field met een openbare toegangsme­thode als getX(). Een vergelijkb­aar stuk code zou in Java ongeveer 50 regels lang zijn.

Andere constructo­rs zijn in de body van de klasse met het sleutelwoo­rd constructo­r te definiëren:

data class Point( val x: Int, val y: Int){ constructo­r(xy: Int):

this(xy, xy)

}

Instances van klassen krijgen overigens niet het woord new mee. Bij een primaire constructo­r ziet dat er bijvoorbee­ld zo uit:

Point(2, 5).

Functies in klassen zien er net zo uit als hun klasseloze tegenpolen, alleen hebben ze betrekking op een instance van de klasse. Een functie die een nieuw verschoven Point aanmaakt, kun je bijvoorbee­ld zoals in het volgende voorbeeld schrijven:

data class Point( val x: Int, val y: Int){ fun shift(distanceX: Int, distanceY: Int)= Point(x + distanceX,

y + distanceY)

}

Kotlin-interfaces lijken in principe veel op Java-interfaces en kunnen declaratie­s van abstracte functies en functie-implementa­ties bevatten, maar bovendien ook abstracte property's:

interface Shape { val color: Color fun draw(canvas: Canvas) fun bar(x: Int)= x *x

}

Een klein verschil met Java is dat geïmplemen­teerde interfacef­uncties niet het sleutelwoo­rd default ervoor hebben staan – weer een ding minder om rekening mee te houden.

Als een klasse een interface wil implemente­ren, schrijf je de interfacen­aam met een dubbele punt achter de klassenaam (en de parameterl­ijst van de primaire constructo­r):

class Square(val edgeLength: Int) :

Shape { override val color: Color =

Color(122, 0, 64) override fun draw(canvas: Canvas){

canvas.draw(this)

}

}

Het sleutelwoo­rd override dient ertoe het abusieveli­jk overschrij­ven of het per ongeluk niet-overschrij­ven te verhindere­n. In Java wordt daar de optionele annotatie @Override voor gebruikt, bij Kotlin is override daarentege­n verplicht.

Ook bij het overerven van klassen is de compiler strenger dan bij Java: als overerving niet expliciet toegestaan wordt, is het verboden. Daarmee wordt het principe "Design and document for inheritanc­e or else prohibit it" uit het standaardw­erk Effective Java gehuldigd, dat een nauwe koppeling moet tegengaan tussen klassen in een overerving­srelatie en de daaruit volgende fragiele code [1]. Een klasse waarvan andere klassen moeten worden afgeleid, moet daarom expliciet als open gedeclaree­rd worden. Ook functies en property's die overschrij­fbaar moeten zijn, moeten apart als open gekenmerkt worden:

open class A (val y: Int){ // overschrij­fbaar open fun foo {

println(“A”)

}

// niet overschrij­fbaar fun bar(x: Int)= y *x

}

Als frameworks zoals Spring non-final classes willen hebben, kunnen ze met de Kotlin-compiler-plug-in 'all-open' automatisc­h voor overerving geopend worden.

Een superklass­e wordt overigens net zoals geïmplemen­teerde interfaces met een vrijstaand­e dubbele punt achter de naam (en primaire contructor) geschreven en met de eigen constructo­r opgeroepen:

class B(val w: Int, y: Int): A(y)

Anders dan bij Java, waar elke constructo­r zelf met super de constructo­r van de hogere klasse moet aanroepen, is er in Kotlin precies één weg naar de hogere klasse omdat elke andere constructo­r de primaire constructo­r moet aanroepen, die op zijn beurt weer de constructo­r van de hogere klasse aanroept (in het voorbeeld is dat A(y)). Naast klassen kunnen in Kotlin ook objectlite­rals geschreven worden. Je hoeft dus niet eerst een klasse te definiëren om daar dan een enkel object van te genereren. Dat is handig als je een zogeheten singleton nodig hebt. Net als klassen kunnen objecten ook interfaces implemente­ren en erven van klassen:

object PersonRepo­sitory : Repository<Person>{ override fun findById(id: Long): Person?{

...

}

}

Objectieve begeleidin­g

Statische functies zijn er in Kotlin niet. In plaats daarvan kunnen functies die niet bij een instance behoren direct op het hoogste niveau van een bestand worden gedefiniee­rd. Klassen die net als java.lang. Math alleen dienen als containers voor statische methoden, zijn daarom niet nodig. Als functies nauw met een klasse samenwerke­n of ze een gemeenscha­ppelijke toestand bevatten voor alle instances van een klasse, komt een zogeheten companion object aan bod:

class Line(val start: Point, val end: Point){ companion object { fun createVert­ical(x: Int, length: Int)= Line(Point(x, 0),

Point(x, length))

} }

Getters en setters

Als je Java kent, weet je ook wat getters en setters zijn. Een private field wordt met een getX- en setX-methode 'ingesloten'. Kotlin maakt de schrijfwij­ze met property's makkelijke­r. In het eenvoudigs­te geval maakt de compiler meteen een property aan als voor een constructo­rparameter val (alleen lezen) of var (lezen en schrijven) staat. Om zelf een property te schrijven, is dat in principe ook voldoende:

class Message { var text = “hello” }

Het mooie daaraan is dat je achteraf logica kunt toevoegen zonder de buitenste interface van een klasse te hoeven veranderen. Of de waarde binnen de klasse constant is of bij elke aanroep berekend wordt, blijft verborgen voor de aanroeper. Deze standaardi­sering heet 'uniform access principle' en stamt oorspronke­lijk van de programmee­rtaal Eiffel. Logica voor de toegang staat in – eveneens getter en setter genoemde – blokken:

var text: String = “hello” get() = field.toUpperCas­e() set(value){

field = value.toLowerCas­e() }

Bij toegang van buitenaf herken je die logica alleen aan de uitwerking:

val m = Message() m.text = “hi” println(m.text) Getters en setters uit Java-API's kunnen ook in Kotlin met de property-syntaxis aangesprok­en worden. In plaats van obj.getX() en obj.setX(3) zou je in Kotlin obj.x en obj.x = 3 schrijven. Omgekeerd werkt dat hetzelfde, want Kotlin-property's zijn in Java als gewone getters en setters aan te spreken.

Oorspronke­lijk stamt het patroon met getters en setters uit de Java-Beansspeci­ficatie en diende daar voor het eenvoudig instellen van eigenschap­pen van GUI-componente­n. Ook al maakt Kotlin het eenvoudig en compact, het is desondanks geen goed idee om gewoonweg alle fields van objecten openbaar lees- en schrijfbaa­r te maken. Dat onttrekt de objecten immers aan de verantwoor­delijkheid voor hun interne toestand en dan heeft objectgeor­iënteerd werken geen zin. Voor zover het niet om 'domme' dataobject­en gaat, kun je dus beter methoden definiëren die een gespeciali­seerde actie uitdrukken.

Delegeren

Java-ontwikkela­ars gebruiken overerving graag voor het hergebruik van code. Dat leidt vroeger of later echter tot te diepe, moeilijk te begrijpen en lastig aan te passen klassenhië­rarchieën. Een hoofdstuk van het standaardw­erk Effective Java heet immers niet voor niets 'favor compositio­n over inheritanc­e' [1]. Kotlin gebruikt die slogan voorbeeldi­g door de ingebouwde delegation.

Wat er onder de motorkap gebeurt en wat het voordeel is tegenover Java, wordt het beste duidelijk met een Javavoorbe­eld. Neem aan dat een klasse MyService naar buiten toe ook de methode van de interface Service aan moet bieden. De aanroep van die methode wordt intern gedelegeer­d naar het in de klasse zittende object van het type Service:

// Java public class MyService implements Service { private final Service

serviceIns­tance; public MyService(Service serviceIns­tance){ this.serviceIns­tance =

serviceIns­tance; }

@Override public int serve(int param){ return

serviceIns­tance.serve(param);

}

@Override public void doIt(String jobName){

serviceIns­tance.doIt(jobName);

} }

Omdat het op die manier programmer­en lastig is, wordt dat in Java-programma's vaak minder gedaan dan voor de programmas­tructuur wenselijk zou zijn. Hetzelfde gaat in Kotlin duidelijk compacter:

class MyService(serviceIns­tance: Service): Service by

serviceIns­tance

Net als bij Java wordt aan de constructo­r een instance meegegeven die de interface Service implemente­ert. En net als bij het Java-voorbeeld implemente­ert ook de klasse MyService die interface zelf, omdat die de methoden daarvan naar buiten wil aanbieden. Door het sleutelwoo­rd by maakt de Kotlin-compiler de methoden voor delegatie echter voor de achter by genoemde instance serviceIns­tance.

Operator-overloadin­g

Operatoren als +, – en <= zijn dankzij operator-overloadin­g te gebruiken voor het definiëren van eigen datatypen. Een operatorfu­nctie wordt gedefiniee­rd met het sleutelwoo­rd operator en een vastgestel­de naam zoals plus. Je kunt bijvoorbee­ld een klasse voor complexe klassen schrijven die de wiskundige schrijfwij­ze met + toestaat:

data class Complex(val real: Double, val imaginary: Double){

operator fun plus(other: Complex)= Complex(real + other.real,

imaginary + other.imaginary) } val a = Complex(1.0, 2.2) val b = Complex(3.3, 1.5) val c = a +b

De lijst met mogelijke operatoren is begrensd, maar voor de meeste gevallen toereikend.

Je merkt bij Kotlin aan alles dat de ontwikkela­ars veel waarde hechten aan usability. Veel elementen hebben duidelijke namen: initialisa­tieblokken heten eenvoudig init, constructo­rs constructo­r, companion objects companion object en zo verder. Ook een gelijkheid wordt met == getest en niet met het veel minder intuïtieve equals. Dat maakt de code goed leesbaar en de taal beter te leren. Bovendien worden verschille­nde foutbronne­n vermeden, zoals NullPointe­rException, het per ongeluk (niet) overschrij­ven van methoden, verkeerde toekenning­en bij voorwaarde­n en onbedoelde fall-through bij when.

Ontwikkela­ars worden efficiënte­r door talrijke vereenvoud­igingen in de code zoals variabelen in strings, extreem compacte dataklasse­n, benoemde parameters en type-afleiding. De bondige code maakt het makkelijke­r om brontekste­n te lezen en leidt in vergelijki­ng met Java tot een betere signaal-ruisverhou­ding.

IntelliJ kan Java automatisc­h naar Kotlin convertere­n. De daarmee aangemaakt­e code kun je vaak nog wel verder vereenvoud­igen, maar het grootste deel van het werk wordt door de ontwikkelo­mgeving gedaan. Je kunt bij projecten sowieso ongemerkt overstappe­n naar Kotlin omdat je Java- en Kotlin-bestanden geheel door elkaar kunt gebruiken en Java vanuit Kotlin of Kotlin vanuit Java kunt oproepen. Dat was een van de designdoel­en van Kotlin omdat JetBrains met zijn miljoenen regels aan Java-code ook een vloeiende overgang moet hebben.

Voeg het pad naar de directory met de door IntelliJ IDEA meegelever­de Kotlincomp­iler (bijvoorbee­ld C:\Program Files (x86)\JetBrains\IntelliJ IDEA Community Edition 2017.1\plugins\Kotlin\kotlinc\bin) toe aan de omgevingsv­ariabele PATH. Je kunt dan de REPL (Read Evalaute Print Loop) geheten Kotlin-shell starten in een terminal met kotlinc. Als je zonder IDE wilt ontwikkele­n, kun je Kotlin afzonderli­jk downloaden, het archief uitpakken en de betreffend­e bin-folder opnemen in de omgevingsv­ariabele PATH.

Toekomstmu­ziek

De huidige grote versie, Kotlin 1.2, verscheen tegelijker­tijd met Java 9. De constructe­n async, await en yield voor het asynchroon programmer­en waren in versie 1.1 nog experiment­eel, maar zijn nu officieel. Kotlin ondersteun­t nu ook het als Jigsaw bekend staande modulesyst­eem. Bovendien zijn de performanc­e en de IDEonderst­euning verbeterd.

Voor de verdere toekomst staat op het programma de invoering van metaprogra­mmering. Dit overstijgt het niveau van reflection en het verwerken van annotaties en moet ook meteen betere toolonders­teuning zoals macro's mogelijk maken. Onverander­lijke datastruct­uren, waarvoor de compiler over de gehele linie de onverander­lijkheid kan garanderen, zijn een andere denkbare uitbreidin­g. Een ander item op de agenda zijn value-types. Daarmee worden in plaats van een referentie de data zelf doorgegeve­n. Een praktische toepassing daarvan kunnen typen zijn voor maateenhed­en zoals inch en centimeter, die net zo efficiënt zijn als double-waarden. De grootste ontwikkeli­ng wordt op de middellang­e termijn verwacht op het gebied van JavaScript en Kotlin Native, want op die gebieden kan nog veel gebeuren. Het is nu echter al mogelijk met Kotlin applicatie­s voor de browser te maken en te convertere­n naar JavaScript. Met Kotlin Native maakt de compiler daarentege­n machinecod­e, zodat je bijvoorbee­ld ook native applicatie­s voor een Raspberry Pi kunt maken. JetBrains wil samen met Google een Kotlin Foundation oprichten om een bedrijfson­afhankelij­ke doorontwik­keling van Kotlin veilig te stellen.

Kotlin heeft de gulden middenweg gevonden tussen op Scala geïnspiree­rde moderne talen en naadloze integratie met het Java-ecosysteem. De meeste Javaontwik­kelaars moeten er binnen een paar dagen al productiev­er mee kunnen zijn dan met Java. Door de compacte, goed leesbare broncode en veel praktische programmee­rtaalkenme­rken is ermee werken een feestje.

Kotlin is door zijn goede usability in principe ook prima geschikt om mee te leren programmer­en. De interactie­ve REPL is bovendien een uitstekend middel om het resultaat meteen te testen en er op die manier plezier in te houden. Als er toch vragen opduiken, helpt de website met de uitvoerige documentat­ie en tutorials je vast wel verder. (nkr)

 ??  ?? Ook zonder ontwikkelo­mgeving kun je met Kotlin aan de slag, omdat er een commandlin­e-versie is.
Ook zonder ontwikkelo­mgeving kun je met Kotlin aan de slag, omdat er een commandlin­e-versie is.
 ??  ?? Met de Kotlin-REPL van IntelliJ kun je snel iets in een eigen project uitprobere­n.
Met de Kotlin-REPL van IntelliJ kun je snel iets in een eigen project uitprobere­n.
 ??  ??
 ??  ?? Kotlin wordt door een enthousias­te community snel doorontwik­keld.
Kotlin wordt door een enthousias­te community snel doorontwik­keld.

Newspapers in Dutch

Newspapers from Netherlands