What is the experience like as a Java developer to start programming in Kotlin?

I didn't remember, it was years ago for me!

Fortunately a mob-programming session with my colleagues gave me the chance to see again things with a beginner's mind.

Story time!

Show me the code!

To follow along, checkout the code.

You need to have IntelliJ Community Edition installed. It's free!

On MacOS for example, that's $ brew install intellij-idea-ce

The code is here, and you can see all the changes described below in this pull-request

But first some context

Mob-programming

My colleagues Sarah and Peter and I were doing in a session of Mob programming

The goal was to solve the kata of The observed PIN, where an unreliable spy tells that he saw the PIN 1357 being used, but actually, he's not quite sure, each digit could be instead one of its neighbor on the keyboard layout. It could be 1357 but also for example 2357 or 1368.

The project was a Java project built with Maven. It contains two files: PinGuesser.java and PinGuesserTest.java . It compiles and run the unit tests in a matter of seconds, not minutes like in many Android apps. That makes for a better developer experience IMHO.

We were using IntelliJ's Code With Me to share the code.

We were doing well and had solved the Kata in Java, then had refactored it to a satisfactory state.

  • Sarah : Is there anything else we could improve?
  • Peter : I don't know, looks good to me.
  • Me : Well, we have 20 minutes left, why not rewriting the whole thing in Kotlin?
  • Sarah : Oh, I've heard about Kotlin but haven't had the chance to use it yet. 20 minutes though, do you think we can do it?
  • Me : Let's get started and see where it leads us!

Tools > Kotlin > Configure Kotlin in project

  • Peter : Ok, so I have never done any Kotlin in my life, tell me what to do.
  • Me : There is a command IntelliJ called Convert Java File to Kotlin File . It's a great starting point!
  • Peter : Let's give it a try.

https://user-images.githubusercontent.com/459464/118158571-42ef6000-b41c-11eb-89df-c32de3ffe8f0.png

https://user-images.githubusercontent.com/459464/118158602-4d115e80-b41c-11eb-8cb6-ee85143251ae.png

  • Peter : IntelliJ tells me that Kotlin is not configured, that makes sense.
  • Peter : How do I configure Kotlin in Maven?
  • Me : I don't know, I always used Gradle.
  • Me : Just let IntelliJ do it!
  • Me : By the way, what it will do is the same thing as Tools > Kotlin > Configure Kotlin in project
  • Peter : Let's do it
  • Peter : It seems to have worked. There are updates to the file pom.xml
  • Peter : first commit

Tell Java that

@ParametersAreNonnullByDefault

  • Me : Before we try the Java to Kotlin converter, there is something we want to take are of.
  • Me : As you know, Kotlin has integrated nullability in the type system while Java by default has not.
  • Me : Therefore the converter is going to allow nulls everywhere, which is technically correct but not what you want.
  • Sarah : But there are annotations in Java to say if something is nullable or not, right?
  • Me : Exactly! And the one we want is to tell by default everything is non-null. Conveniently, it's exactly how it works in Kotlin too.
diff --git a/pom.xml b/pom.xml
     <dependencies>
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+            <version>3.0.2</version>
+        </dependency>

+++ b/src/main/java/pin/package-info.java
@@ -0,0 +1,4 @@
+@ParametersAreNonnullByDefault
+package pin;
+
+import javax.annotation.ParametersAreNonnullByDefault;

PinGuesser:

Convert Java File to Kotlin File

  • Peter : I guess I now open PinGuesser.java and just relaunch the converter
    Convert Java File to Kotlin File
  • Me : Correct
  • Peter : It seems that... it worked? There is a file PinGuesser.kt
  • Me : How do you know it worked, though?
  • Sarah : You should run the unit tests
  • Peter : Right

