Kotlin logo

Kotlin

A concise multiplatform language developed by JetBrains

Evaluierung von Kotlin in realen Projekten

Read this post in other languages:

Gastbeitrag von Urs Peter, Senior Software Engineer und JetBrains-zertifizierter Kotlin Trainer. Für Leser*innen, beim Ausbau ihrer Kotlin-Kenntnisse strukturierter vorgehen möchten, leitet Urs auch das Kotlin Upskill Program an der Xebia Academy.

Dies ist der zweite Beitrag der Reihe Der ultimative Leitfaden für den erfolgreichen Einstieg in Kotlin in einer Java-dominierten Umgebung, die der Frage nachgeht, wie sich Kotlin in Teams in der realen Welt verbreitet, beginnend mit der Neugier einer einzelnen Person bis hin zum unternehmensweiten Wechsel.

Hier finden Sie den ersten Teil: Einführung in Kotlin für Java-Entwickler*innen


Die Evaluierungsphase: Wenn Kotlin mehr als nur eine Spielwiese ist

Sobald Sie Kotlin durch den Einsatz in Tests kennengelernt haben, wird es Zeit für eine gründlichere Evaluierung. Sie haben zwei Hauptansätze zur Auswahl:

  1. Einen neuen Microservice oder eine neue Anwendung in Kotlin entwickeln
  2. Eine bestehende Java-Anwendung erweitern/konvertieren

1. Einen neuen Microservice oder eine neue Anwendung in Kotlin entwickeln

Wenn Sie mit einer neuen Anwendung oder einem neuen Microservice von vorn beginnen, können Sie die vollständige Kotlin-Erfahrung testen, ohne durch bestehenden Code eingeschränkt zu werden. Dieser Ansatz bietet häufig die beste Lernerfahrung und die klarsten Einblicke in die Stärken von Kotlin.

Profitipp: Lassen Sie sich in dieser Phase von Fachleuten unterstützen. Entwickler*innen sind zwar naturgemäß von ihren Fähigkeiten überzeugt, jedoch kann Ihnen das Vermeiden früher Fehler in Form von Java-ähnlichem Kotlin und fehlenden Kotlin-basierten Bibliotheken Monate an technischen Schulden einsparen.

So vermeiden Sie häufige Fehler, wenn Sie mit einem Java-Hintergrund in Kotlin einsteigen:

Fehler: Ein anderes Framework in Kotlin als in Java verwenden.

Tipp: Bleiben Sie bei Ihrem bestehenden Framework.

Höchstwahrscheinlich haben Sie Spring Boot mit Java verwendet, und das können Sie auch mit Kotlin verwenden. Spring Boot wird von Kotlin hervorragend unterstützt, die Verwendung einer anderen Lösung bietet also keine Vorteile. Wenn Sie nicht nur eine neue Sprache, sondern auch ein neues Framework erlernen müssen, handeln Sie sich nur zusätzliche Komplexität ohne besondere Vorteile ein.

Wichtig: Spring kommt dem Kotlin-Prinzip der „standardmäßigen Vererbung“ in die Quere, sodass Sie Klassen explizit als „open“ markieren müssen, um sie erweitern zu können.

Um nicht alle Spring-bezogenen Klassen (etwa @Configuration usw.) als „open“ markieren zu müssen, verwenden Sie das folgende Build-Plugin: https://kotlinlang.org/docs/all-open-plugin.html#spring-support. Wenn Sie ein Spring-Projekt mit dem bekannten Online-Tool Spring Initializr anlegen, ist dieses Build-Plugin bereits für Sie konfiguriert.

Fehler: Kotlin in einer an Java angelehnten Weise schreiben und sich eher auf gängige Java-APIs als auf die Kotlin-Standardbibliothek verlassen: 

Diese Liste könnte sehr lang werden, daher gehen wir hier nur auf die häufigsten Fallstricke ein:

Fehler 1: Java-Streams statt Kotlin-Collections verwenden

Tipp: Verwenden Sie stets Kotlin-Collections.

Kotlin-Collections sind vollständig kompatibel mit Java-Collections, verfügen jedoch über einfache und reichhaltige Funktionen höherer Ordnung, die Java-Streams überflüssig machen.

Im Folgenden finden Sie ein Beispiel, das die drei umsatzstärksten Produkte (Preis * Absatzmenge) je Produktkategorie ausweist:

