Blog

Kotlin in einem Java-Projekt verwenden: 10 Lektionen gelernt

Benjamin Komen

Aktualisiert Oktober 17, 2025
14 Minuten

Eine der besten Möglichkeiten, Ihre Erfahrung als Entwickler zu verbessern, wenn Sie von Java kommen, besteht darin, Kotlin zu verwenden. Die Vorteile sind zahlreich, aber der große Vorteil der Nullsicherheit ist (meiner Meinung nach) schon genug, um den Wechsel zu vollziehen. Aufgrund der großen Interoperabilität zwischen Java und Kotlin ist es nicht schwierig, Kotlin in einem Java-Projekt zu verwenden. Es ist nicht notwendig, den gesamten vorhandenen Code umzuschreiben. Sie können damit beginnen, den gesamten neuen Code in Kotlin zu schreiben und den bestehenden Code in Java zu belassen.

Wenn Sie anfangen, Kotlin in Ihrem bestehenden Java-Projekt zu verwenden, werden Sie höchstwahrscheinlich auf einige Probleme stoßen, die Sie angehen müssen. In diesem Blog möchte ich einige Erfahrungen mit Ihnen teilen, in der Hoffnung, dass sie Ihnen auf dieser Reise helfen.

0 - Festlegen einer Migrationsstrategie

In der Einleitung wurde bereits die erste Migrationsstrategie erwähnt: Behalten Sie bestehenden Code in Java und schreiben Sie neuen Code nur in Kotlin. Aber das ist nur ein Vorschlag. Überlegen Sie, was Ihren Bedürfnissen am besten entspricht.

Wenn Ihre Codebasis klein ist, können Sie in Erwägung ziehen, den gesamten vorhandenen Java-Code in Kotlin umzuschreiben. IntelliJ verfügt über eine Funktion, die dies automatisch für Sie erledigt ( CodeConvert Java File to Kotlin File). Der generierte Kotlin-Code ist oft noch sehr Java-lastig und muss überarbeitet werden, um sauberen und idiomatischen Kotlin-Code zu erhalten. Da diese Strategie je nach Größe Ihrer Codebasis sehr zeitaufwendig sein kann, sollten Sie sich fragen, ob es die Zeit wert ist. Es kann sich lohnen, wenn sich der Code, den Sie umschreiben, in ständiger Entwicklung befindet. Es lohnt sich jedoch wahrscheinlich nicht, wenn der Code so gut wie nie angefasst wird.

Ein anderer Ansatz besteht darin, in kleineren Schritten vorzugehen. In diesem Fall schreiben Sie alle neuen Funktionen in Kotlin und schreiben zusätzlich den gesamten vorhandenen Java-Code um, der von dieser neuen Funktion berührt wird, aber keinen anderen vorhandenen Java-Code. Mit dieser Strategie vermeiden Sie, dass Sie im Vorfeld viel Zeit aufwenden müssen, um Ihre gesamte Codebasis neu zu schreiben. Stattdessen ist eine vollständig umgeschriebene Codebasis ein Endziel, auf das Sie in kleinen Schritten hinarbeiten.

Sie können auch unterscheiden, welchen Code Sie in Java behalten und welchen Sie in Kotlin umschreiben sollten, indem Sie seine Funktion aus der Perspektive der Architektur betrachten. Wenn Sie z.B. die hexagonale Architektur (auch bekannt als "Ports und Adapter") verwenden, ist es sinnvoll, den inneren Teil, Ihre Kerngeschäftslogik, in Kotlin umzuschreiben. Auf diese Weise kann er von allen Vorteilen von Kotlin profitieren und Sie können den äußeren Code in Java schreiben.

Dies sind nur einige Vorschläge für Ihre Migrationsstrategie. Welche Strategie Sie auch immer wählen, es ist zumindest gut, sich bewusst für eine zu entscheiden. Einfach loszulegen und zu sehen, wo Sie landen, ist wahrscheinlich eine schlechte Idee.

