Kotlin-Leckerbissen, die dem Entwickler das Leben erleichtern

Keine Kommentare

In diesem Beitrag möchte ich einige Features von Kotlin vorstellen, die es ermöglichen, kurzen und präzisen Code zu schreiben. Zum einen sind dies Funktionen der Kotlin-Standard-Bibliothek, zum anderen eigene Funktionen, welche Eigenschaften der Sprache Kotlin verwenden wie zum Beispiel Extension Functions und Lambda-Argumente.

Ich gehe nicht im Detail darauf ein, was Extension Functions sind oder auf spezielle Syntaxaspekte. Hierfür möchte ich auf die hervorragende Kotlin Documentation verweisen.

Funktionen aus der Kotlin-Standard-Bibliothek

Im ersten Teil des Artikels möchte ich einige Funktionen der Kotlin-Standard-Bibliothek vorstellen. Es handelt sich hier um eine kleine Auswahl, die ich nützlich finde, und ich möchte den Leser ausdrücklich ermutigen, sich den Quellcode dieser Funktionen anzuschauen und die Dokumentation zu lesen.

Überprüfen von Argumenten

Wenn man in seinem Code kein Framework zur Validierung verwendet, ist es oft notwendig, die Gültigkeit von Argumenten zu überprüfen. Im Normalfall wird das dann wie im folgenden Beispiel gemacht:

fun someFunction(answer: Int) {
  if(answer != 42) {
    throw IllegalArgumentException("the answer must be 42!")
  }
  // function code
}

Besser ist es, die require-Funktion der Standard-Bibliothek zu verwenden, welche die gleiche Funktionalität bietet, aber weniger Code benötigt:

fun someFunction(answer: Int) {
  require(answer == 42) {" the answer must be 42!"}
  // function code
}

Mehrere Werte aus einer Funktion zurückgeben

Manchmal ist es nötig, dass eine Funktion zwei Werte zurückgibt. Eine Möglichkeit hierfür ist es, eine neue Klasse anzulegen, die diese Werte enthält. Einfacher ist es, die Pair-Klasse aus Kotlin zu verwenden, zusammen mit der to infix-Funktion und einer destructuring-Variablendeklaration:

fun someFunction() : Pair<Int, String> {
  return 42 to "the answer"
}

fun main(args: Array<String>) {
  val (i: Int, s: String) = someFunction()
  log.info("$s is $i") // logs "the answer is 42"
}

Zeitmessungen

Um die Ausführungszeit von Programmteilen zu messen, wird häufig Code in dieser Form verwendet:

val start = System.currentTimeMillis()
// do something    
val end = System.currentTimeMillis()   

log.info("duration: ${end - start} msecs")

Kotlin bietet hierfür zwei inline-Funktionen, measureTimeMillis und measureNanoTime, die folgendermassen verwendet werden:

val duration = measureTimeMillis {
  // do something
}

log.info("duration: $duration msecs")

File IO

Die normalen JVM-Bibliotheken bieten keine Funktionen um ein Verzeichnis zu löschen, das nicht leer ist. Hierfür ist es notwendig, die im Verzeichnis liegenden Dateien und Unterverzeichnisse rekursiv zu löschen.
Kotlin hat für diesen Zweck eine Extension Function:

File("/path/to/some/directory").deleteRecursively()

Falls man den Namen einer Datei ohne die Dateiendung benötigt:

val f = File("image.jpg")
val name = f.nameWithoutExtension // sets name to "image"

Eine Datei zeilenweise einlesen und für jede Zeile eine Funktion aufrufen:

File("filename").useLines { line ->
  println(line)
}

Es gibt noch sehr viel mehr nützliche Funktionen im Package kotlin.io, auch hier verweise ich auf die Dokumentation.

Nebenläufige Programmierung

Reentrant read write locks

Für den Fall, dass man eine Resource absichern möchte, so dass immer nur ein Thread Schreibzugriff hat, aber mehrere Threads Lesezugriff, gibt es in der Java-Runtime die Klasse ReentrantReadWriteLock. Die korrekte Verwendung dieser Klasse ist aber nicht trivial, da auf Interruptions oder Exceptions geachtet werden muss, wenn die entsprechenden Locks belegt und freigegeben werden.