Java

record Product(String name, String category, double price, int sold){}

List products = List.of(
           new Product("Lollipop", "sweets", 1.2, 321),
           new Product("Broccoli", "vegetable", 1.8, 5);

Map<String, List> top3RevenueByCategory =
       products.stream()
          .collect(Collectors.groupingBy(
                Product::category,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    list -> list.stream()
                              .sorted(Comparator.comparingDouble(
                                  (Product p) -> p.price() * p.sold())
                                   .reversed())
                                   .limit(3)
                                   .toList()
                       		)
          )
);

Kotlin

val top3RevenueByCategory: Map<String, List> =
   products.groupBy { it.category }
       .mapValues { (_, list) ->
           list.sortedByDescending { it.price * it.sold }.take(3)
       }

Dank der Java-Interoperabilität von Kotlin können Sie mit Java-Klassen und -Records arbeiten, als wären sie native Kotlin-Klassen – wobei Sie stattdessen auch eine Kotlin-(Daten-)Klasse verwenden könnten.

Fehler 2: Optional aus Java weiterhin verwenden.

Tipp: Nutzen Sie Nullable-Typen.

Einer der Hauptgründe, warum Java-Entwickler*innen zu Kotlin wechseln, ist die integrierte Nullability-Unterstützung von Kotlin, die NullPointerExceptions ein für alle Mal den Garaus macht. Versuchen Sie daher, ausschließlich Nullable-Typen statt Optionals zu verwenden. Sie haben noch Optionals in Ihren Schnittstellen? So können Sie sie unkompliziert loswerden, indem Sie sie in Nullable-Typen konvertieren:

Kotlin

//Let’s assume this repository is hard to change, because it’s a library you depend on
class OrderRepository {
      //it returns Optional, but we want nullable types
      fun getOrderBy(id: Long): Optional = …
}

//Simply add an extension method and apply the orElse(null) trick
fun OrderRepository.getOrderByOrNull(id: Long): Order? = 
                                    getOrderBy(id).orElse(null)

//Now enjoy the safety and ease of use of nullable types:

//Past:
 val g = repository.getOrderBy(12).flatMap { product ->
     product.goody.map { it.name }
}.orElse("No goody found")

//Future:
 val g = repository.getOrderByOrNull(12)?.goody?.name ?: "No goody found"

Fehler 3: Weiterhin statische Wrapper verwenden.

Tipp: Nutzen Sie Extension-Methoden.

Extension-Methoden haben zahlreiche Vorteile:

  • Sie machen Ihren Code wesentlich flüssiger und lesbarer als Wrapper.
  • Sie können von der Code-Completion gefunden werden, was auf Wrapper nicht zutrifft.
  • Da Extensions importiert werden müssen, ermöglichen sie die selektive Nutzung erweiterter Funktionalitäten in bestimmten Bereichen Ihrer Anwendung.

Java

//Very common approach in Java to add additional helper methods
public class DateUtils {
      public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = 
           DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

      public String formatted(LocalDateTime dateTime, 
		              DateTimeFormatter formatter) {
         return dateTime.format(formatter);
      }

      public String formatted(LocalDateTime dateTime) {
         return formatted(dateTime, DEFAULT_DATE_TIME_FORMATTER);
      }
}

//Usage
 formatted(LocalDateTime.now());

Kotlin

val DEFAULT_DATE_TIME_FORMATTER: DateTimeFormatter = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

//Use an extension method, including a default argument, which omits the need for an overloaded method. 
fun LocalDateTime.asString(
   formatter: DateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER): String = 
      this.format(formatter)

//Usage
LocalDateTime.now().formatted()

Beachten Sie, dass Kotlin Top-Level-Methoden und -Variablen erlaubt. Dies bedeutet, dass wir beispielsweise einen DEFAULT_DATE_TIME_FORMATTER einfach auf der obersten Ebene deklarieren können, ohne ihn wie in Java an ein Objekt binden zu müssen.

Fehler 4: (Umständliche) Java-APIs verwenden.

Tipp: Nutzen Sie das elegante Kotlin-Gegenstück. 

Die Kotlin-Standardbibliothek bietet Erweiterungsmethoden, die eine erheblich benutzerfreundlichere Nutzung der Java-Bibliotheken ermöglichen, obwohl weiterhin die Java-Implementierung zugrunde liegt. Fast alle gängigen Bibliotheken und Frameworks von Drittanbietern, darunter auch Spring, gehen ebenso vor.