1 - Sie können Kotlin- und Java-Dateien mischen

Es gibt zwei Möglichkeiten, Ihren Kotlin- und Java-Quellcode in Ihrem Projekt zu strukturieren. Die eine ist, ein separates src/main/java und src/main/kotlin Paket zu haben. Das eine enthält nur Java-Dateien und das andere nur Kotlin-Dateien. Aber, und das hat mich überrascht, Sie müssen das nicht tun. Sie können Ihre Kotlin-Dateien auch mit Ihren Java-Dateien mischen, die sich beide im Ordner src/main/java befinden.

Java- und Kotlin-Code in separaten Paketen Java- und Kotlin-Code in separaten Paketen

Java- und Kotlin-Code gemischt im selben Paket Java- und Kotlin-Code gemischt im selben Paket

Ihre IDE und der Compiler können anhand der Dateierweiterung feststellen, ob eine Datei eine Java- oder eine Kotlin-Datei ist. Außerdem werden letztendlich sowieso beide Arten von Dateien zu JVM-Bytecode kompiliert. Daher ist die Tatsache, dass die Dateien gemischt sind, kein Problem.

Sie können darüber diskutieren, was eine sauberere Lösung ist: die Trennung Ihres Java- und Kotlin-Codes oder die Vermischung beider Sprachen? Einige würden argumentieren, dass eine Vermischung in Ordnung ist, da Sie Ihre Codebasis nach Funktionalität und nicht nach Art der verwendeten Sprache organisieren möchten. Andere mögen die klare Unterscheidung, um den 'alten' und den 'neuen' Code in gewisser Weise getrennt zu halten. Dies kann auch hilfreich sein, wenn Sie zu einer neuen Architektur übergehen, zum Beispiel von einer mehrschichtigen Architektur zu einer hexagonalen Architektur.

2 - Aufrufen von löschbarem Java-Code aus Kotlin

Wenn Ihre Codebasis noch größtenteils aus Java und teilweise aus Kotlin besteht, ist es wahrscheinlich, dass Sie eine in Java geschriebene Methode von Ihrem Kotlin-Code aus aufrufen müssen. Der Rückgabetyp der Java-Methode ist nullbar, da alles in Java null sein kann, aber das ist in Ihrem Kotlin-Code, in dem Sie die Methode aufrufen, nicht direkt ersichtlich. Dies kann eine Ursache für NullPointerExceptions sein, wenn die Java-Methode null zurückgibt und Ihr Kotlin-Code dies nicht erwartet.

Der Rückgabetyp der folgenden Java-Methode kann entweder vom Typ Lieferant oder null sein:

public Supplier getSupplier(String uid) {
  return [...]
}

Eine Möglichkeit, dies deutlicher zu machen, besteht darin, die Anmerkung org.jetbrains.annotations.Nullable über der Java-Methode hinzuzufügen, die aufgerufen wird und null zurückgeben kann. Dadurch wird Ihre IDE darüber informiert, dass Sie einen sicheren Aufrufoperator ?. für das Ergebnis verwenden sollten.

Eine andere Möglichkeit, damit umzugehen, ist, das Ergebnis der Java-Methode einer Variablen in Kotlin zuzuweisen und ihren Typ explizit als nullable zu definieren:

val supplier: Supplier? = supplierService.getSupplier(uid)

Das ist natürlich ausführlicher, als Sie Ihren Code normalerweise schreiben möchten, da Sie oft einfach val oder var und den Variablennamen schreiben und den Compiler den Typ ableiten lassen. In diesem Fall könnte es jedoch besser sein, den nullbaren Typ explizit zu definieren.

3 - Lombok verträgt sich nicht gut mit Kotlin

