Chapter 2. Java To Kotlin Projects

What is the first step of the journey from pure Java to a mixed and then increasingly Kotlin codebase?

The first time we introduced Kotlin to a Java codebase, we were a small team that included six developers, building a relatively greenfield project. We had already deployed some web applications with Kotlin, but our enterprise architects insisted that we write the new system in Java 8. This was shortly after Kotlin 1.0 had been released, but before Google announced that Kotlin was an official language for Android, so the architects were understandably wary about committing to a language with an uncertain future for a strategic system that they expected to be around for decades.

In Java, we leaned towards a functional approach, designing the core application domain model as immutable data types transformed by pipelines. However, we kept bumping into Java’s limitations: the verbosity required to implement immutable value types, the distinction between primitive and reference types, null references, Streams lacking common higher-order functions. Meanwhile, we could see Kotlin being adopted at an ever-increasing rate across the industry, and even within the company. When we saw Google’s announcement, we decided to start converting our Java to Kotlin.

Our judgement was that starting in the core domain model would give us the biggest bang for our buck. Kotlin’s data classes shrunk the code significantly, in some cases replacing hundreds of lines of code with a single declaration. We started carefully, using IntelliJ to convert a small value class that had no dependencies on other classes beyond those in the standard library, and examined how that affected the rest of our Java codebase. It had no effect at all! Emboldened by this success we picked up the pace. Whenever a new feature needed changes to a Java domain model class, we would first convert it to a Kotlin data class, commit the conversion, and then implement the feature.

As more of the domain model logic became pure Kotlin, we were able to make better use of Kotlin features. For example, we replaced calls to the Stream API with Kotlin’s standard functions on collections and sequences. The biggest improvement though, was replacing our use of Java’s Optional type with nullable references. This simplified our code and gave us greater confidence in its null safety.

Another project in the company adopted Kotlin for a different reason. They had a mature Java system that was built on Spring. The developers found that the use of reflection and annotations made the code difficult to understand and navigate in the IDE. Kotlin’s lightweight syntax for closures offered a way to define the structure of their application and distinguish between object graphs instantiated for the whole application, for each HTTP request, or each database transaction. They gradually refactored the underpinnings of their system from a framework that obscured the architecture to a compositional style in which the architecture was visible in the code. This work became the HTTP4K HTTP toolkit.

As these two examples show, the choice of starting point depends on a number of factors: why is the team adopting Kotlin, how large is the codebase, how frequently is it changed, … You probably have reasons aren’t in this list.

If you are choosing Kotlin for its language features it makes sense to convert the classes you are working in most frequently, as we did in the first example above. If you are choosing Kotlin to use a specific library then it makes sense to start writing Kotlin against the API, annotate it to make your Kotlin code convenient to the Java code in the rest of the app, and continue from there.

In a small team it’s easy to establish the Kotlin coding style for your system (beyond the standard style guide) — eg error handling conventions, how code is to be organised into files, what should be a top-level declaration and what should be in an object, etc.

Above a certain size, you run the risk of Kotlin code becoming inconsistent as people establish their own conventions in different parts of the system. So it may be worth starting with a small sub-team working in one area of the system, who establish conventions and build up a body of example code. Once there are some established conventions, you can expand the effort to the rest of the team and other parts of the system.

In the rest of this book we will examine in detail how to progress, how to keep your Java code maintainable while you are introducing Kotlin that it depends upon, how to take advantage of Kotlin’s features to further simplify the code after IntelliJ has performed its conversion magic. But all that follows the first small step.

Migrating a Java Build to Kotlin

If we want to refactor our Java to Kotlin, the first change we must make is to give ourselves the ability to write Kotlin code in our codebase. Happily, the Kotlin build tools and IDE make this very straightforward. It takes a few additional lines in our Gradle build configuration for it to compile Kotlin as well as Java. The IntelliJ IDE will pick up that configuration when we resync the build file, allowing us to navigate, autocomplete and refactor across both languages almost seamlessly.

To add Kotlin to our Gradle build we need to add the Kotlin plugin. There is a different plugin for each target that Kotlin supports (JVM, JavaScript and native code), and a plugin for building multiplatform projects. Because we have a Java project, we can ignore other platforms and use the Kotlin JVM plugin.

We also need to add the Kotlin standard library to our dependencies, and specify the minimumev JVM version that the output bytecode will support. Our project targets JDK 11 (the latest LTS at the time of writing). At the time of writing, the Kotlin compiler can generate bytecode compatible with JDK 1.6 or JDK 1.8. JDK 1.8 bytecode is more efficient and runs fine on JDK 11, so we will pick that.

Here are the relevant parts of our Gradle build file before changes.

plugins {
    id("java")
}

java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
// ... and other project settings ...

dependencies {
    implementation "com.fasterxml.jackson.core:jackson-databind:2.10.0"
    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.0"
    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.0"
    // ... and the rest of our app's implementation dependencies

    testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"
    testImplementation "org.junit.jupiter:junit-jupiter-params:5.4.2"
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.2"
    testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.4.2"
    // ... and the rest of our app's test dependencies
}

// ... and the rest of our build rules

and after

plugins {
    id 'org.jetbrains.kotlin.jvm' version "1.4.10"
}

java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
// ... and other project settings ...

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    implementation "com.fasterxml.jackson.core:jackson-databind:2.10.0"
    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.0"
    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.0"
    // ... and the rest of our app's implementation dependencies

    testImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"
    testImplementation "org.junit.jupiter:junit-jupiter-params:5.4.2"
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.2"
    testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.4.2"
    // ... and the rest of our app's test dependencies
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

// ... and the rest of our build rules

Given those changes, we can rerun our build, and see that …

  1. the build still works!

We can also resync the Gradle project in IntelliJ, and after waiting a while for IntelliJ to do some indexing, we can run our tests and programs within the IDE.

Our tests still pass, so we haven’t broken anything, but neither have we proved that we can use Kotlin in our project. Let’s test that by writing a hello world program in the root package of our Java source tree:

fun main() {
    println("hello, world")
}

We can run that within the IDE by clicking on the little green arrow in the left hand margin next to “fun main()”.

And we can run our build, and then run it from the command line with the java command. Compiling the source file named HelloWorld.kt creates a Java class file named HelloWorldKt. We’ll look into how Kotlin source gets translated into Java class files in more detail later, but for now, we can run our program with the java command like so:

$ java -cp build/classes/kotlin/main HelloWorldKt
hello, world

It lives!

Let’s delete HelloWorld.kt — it’s done its job — commit and push.

We now have the option to use Kotlin in our project - the first part of this chapter gives some pointers to where to start using it.

Conclusions

Adding Kotlin to a Java build is straightforward, even if we expect the instructions here to be out of date for most of the life of this book.

Devising a strategy for moving code from Java to Kotlin is more complicated and context-specific. Individual projects should examine where Java is and isn’t working for them, and where using Kotlin would alleviate problems and improve the code. In Chapter 3, we’ll look at what is involved in converting an existing Java class to Kotlin.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.238.161.165