Beispiel – Standardbibliothek:

Java

String text;
try (
       var reader = new BufferedReader(
                  new InputStreamReader(new FileInputStream("out.txt"), 
            StandardCharsets.UTF_8))) {
   text = reader
            .lines()
            .collect(Collectors.joining(System.lineSeparator()));
}
System.out.println("Downloaded text: " +  text + "n");

Kotlin

//Kotlin has enhanced the Java standard library with many powerful extension methods, like on java.io.*, which makes input stream processing a snap due to its fluent nature, fully supported by code completion

val text = FileInputStream("path").use {
             it.bufferedReader().readText()
           }
println("Downloaded text: $textn");

Beispiel – Spring:
Java

final var books =  RestClient.create()
       .get()
       .uri("http://.../api/books")
       .retrieve()
       .body( new ParameterizedTypeReference<List>(){}); // ⇦ inconvenient ParameterizedTypeReference

Kotlin

import org.springframework.web.client.body
val books = RestClient.create()
   .get()
   .uri("http://.../api/books")
   .retrieve()
   .body<List>() //⇦ Kotlin offers an extension that only requires the type without the need for a ParameterizedTypeReference

Fehler 5: Für jede öffentliche Klasse eine separate Datei verwenden.

Tipp: Fassen Sie verwandte öffentliche Klassen in einer einzigen Datei zusammen. 

Auf diese Weise können Sie sich einen guten Überblick über die Struktur eines (Teil-)Bereichs verschaffen, ohne Dutzende Dateien durchlesen zu müssen.

Java

Kotlin

//For domain classes consider data classes - see why below
data class User(val email: String,
            //Use nullable types for safety and expressiveness
           val avatarUrl: URL? = null, 
           var isEmailVerified: Boolean)

data class Account(val user:User,
              val address: Address,
              val mfaEnabled:Boolean,
              val createdAt: Instant)

data class Address(val street: String,
              val city: String,
              val postalCode: String)

Fehler 6: Am Paradigma der Mutabilität festhalten.

Tipp: Auf Immutabilität setzen – den Standard in Kotlin

In vielen Programmiersprachen – einschließlich Java – gibt es einen eindeutigen Trend: Immutabilität sticht Mutabilität aus.

Der Grund dafür ist einfach: Immutabilität (Unveränderlichkeit) verhindert unbeabsichtigte Nebenwirkungen, und dadurch wird der Code sicherer, vorhersehbarer und leichter nachvollziehbar. Auch die Nebenläufigkeit wird dadurch vereinfacht, da unveränderliche Daten problemlos von mehreren Threads gemeinsam genutzt werden können, ohne dass Race-Conditions auftreten können.

Aus diesem Grund setzen die meisten modernen Sprachen – darunter auch Kotlin – entweder standardmäßig auf Immutabilität oder empfehlen diese nachdrücklich. In Kotlin ist Immutabilität der Standard, obwohl auch Mutabilität eine Option ist, wenn sie tatsächlich gebraucht wird.

Hier ist eine kurze Einführung in die Immutabilität in Kotlin:

1. Nutzen Sie val anstelle von var.

Sie sollten val gegenüber var den Vorzug geben. IntelliJ IDEA warnt Sie, wenn Sie an einer Stelle var verwenden, wo auch val ausreichen würde.

2. Verwenden Sie (unveränderliche) Datenklassen mit copy(...).

Für domänenbezogene Klassen verwenden Sie data-Klassen mit val. Die data-Klassen von Kotlin werden häufig mit den records von Java verglichen. Zwar gibt es einige Überlappungen, jedoch bieten data-Klassen das Killer-Feature copy(...), dessen Fehlen in Java das Verändern von Records sehr mühsam macht, obwohl dies in der Anwendungslogik ein alltäglicher Vorgang ist:

Java

//only immutable state
public record Person(String name, int age) {
   //Lack of default parameters requires overloaded constructor
   public Person(String name) { 
       this(name, 0);
   }
   //+ due to lack of String interpolation
  public String sayHi() {
       return "Hello, my name is " + name + " and I am " + age + " years old.";
   }
}

//Usage
final var jack = new Person("Jack", 42);
jack: Person[name=Jack, age=5]