Wenn Ihr Java-Code Klassen enthält, die sich stark auf Lombok stützen, z.B. mit der @Builder Annotation, wird es schwierig, diesen von Lombok generierten Code in Kotlin aufzurufen, was mit der Kompilierabhängigkeit zu tun hat. Wenn Sie in Ihrem Projekt ausführen, werden Sie sehen, welche Aufgaben in welcher Reihenfolge ausgeführt werden. Sie werden sehen, dass der Schritt vor ausgeführt wird. Das bedeutet, wenn Ihr Kotlin-Code eine Methode verwendet, die erst aus einer Lombok-Annotation in JVM-Bytecode generiert werden muss, ist diese im Schritt compileKotlin noch nicht fertig.

Sie könnten diese Kompilierreihenfolge ändern, aber dann könnten Sie nicht mehr von Ihrem Java-Code auf Kotlin-Code verweisen.

2023-01-26: Seit Kotlin 1.8.0 gibt es nun Unterstützung für bestimmte Lombok-Annotationen in Kotlin.

Wie lässt sich das beheben?

Es gibt mehrere Lösungen. Die einfachste (schnellste und vielleicht schmutzigste) Lösung besteht darin, den Code, den Sie von Kotlin aus aufrufen, zu de-lombokisieren. Wenn Sie z.B. eine Java-Klasse mit einer @Getter Annotation haben und in Ihrem Kotlin-Code zwei dieser Getter auf Eigenschaften aufrufen, fügen Sie dem Java-Code wieder explizit Getter-Methoden hinzu. In IntelliJ ist es einfach, diese mit der Aktion Generate zu erzeugen. Ich füge oft einen Kommentar darüber hinzu, der erklärt, dass sie für den Aufruf aus meinem Kotlin-Code erforderlich sind:

// needed for Kotlin code
public String getEmailAddress() {
  return emailAddress;
}

Andere Lösungen sind die Verwendung des Lombok-Compiler-Plugins, das noch experimentell ist, oder die Verwendung von kapt (kotlin annotation processor), das in der Lage sein sollte, Lombok-Annotationen für Sie zu verarbeiten, so dass Sie den generierten Code aus Kotlin verwenden können.

Natürlich können Sie sich auch dafür entscheiden, Ihre Lombok-annotierten Java-Klassen vollständig durch Kotlin-Datenklassen zu ersetzen. Auf diese Weise haben Sie nicht das Problem, Lombok und Kotlin zu vermischen. Beachten Sie jedoch, dass Sie bei diesem Ansatz möglicherweise eine Menge an bestehendem Java-Code ändern müssen, der z.B. von Lombok generierte Builder verwendet und ein Refactoring benötigt, um mit den neuen Kotlin-Datenklassen zu arbeiten.

4 - Verwendung von Konstruktoren anstelle von Buildern

Wenn Sie es gewohnt sind, (Lombok-)Builder in Ihrem Java-Code zu verwenden, werden Sie sich wahrscheinlich an den Kotlin-Stil gewöhnen müssen, der stattdessen einen Konstruktor mit allen Argumenten vorsieht. Natürlich könnten Sie Builder auch manuell in Ihren Kotlin-Code schreiben, aber das ist eine so zeitaufwändige und mühsame Arbeit, dass ich nicht glaube, dass Sie diesen Weg einschlagen möchten. Das Schöne daran ist, dass Sie das auch gar nicht müssen, denn die Verwendung von benannten Parametern und eines Konstruktors mit allen Argumenten in Kotlin ist genauso lesbar wie ein Builder in Java.

Um den Konstruktor für alle Argumente mit benannten Parametern schnell zu schreiben, ist es hilfreich, die Kotlin Fill-Klasse in IntelliJ zu verwenden.

Die Verwendung der Aktion 'Klassenkonstruktor füllen' erleichtert die Instanziierung von Kotlin-Objekten Die Verwendung der Aktion 'Klassenkonstruktor füllen' erleichtert die Instanziierung von Kotlin-Objekten

Einheitstests