Kotlin bietet einige Extension-Functions der ReentrantReadWriteLock Klasse an, mit denen die Verwendung sehr vereinfacht wird:

val sharedResource = mutableMapOf<String, String>()
val lock = ReentrantReadWriteLock()

fun readSomeData(key: String) {
  return lock.read {
    sharedResource[key]
  }
}

fun writeSomeData(key: String, value: String) {
  lock.write {
    sharedResource[key] = value
  }
}

Threads und ThreadLocal Objekte

Wenn man ein ThreadLocal-Objekt verwendet, wird es normalerweise wie folgt initialisiert:

class ClassWithThreadState {
  private val state = object : ThreadLocal<String>() {
    override fun initialValue(): String = "initialValue"
  }

  fun someFunction() {
    var currentState = state.get()
    log.info { "currentState: $currentState" }
    state.set("newState")
  }
}

Kotlin fügt der Klasse die Funktion getOrSet hinzu, welche die Initiaisierung einfacher macht:

class ClassWithThreadState {
  private val state = ThreadLocal<String>()

  fun someFunction() {
    val currentState = state.getOrSet { "initalValue" }
    log.info { "currentState: $currentState" }
    state.set("newState")
  }
}

Um Code in einem eigenen Thread auszuführen, muss man ihn in ein Runnable einpacken, welches dann einem Thread-Objekt übergeben wird, das dann wiederum gestartet wird:

fun main(args: Array<String>) {
  val classWithThreadState = ClassWithThreadState()
  Thread(Runnable { classWithThreadState.someFunction() }).start()
}

Die Kotlin-Bibliothek bietet eine Funktion thread, die das Ganze vereinfacht:

fun main(args: Array<String>) {
  val classWithThreadState = ClassWithThreadState()
  // imediately start the thread with the runnable
  thread { classWithThreadState.someFunction() }
    
  // or first create it and start it later:
  val t = thread(start = false) { classWithThreadState.someFunction() }
  t.start()
}

Die thread-Funktion hat neben dem start-Parameter weitere optionale Parameter, die es erlauben, zum Beispiel die Priorität, den Namen oder auch den Daemon-Status des erzeugten Thread zu setzen.

Eigene (Extension-)Funktionen

Im zweiten Teil diese Artikels möchte ich zeigen, wie leicht es ist, eigene Funktionen zu schreiben, die dazu beitragen, dass der Code präziser, einfacher und besser zu warten wird.

Die Prinzipien, die ich für diese Funktionen verwende, sind die gleichen, die in der Kotlin-Standard-Bibliothek verwendet werden: Extension-Funktionen und Funktionen, die ein Lambda als letzten Parameter nehmen. Für weitere Informationen zu diesen Techniken verweise ich auf meinen früheren Blogpost “ Wie schreibt man eine Kotlin-DSL“.

Slf4j

Jeder Entwickler, der slf4j verwendet, kennt den Code um einen Logger zu erzeugen, bei dem der Name der Klasse wiederholt werden muss:

class SomeClass {
  fun doSomething() {
    log.info("doing something")
  }
    
  companion object {
    // always have to repeat the name of the class here:
    private val log: Logger = LoggerFactory.getLogger(SomeClass::class.java)
  }
}

Mit einer kleinen Extension-Funktion für die Any-Klasse ist es möglich, einen slf4j-Logger für jede Klasse zu erzeugen, auch wenn der Logger als Property eines companion-Objekt definiert wird:

fun Any.logger(): Logger {
  val clazz = if (this::class.isCompanion) this::class.java.enclosingClass else this::class.java
  return LoggerFactory.getLogger(clazz)
}

Bei der Verwendung dieser Funktion ist es nicht mehr nötig, die Klasse, für die der Logger erzeugt wird, anzugeben:

package de.codecentric.kotlingoodies
import logger

class SomeClass {
  fun doSomething() {
    log.info("doing something")
  }

  companion object {
    // creates a logger with the name "de.codecentric.kotlingoodies.SomeClass"
    private val log = logger()
  }
}

Wenn man zum Erstellen in der Log-Message Aufrufe wie String-Templating verwendet, empfiehlt es sich, vorher das Level des Loggers zu prüfen:

fun someFunction() {
  val someObject = Any() // or something else, doesn't matter here

  try {
    someObject.doSomething()
    if(log.isDebugEnabled) {
      log.debug("doing something with $someObject")
    }
  } catch (e: Exception) {
    if(log.isWarnEnabled) {
      log.warn("could not do something with $someObject" e)
    }
  }
}

Durch ein paar Extension-Funktionen der Logger-Klasse kann dies erheblich vereinfacht werden – ich zeige hier nur die Funktionen für das debug– und info-Level:

inline fun Logger.info(msg: (() -> String)) {if (isInfoEnabled) info(msg())}
inline fun Logger.info(t: Throwable, msg: (() -> String)) {if (isInfoEnabled) info(msg(), t)}
inline fun Logger.debug(msg: (() -> String)) {if (isDebugEnabled) debug(msg())}
inline fun Logger.debug(t: Throwable, msg: (() -> String)) {if (isDebugEnabled) debug(msg(), t)}

// use it like this: 
fun someFunction() {
  val someObject = Any() // or something else, doesn't matter here

  try {
    someObject.doSomething()
    log.debug { "doing something with $someObject" }
  } catch (e: Exception) {
    log.warn(e) { "could not do something with $someObject" }
  }
}

Diese Funktionen machen den Code erheblich lesbarer und verhindern auch, dass man das Loglevel für den Aufruf ändert und dabei vergisst, die dazugehörige Abfrage an das neue Level anzupassen.

Funktionen, um Boilerplate-Code zu kapseln

Es kommt häufig vor, dass im Code immer das Gleiche gemacht wird: zum Beispiel eine Transaktion starten, etwas ausführen, im Erfolgsfall die Transaktion committen und im Fehlerfall ein Rollback durchführen.

Um das elegant und wiederverwendbar zu schreiben, definiert man eine Funktion – inline und reified um den Rückgabewert zu erhalten – und übergibt dieser Funktion ein Lambda mit dem auszuführenden Code. Hier ein Beispiel mit einer Funktion transactional:

inline fun <reified R> transactional(f: () -> R): R {
    // code to begin transaction
  return try {
    f()
    // code to commit transaction
  } catch(e: Exception){
    // code to rollback transaction
    throw e
  }
}

Verwendet wird die Funktion dann wie folgt:

data class Record(val name: String)
fun main(args: Array<String>) {

  val records = transactional { 
    // this code is run within a transaction and is probably more complex in a real world scenario
    listOf(Record("John"), Record("James"))
  }

  records.forEach { println(it) }
}

Diakritische Zeichen entfernen

Kürzlich habe ich eine Funktion benötigt, die aus einem String diaktritische Zeichen (z. B. Akzente) entfernt. Ein erster Ansatz hierfür sieht folgendermassen aus:

fun noDiacritics (s: String) : String {
  val normalized = Normalizer.normalize(this, Normalizer.Form.NFD)
  val stripped = normalized.replace("\\p{M}".toRegex(), "")
  return stripped
}

Eleganter und besser ist es, diese Logik in eine Extension-Funktion der String-Klasse zu legen, damit kann sie für jede String-Variable und auch für String-Konstanten aufgerufen werden:

inline fun String?.noDiacritics() = this?.let{Normalizer.normalize(this, Normalizer.Form.NFD)
  .replace("\\p{M}".toRegex(), "")}

fun main(args: Array<String>) {
  println("Porsche Coupé".noDiacritics())
  // output is: Porsche Coupe
}

Zusammenfassung

In diesem Artikel habe ich gezeigt, wie kleine Funktionen – entweder aus der Kotlin-Standard-Bibliothek oder selbst geschrieben – dazu führen können, dass der eigene Code kürzer, besser lesbar und besser wartbar wird.

Kotlin-Eigenschaften wie Extension-Funktionen, inline- oder infix-Funktionen helfen dabei, solche „Hilfs“funktionen zu schreiben.

Ich kann nur empfehlen, sich den Quellcode der Kotlin-Standard-Bibliothek anzuschauen, man kann hier immer wieder Neues entdecken und lernen.

Tags

Peter-Josef Meisch

P.J. schreibt Software seit er 1980 zum ersten Mal einen Computer in die Hände bekommen hat. Er entwickelt hauptsächlich in Java und Kotlin, ist aber immer offen für neue Sprachen und Technologien.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.