//The issue is here: transforming a record requires manually copying the identical state to the new instance ☹️
final var fred = new Person("Fred", jack.name);

Kotlin

//also supports mutable state (var)
data class Person(val name: String,
                  val age: Int = 0) {
  //string interpolation
  fun sayHi() = "Hi, my name is $name and I am $age years old."
}
val jack = Person("Jack", 42)
jack: Person(name=Jack, age=42)

//Kotlin offers the copy method, which, due to the ‘named argument’ feature, allows you to only adjust the state you want to change 😃
val fred = jack.copy(name = "Fred")
fred: Person(name=Fred, age=42)

Verwenden Sie außerdem nach Möglichkeit Datenklassen für domänenbezogene Klassen. Durch ihre Unveränderlichkeit gewährleisten diese eine sichere, prägnante und reibungslose Nutzung im Kernbereich Ihrer Anwendung.

Tipp: Verwenden Sie unveränderliche statt veränderliche Collections

Unveränderliche Collections bieten eindeutige Vorteile hinsichtlich Thread-Sicherheit, können sicher weitergegeben werden und sind einfacher zu verstehen. Java bietet zwar einige Funktionen in Bezug auf die Immutabilität von Collections, aber diese sind mit Vorsicht zu genießen, da sie leicht zu Laufzeitfehlern führen können:

Java

List.of(1,2,3).add(4); ❌unsafe 😬! .add(...) compiles, but throws UnsupportedOperationException

Kotlin

//The default collections in Kotlin are immutable (read-only)
listOf(1,2,3).add(4);  //✅safe: does not compile

val l0 = listOf(1,2,3) 
val l1 = l0 + 4 //✅safe: it will return a new list containing the added element
l1 shouldBe listOf(1,2,3,4) //✅

Das Gleiche gilt für die Verwendung von Collections.unmodifiableList(...), was nicht nur unsicher ist, sondern auch eine zusätzliche Zuweisung erfordert:

Java

class PersonRepo {
   private final List cache = new ArrayList();
   // Java – must clone or wrap every call
   public List getItems() {
       return Collections.unmodifiableList(cache);   //⚠️extra alloc
   }
}

//Usage
personRepo.getItems().add(joe) ❌unsafe 😬! .add(...) can be called but throws UnsupportedOperationException

Kotlin

class PersonRepo {

//The need to type ‘mutable’ for mutable collections is intentional: Kotlin wants you to use immutable ones by default. But sometimes you need them:

   private val cache: MutableList = mutableListOf()

   fun items(): List = cache //✅safe: though the underlying collection is mutable, by returning it as its superclass List, it only exposes the read-only interface

}

//Usage
personRepo.items().add(joe) //✅safe:😬! Does not compile

In Bezug auf Nebenläufigkeit sollten unveränderliche Datenstrukturen bevorzugt werden, und dies gilt auch für Collections. In Java ist erhöhter Aufwand mit speziellen Collections erforderlich, die eine unterschiedliche oder eingeschränkte API bieten, wie beispielsweise CopyOnWriteArrayList. In Kotlin hingegen eignet sich die schreibgeschützte List für nahezu alle Anwendungsfälle.

Wenn Sie veränderbare, threadsichere Collections benötigen, bietet Kotlin Persistent Collections (persistentListOf(...), persistentMapOf(...)), die alle dieselbe leistungsstarke Schnittstelle nutzen.

Java

ConcurrentHashMap persons = new ConcurrentHashMap();
persons.put("Alice", 23);
persons.put("Bob",   21);

//not fluent and data copying going on
Map incPersons = new HashMap(persons.size());
persons.forEach((k, v) -> incPersons.put(k, v + 1));

//wordy and data copying going on
persons
   .entrySet()
   .stream()
   .forEach(entry -> 
      entry.setValue(entry.getValue() + 1));

Kotlin

persistentMapOf("Alice" to 23, "Bob" to 21)
         .mapValues { (key, value) -> value + 1 } //✅same rich API like any other Kotlin Map type and not data copying going on

Fehler 7: Weiterhin Builder einsetzen (oder noch schlimmer: Lombok verwenden) 

Tipp: Verwenden Sie benannte Argumente.