Bei Unit-Tests kann es recht mühsam sein, den Konstruktor für große Domänenobjekte zu füllen. Oft benötigen Sie nur einige der Parameter des Objekts für Ihren Test und müssen für die meisten Parameter Dummy-Daten eingeben. In diesen Fällen kann es sinnvoll sein, anstelle einer echten Instanziierung des großen Domänenobjekts eine Attrappe zu erstellen, in der Sie nur die Parameter angeben, die Sie in Ihrem Test benötigen. Dadurch wird Ihr Test auch viel konzentrierter, da er frei von Unordnung ist.

In Kotlin gibt es verschiedene Test-Frameworks als Alternative zu dem weit verbreiteten Mockito in Java. Ein gutes Framework für diesen Zweck ist mockk. Das Mocking eines großen Domänenobjekts kann wie folgt aussehen:

val order = mockk<Order> {
  every { uid } returns "someOrderUid"
  every { entries } returns listOf(
    mockk {
      every { productCode } returns "123456"
      every { quantity } returns 3
    }
  )
}

Jetzt müssen Sie nicht mehr die 14 Argumente einer Order und die 13 Argumente einer Ordereingabe definieren, während Sie in diesem Test nur an den grundlegenden Informationen einer Order interessiert sind. Die Parameter Ihres Mocks auf diese Weise auszudrücken, ist sehr lesbar, insbesondere wenn Sie verschachtelte Parameter haben.

Ein alternativer Ansatz wäre die Verwendung von Vorlagenobjekten in Ihren Tests, für die einige Standardwerte festgelegt sind. In dem Test, in dem Sie sie benötigen, ändern Sie nur bestimmte Werte, die für den Test benötigt werden, indem Sie die Funktion copy(…) verwenden. Dies funktioniert in zweierlei Hinsicht gut:

  • im Test sehen Sie deutlich, welche Attribute relevant sind - da Sie sie explizit anpassen;
  • kann eine Menge Boilerplate für das Mock-Verhalten weggelassen werden, was zu sauberen und lesbaren Tests führt.

5 - Kotlin-Alternative zu Spotless für die Formatierung

In Java können Sie Spotless für die Code-Stilprüfung verwenden. Die Verwendung von Spotless hilft Ihnen, den Code in Ihrem Team einheitlich zu formatieren. Bevor Sie den Code an VSC übergeben, müssen Sie den gradle-Befehl spotlessApply ausführen. Während des Builds auf dem CI/CD-Server wird die gradle-Aufgabe spotlessCheck ausgeführt, die den Build fehlschlagen lässt, wenn der Code nicht korrekt formatiert ist.

Obwohl Spotless auch Kotlin unterstützt, hat es einige merkwürdige Einschränkungen. Zum Beispiel lässt es keine Kommas am Ende Ihres Kotlin-Codes zu und schlägt mit einer kryptischen Fehlermeldung fehl:

Step 'ktlint' found problem in 'src/main/java/[...]/YourKotlinClass.kt':
Expecting a parameter declaration
com.pinterest.ktlint.core.ParseException: Expecting a parameter declaration
      at com.pinterest.ktlint.core.KtLint.format(KtLint.kt:357)

Daher können Sie für reine Kotlin-Projekte eine Implementierung von ktlint verwenden, das ktlint-gradle Plugin. Es bietet eine ähnliche Erfahrung wie Spotless. Sie müssen die Aufgabe ktlintFormat vor der Übergabe des Codes ausführen und ktlintCheck wird während des Builds im CI/CD ausgeführt.

Nehmen Sie das folgende, schlecht formatierte Codefragment als Beispiel:

private    fun foobar( )   : Unit {
    val    baz="some poorly formatted code"
  if(true){
    println("hahahaha")
   }
}

Wenn Sie die gradle-Aufgabe ktlintCheck ausführen, erhalten Sie ein Ergebnis:

> Task :ktlintMainSourceSetCheck FAILED
/Main.kt:40:13 Unnecessary space(s)
/Main.kt:40:26 Unexpected spacing after "("
/Main.kt:40:29 Unnecessary space(s)
/Main.kt:40:30 Unexpected spacing before ":"
/Main.kt:40:32 Unnecessary "Unit" return type
/Main.kt:41:13 Unnecessary space(s)
/Main.kt:41:18 Missing spacing around "="
/Main.kt:42:1 Unexpected indentation (6) (should be 8)
/Main.kt:42:9 Missing spacing after "if"
/Main.kt:42:15 Missing spacing before "{"
/Main.kt:43:1 Unexpected indentation (10) (should be 12)
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':ktlintMainSourceSetCheck'.
> A failure occurred while executing org.jlleitschuh.gradle.ktlint.worker.ConsoleReportWorkAction
   > KtLint found code style violations. Please see the following reports:
     - /SomeProject/build/reports/ktlint/ktlintMainSourceSetCheck/ktlintMainSourceSetCheck.txt

6 - (De)serialisierung polymorpher Klassen

In Java können Sie eine abstrakte Klasse haben, die mehrere konkrete Implementierungen hat. Wenn Sie diese Klasse in JSON serialisieren und dann von JSON wieder zurück nach Java deserialisieren möchten, benötigen Sie einen Mechanismus, um die Information, welche konkrete Klasse verwendet wurde, nicht zu verlieren.

Wenn Sie mit Jackson vertraut sind, können Sie dies mit Anmerkungen wie dieser tun:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "Dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "Cat")
})
public abstract class Animal {
    private String name;
}
public class Dog extends Animal {}
public class Cat extends Animal {}

Dadurch wird in der JSON-Ausgabe ein zusätzlicher Parameter hinzugefügt, der von der Deserialisierung verwendet wird, um zu wissen, welche konkrete Klasse instanziert werden soll.

In Kotlin können Sie dies weiterhin mit Jackson tun, aber Kotlin verfügt auch über sein eigenes Kotlin Serialisierungs-Framework. Ein zusätzlicher Vorteil ist die Kompatibilität mit Sealed-Klassen. Die Verwendung von Sealed-Klassen kann sehr nützlich sein, da Sie den Musterabgleich (mit dem Ausdruck when ) in Ihrem Kotlin-Code verwenden können, um die Logik für alle Unterklassen zu implementieren.

Es ist auch weniger Konfiguration erforderlich, da Sie nur die @Serializable Annotation zu Ihren Sealed- und Subklassen hinzufügen. Technisch gesehen funktioniert es sehr ähnlich wie Jackson. Es fügt der JSON-Ausgabe einen Parameter namens 'type' hinzu, der den Namen der Unterklasse enthält, der bei der Deserialisierung des JSON-Textes verwendet wird.

Eine Einschränkung: Wenn der Code, der Ihr JSON deserialisiert, nicht das Kotlin-Serialisierungs-Framework verwendet, weiß er nicht automatisch, wie er die Typinformationen im JSON behandeln soll.

7 - Alias eingeben

Manchmal hat eine Funktion ein Argument als Eingabe oder Ausgabe, das nicht offensichtlich ist. Zum Beispiel kann die Ausgabe der Gruppierung einer Liste zu einem Argument vom Typ Map<String, List<Long>> führen. Es kann schwierig sein, aus diesem Typ herauszulesen und zu verstehen, auf welche Daten er wirklich verweist.

Sie können dies verbessern, indem Sie einen Alias für diesen Typ definieren, z.B. mit:

typealias SomeMeaningfulTypeName = Map<String, List<Long>>

Jetzt können Sie SomeMeaningfulTypeName in Ihrem Code verwenden, wodurch die Stellen, an denen es verwendet wird, besser lesbar werden.

Dabei handelt es sich um syntaktischen Zucker, der nur in bestimmten Fällen verwendet werden kann, aber dennoch eine nette Funktion ist.

8 - Aussagekräftige Test-Assertionen

Apropos syntaktischer Zucker: Sie können eine Funktion nutzen, um Ihre Testaussagen aussagekräftiger und lesbarer zu schreiben.