https://user-images.githubusercontent.com/459464/117936889-aaff5280-b305-11eb-9c84-be7205e9673c.png

  • Peter : It's still all green. Amazing, I have written my first Kotlin code ever, and it is bug-free!
  • Sarah : Good job!
  • Peter : What about the tests? Shouldn't we convert those too?
  • Me : You don't need to. Java and Kotlin can co-exist peacefully in the same codebase.
  • Sarah : Ok, but it looks fun, I want to try it out too!
  • Peter : First let me commit

PinGuesserTest:

Convert Java File to Kotlin File and manual fixes

  • Sarah : So I open PinGuesserTest.java and run the command. How is it called?
  • Peter : Convert Java File to Kotlin File
  • Sarah : Let's go!
  • Sarah : I now have a PinGuesserTest.kt . It has some errors though

https://user-images.githubusercontent.com/459464/117937632-80fa6000-b306-11eb-931b-7642e4aac07a.png

  • Peter : Maybe apply the suggestion to optimize imports?
  • Sarah : Ok.
  • Sarah : It worked.
  • Me : as you see it's not perfect, but it's an awesome learning tool: you start with what you already know (in Java) and see it converted in what you want to learn (in Kotlin)
  • Sarah : Let me run the unit tests
  • Sarah : I have some weird JUnit errors

https://user-images.githubusercontent.com/459464/118160523-b3977c00-b41e-11eb-8151-a4dea10aa9e2.png

  • Me : Ok, so I understand that. Java has static methods while Kotlin has the concept of a companion object { ... }
  • Me : Its methods look like static methods but are a bit different. Here JUnit really wants static methods, and we need an annotation to make it happy