Builder sind in Java weit verbreitet. Sie sind zwar praktisch, doch sie bedeuten zusätzlichen Code, sind unsicher und erhöhen die Komplexität. In Kotlin sind sie nutzlos, da sie durch ein einfaches Sprachmerkmal abgelöst wurden: benannte Argumente.

Java

public record Person(String name, int age) {

   // Builder for Person
   public static class Builder {
       private String name;
       private int age;

       public Builder() {}

       public Builder name(String name) {
           this.name = name;
           return this;
       }

       public Builder age(int age) {
           this.age = age;
           return this;
       }

       public Person build() {
           return new Person(name, age);
       }
   }
}

//Usage
new JPerson.Builder().name("Jack").age(36).build(); //compiles and succeeds at runtime

new JPerson.Builder().age(36).build(); //❌unsafe 😬: compiles but fails at runtime.

Kotlin

data class Person(val name: String, val age: Int = 0)

//Usage - no builder, only named arguments.
Person(name = "Jack") //✅safe: if it compiles, it always succeeds at runtime
Person(name = "Jack", age = 36) //✅

2. Eine bestehende Java-Anwendung erweitern/konvertieren

Wenn Sie keine Möglichkeit haben, Kotlin „auf der grünen Wiese“ auszuprobieren, können Sie neue Kotlin-Features oder ganze Kotlin-Module zu einem bestehenden Java-Codebestand hinzufügen. Dank der mühelosen Java-Interoperabilität von Kotlin können Sie Kotlin-Code schreiben, der für Java-Aufrufer wie Java aussieht. Dieser Ansatz bietet Ihnen folgende Möglichkeiten:

  • Schrittweise Migration ohne umfassende Neuprogrammierung
  • Praxistests mit Kotlin in Ihrem spezifischen Kontext
  • Stärkung des Teamvertrauens durch Kotlin-Produktionscode

Anstatt einfach an einer beliebigen Stelle zu beginnen, wählen Sie einen der folgenden Ansätze:

Von außen nach innen:

Sie beginnen in den äußeren Abschnitten Ihrer Anwendung, z. B. Controller, Batch-Job usw., und arbeiten sich dann in Richtung Anwendungskern vor. Dies bietet die folgenden Vorteile:

  • Isolierte Kompilierung: Außen liegende Klassen werden selten als Abhängigkeiten genutzt, sodass Sie sie auf Kotlin umstellen können, ohne den Rest des Systems zu verändern.
  • Weniger Folgeänderungen. Wenn Sie eine Bedienoberfläche oder einen Controller konvertieren, können diese dank der problemlosen Interoperabilität bestehenden Java-Code nahezu unverändert aufrufen.
  • Kleinere PRs, einfachere Reviews. Sie können entweder Datei für Datei oder Feature für Feature migrieren.

Von innen nach außen:

Im Kern zu beginnen und dann zu den äußeren Schichten überzugehen ist häufig riskanter, da die oben erwähnten Vorteile des Von-außen-nach-innen-Ansatzes nicht zur Geltung kommen. In den folgenden Fällen ist jedoch auch dies eine praktikable Option:

  • Sehr kleiner oder in sich geschlossener Kern. Wenn Ihre Domänenschicht nur aus einigen POJOs und Services besteht, kann eine frühzeitige Umstellung mit wenig Aufwand zu schaffen sein und sofort den Zugang zu idiomatischen Konstrukten (Datenklassen, Werteklassen, Sealed-Hierarchien) eröffnen.
  • Architekturänderung geplant. Wenn Sie ohnehin Invarianten refaktorieren oder DDD-Codemuster (Wertobjekte, Aggregate) einführen möchten, ist es manchmal sinnvoller, erst die Domäne in Kotlin neu zu konzipieren.
  • Strenge Nullsicherheits-Kontrakte. Durch die Umstellung auf Kotlin wird die Domäne zu einer „nullsicheren Festung“; äußere Java-Schichten können zwar weiterhin Nullwerte senden, die Grenzen sind jedoch explizit und leichter zu kontrollieren.

Modul für Modul

  • Wenn Ihre Architektur eher nach Funktionalitäten als nach Schichten strukturiert ist und die Modulgrößen überschaubar sind, ist die stückweise Konvertierung eine gute Strategie.

Sprachmerkmale für die Konvertierung von Java in Kotlin