Sie sind es wahrscheinlich gewohnt, Test-Assertions in der Form assertEquals(expected, actual) oder alternativ assertThat(actual).isEqualTo(expected) zu schreiben.

Kotlin bietet eine alternative Möglichkeit, dies zu schreiben, indem es ein Format verwendet: actual shouldBe expected. Dies liest sich eher wie natürliche Sprache, die ein Mensch sagen könnte. Zum Beispiel: customer.name shouldBe "John Doe".

Wie funktioniert das?

Nun, shouldBe ist eine Funktion, die Sie selbst definieren müssen. Sie kann auf folgende Weise implementiert werden, wenn Sie JUnit 5 verwenden:

import org.junit.jupiter.api.Assertions.assertEquals
infix fun  T.shouldBe(actual: T) = assertEquals(this, actual)

Es verwendet die Infix-Notation, die es erlaubt, den Punkt und die Klammern in einem Funktionsaufruf wegzulassen.

Sie können verschiedene andere Variationen der Funktion shouldBe erstellen, z.B. shouldNotBe oder shouldBeNull. Diese müssen Sie allerdings selbst schreiben, möglicherweise in einer Datei für Testprogramme. Da Sie wahrscheinlich bereits mit der Notation assertEquals vertraut sind und sich mit der Darstellung von Infix-Funktionen nicht so gut auskennen, könnte es außerdem einige Zeit dauern, bis dies die Lesbarkeit verbessert und Sie nicht verwirrt.

9 - Coroutinen verwenden

Einige Dienste führen selbst nicht viele Berechnungen durch, sondern warten hauptsächlich auf das Ergebnis des Aufrufs mehrerer anderer Dienste. In diesen Fällen haben Sie vielleicht CompletableFutures oder Reactor (in Kombination mit spring-boot-webflux) verwendet, mit all dem Streaming, Mapping und Zipping von Monos und Fluxes.

Wenn Sie diese Art von Code in Kotlin schreiben müssen, ist es sehr sinnvoll, Coroutines zu verwenden. Das Schreiben dieser Art von Code mit Coroutines ist viel sauberer, da das Konzept in die Sprache eingebaut ist, so dass Sie sich nicht auf eine Bibliothek verlassen müssen. Beachten Sie, dass Sie eine separate Abhängigkeit benötigen, um Coroutines in Ihrem Projekt zu verwenden.

Wenn Sie zum ersten Mal mit der Verwendung von Kotlin-Coroutinen beginnen, gibt es eine leichte Lernkurve, da Sie eine neue Sprachsyntax lernen und verstehen müssen, wie sie funktioniert, z.B. die Verwendung des richtigen Dispatchers für IO-Operationen. Wenn Sie nicht wissen, was Sie tun, könnten Sie am Ende Code schreiben, der Ihre Haupt-Threads blockiert, was Sie wahrscheinlich nicht wollen. In vielen Beispielen werden Sie zum Beispiel das Konstrukt runBlocking sehen, das Sie in Ihrem eigentlichen Code eigentlich nicht verwenden sollten. Aber wenn Sie erst einmal gelernt haben, wie man Coroutines richtig einsetzt, können Sie damit sehr lesbaren nebenläufigen Code schreiben.

Fazit

Ich hoffe, dass die Lektüre dieses Blogs Sie ermutigt hat, Kotlin in Ihrem Java-Projekt einzusetzen, denn ich glaube wirklich, dass es der richtige Schritt ist. Wenn Sie diese Lektionen beherzigen, sind Sie besser vorbereitet, diese Reise anzutreten. Sprechen Sie mich an, wenn Sie immer noch Bedenken haben, mit Kotlin zu beginnen. In jedem Fall würde ich sagen, probieren Sie es einfach aus und sehen Sie, was es Ihnen bringt!

Verfasst von

Benjamin Komen

Contact

Let’s discuss how we can support your journey.