-        fun testSingleDigitParameters(): Stream<Arguments> {
+        @JvmStatic fun testSingleDigitParameters(): Stream<Arguments> {
             return Stream.of(
                 Arguments.of("1", java.util.Set.of("1", "2", "4")),
                 Arguments.of("2", java.util.Set.of("1", "2", "3", "5")),
@@ -61,7 +58,7 @@ internal class PinGuesserTest {
             )
         }
 
-        fun invalidParams(): Stream<Arguments> {
+        @JvmStatic  fun invalidParams(): Stream<Arguments> {
             return Stream.of(
                 Arguments.of("   "),
                 Arguments.of("A"),
  • Sarah : Unit tests now work!
  • Sarah : The project is now 100% in Kotlin
  • Sarah : commit

Use the Kotlin standard library

  • Peter : What comes next?
  • Me : It's possible to create List , Set and Map the traditional Java way, but the Kotlin standard library contains plenty of small utilities to streamline that, that would be my first change. Let me do it:

https://user-images.githubusercontent.com/459464/118299183-b6f33c00-b4e0-11eb-9458-c8322d65cae9.png

  • Me : that looks better. Are the unit tests still green?
  • Me : They are, let's commit

Replace stream() API with Kotlin stdlib

  • Me : Something else contained in the Kotlin Standard Library are functions found in the functional programming languages like .map() , .filter() , .flatmap() and much more.
  • Sarah : A bit like the Java Stream API that we are using?
  • Me : Yes, like this but less verbose and more performant under the hood!
-    fun combineSolutions(pins1: Set<String>, pins2: Set<String>): Set<String> {
-        return pins1.stream()
-            .flatMap { pin1: String ->
-                pins2
-                    .stream()
-                    .map { pin2: String -> pin1 + pin2 }
-            .collect(Collectors.toSet())
-    }

+    fun combineSolutions(pins1: Set<String>, pins2: Set<String>): Set<String> =
+        pins1.flatMap { pin1 ->
+            pins2.map { pin2 ->
+                "$pin1$pin2"
+             }
+        }.toSet()
  • Sarah : Unit tests are still green.
  • Sarah : commit

Make val, not var

  • Me : Next, in idiomatic Kotlin style, we tend to use val property instead of var property most of the time.
  • Peter : What's the difference?
  • Me : val property is read-only, it has no setter, it's like a final field in Java
  • Peter : I see. So, I just change the var property with a val?
  • Me : Pretty much so.
  • Peter : Easy enough
  • Peter : commit

Fail fast

  • Sarah : Is there an idiomatic way to validate the parameters of a function?
  • Sarah : The PIN should be something like 7294 with all characters being digits
  • Me : Yes, you use require(condition) { "error message" } for that
  • Sarah : How would that look here?
fun getPINs(observedPin: String): Set<String> {
    require(observedPin.all { it in '0'..'9' }) { "PIN $observedPin is invalid" }
    // rest goes here
}

Functional style

  • Sarah : What comes next?
  • Me : I would like to liberate the functions
  • Peter : What do you mean?
  • Me : Look, we have this PinGuesser class, but what it is doing exactly?
  • Me : It's doing nothing, it's a dumb namespace.
  • Me : It's a noun that prevents us for accessing directly the verbs who are doing the real work.
  • Me : One of my favorite programming language of all time is Execution in the kingdom of nouns by Steve Yegge.
  • Sarah : I know that rant, pure genius!
  • Sarah : How do we free up the verbs/functions?
  • Me : We remove the class and use top-level functions
diff --git a/src/main/java/pin/PinGuesser.kt b/src/main/java/pin/PinGuesser.kt
index 17a20b3..38e457c 100644
--- a/src/main/java/pin/PinGuesser.kt
+++ b/src/main/java/pin/PinGuesser.kt
@@ -1,9 +1,5 @@
 package pin
 
-import java.util.stream.Collectors
-
-class PinGuesser {
-    companion object {
         val mapPins = mapOf(
             "1" to setOf("1", "2", "4"),
             "2" to setOf("1", "2", "3", "5"),
@@ -16,7 +12,6 @@ class PinGuesser {
             "9" to setOf("6", "8", "9"),
             "0" to setOf("0", "8"),
         )
-    }
 
     fun getPINs(observedPin: String): Set<String> {
         for (c in observedPin.toCharArray()) {
@@ -38,5 +33,4 @@ class PinGuesser {
             pins2.map { pin2 ->
                 "$pin1$pin2"
             }
-        }.toSet()
-}


--- a/src/test/java/PinGuesserTest.kt
+++ b/src/test/java/PinGuesserTest.kt
class PinGuesserTest {
-    val pinGuesser = PinGuesser()
 
     @ParameterizedTest
     @MethodSource("testSingleDigitParameters")
     fun testSingleDigit(observedPin: String?, expected: Set<String?>?) {
-        val actual = pinGuesser.getPINs(observedPin!!)
+        val actual = getPINs(observedPin!!)
         Assertions.assertEquals(expected, actual)
     }

List.fold()

  • Peter : Can we go a step back? What does it bring us to make the code nicer like this? At the end of the day, the customer doesn't care.
  • Me : Well, I don't know you, but often I don't really understand the code I'm supposed to work on. I tend to work hard to simplify it and at some point it fits in my head and the solution becomes obvious.
  • Peter : What would it looks like here?
  • Me : Now that the code is in a nice functional idiomatic Kotlin, I realize that the program can be solved using a single functional construct: List.fold()
  • Sarah : Show me the code
  • Me : commit
fun getPINs(observedPin: String): Set<String> {
    require(observedPin.all { it in mapPins }) { "PIN $observedPin is invalid" }

    return  observedPin.fold(initial = setOf("")) { acc: Set<String>, c: Char ->
        val pinsForChar: Set<String> = mapPins[c]!!
        combineSolutions(acc, pinsForChar)
    }
}

fun combineSolutions(pins1: Set<String>, pins2: Set<String>): Set<String> =
    pins1.flatMap { pin1 ->
        pins2.map { pin2 ->
            "$pin1$pin2"
        }
    }.toSet()

Where do We Go From Here?

I hope that you liked this article.

If you want to get in touch, you are welcome to do so via https://jmfayard.dev/

The code is available at https://github.com/jmfayard/from-java-to-kotlin

Start in the java branch and compare with what is the kotlin branch. See this pull-request

If you are interested to learn more about Kotlin, I've written about it here

This post is also available on DEV.