Kotlin bietet verschiedene Funktionalitäten – in erster Linie Annotationen –, die dafür sorgen, dass sich Ihr Kotlin-Code wie nativer Java-Code verhält. Besonders nützlich ist dies in hybriden Umgebungen, in denen Kotlin und Java innerhalb desselben Codebestands koexistieren.
Kotlin

class Person @JvmOverloads constructor(val name: String,
                          var age: Int = 0) {
  companion object {

  @JvmStatic
  @Throws(InvalidNameException::class)
  fun newBorn(name: String): Person = if (name.isEmpty()) 
       throw InvalidNameException("name not set")
     else Person(name, 0)

   @JvmField
   val LOG = LoggerFactory.getLogger(KPerson.javaClass)
  }
}

Java

//thanks to @JvmOverloads an additional constructor is created, propagating Kotlin’s default arguments to Java
var john =  new Person("John");

//Kotlin automatically generates getters (val) and setters (var) for Java
john.setAge(23);
var name = ken.getName();

//@JvmStatic and @JvmField all accessing (companion) object fields and methods as statics in Java

//Without @JvmStatic it would be: Person.Companion.newBorn(...)
var ken =  Person.newBorn("Ken"); 

//Without @JvmField it would be: Person.Companion.LOG
Person.LOG.info("Hello World, Ken ;-)");

//@Throws(...) will put the checked Exception in the method signature 
try {
  Person ken =  Person.newBorn("Ken");
} catch (InvalidNameException e) {
  //…
}

Kotlin

@file:JvmName("Persons")
package org.abc

@JvmName("prettyPrint")

fun Person.pretty() =
      Person.LOG.info("$name is $age old")

Java

//@JvmName for files and methods makes accessing static fields look like Java: without it would be: PersonKt.pretty(...)
Persons.prettyPrint(ken)

Der Java-zu-Kotlin-Konverter von IntelliJ IDEA

IntelliJ IDEA hat einen Java-zu-Kotlin-Konverter, sodass die IDE Ihnen die Umstellung theoretisch abnehmen kann. Der resultierende Code ist jedoch alles andere als perfekt, daher sollte er nur als Ausgangspunkt verwendet werden. Diese Ausgangsbasis sollten Sie so überarbeiten, dass das Wesen von Kotlin stärker zum Tragen kommt. Weitere Details zu diesem Thema folgen im letzten Abschnitt dieser Blogreihe: Erfolgsfaktoren für eine großflächige Einführung von Kotlin.

Wenn Sie Java als Ausgangspunkt nehmen, werden Sie wahrscheinlich Java-ähnliches Kotlin schreiben, was Ihnen zwar auch schon einige Vorteile einbringt, aber bei weitem nicht das volle Potenzial von Kotlin ausschöpft. Daher bevorzuge ich den Ansatz, eine neue Anwendung zu schreiben.

Im nächsten Teil der Reihe

In diesem Teil unserer Blogreihe Der ultimative Leitfaden für den erfolgreichen Einstieg in Kotlin in einer Java-dominierten Umgebung haben wir gezeigt, wie Kotlin-Experimente zu Produktionscode weiterentwickelt werden können. In unserem nächsten Artikel geht es um den menschlichen Faktor: wie Sie Ihre Kolleg*innen überzeugen können. Sie werden erfahren, wie Sie klare, codebezogene Argumente präsentieren, neue Entwickler*innen anleiten und eine kleine, aber ausdauernde Kotlin-Community innerhalb Ihres Teams aufbauen können.

Urs Peter

Urs ist erfahrener Softwareentwickler, Lösungsarchitekt, Vortragsredner und Dozent mit über 20 Jahren Erfahrung in der Entwicklung robuster, skalierbarer und unternehmenskritischer Systeme, hauptsächlich in Kotlin und Scala.

Neben seiner Tätigkeit als Berater ist er leidenschaftlicher Dozent und Autor einer großen Vielfalt von Kursen, die von Spracheinführungen für Kotlin und Scala bis hin zu Architekturschulungen für Microservices und ereignisgesteuerte Architekturen reichen.

Als kontaktfreudiger Mensch ist er gerne auf Treffen und Konferenzen, um sein Wissen zu teilen, andere zu inspirieren und selbst inspiriert zu werden. Urs ist JetBrains-zertifizierter Kotlin Trainer.

Autorin des ursprünglichen Blogposts

Alyona Chernyaeva

Alyona Chernyaeva

image description