DEV Community

Cover image for From Java to Kotlin in 20 minutes ⚡️
Jean-Michel 🕵🏻‍♂️ Fayard
Jean-Michel 🕵🏻‍♂️ Fayard

Posted on • Edited on • Originally published at jmfayard.dev

From Java to Kotlin in 20 minutes ⚡️

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

Kata: the observed PIN

https://www.codewars.com/kata/5263c6999e0f40dee200059d/train/java

Alright, detective, one of our colleagues successfully observed our target person, Robby the robber. We followed him to a secret warehouse, where we assume to find all the stolen stuff. The door to this warehouse is secured by an electronic combination lock. Unfortunately our spy isn't sure about the PIN he saw, when Robby entered it.

The keypad has the following layout:

┌───┬───┬───┐
│ 1 │ 2 │ 3 │
├───┼───┼───┤
│ 4 │ 5 │ 6 │
├───┼───┼───┤
│ 7 │ 8 │ 9 │
└───┼───┼───┘
    │ 0 │
    └───┘

He noted the PIN 1357, but he also said, it is possible that each of the digits he saw could actually be another adjacent digit (horizontally or vertically, but not diagonally). E.g. instead of the 1 it could also be the 2 or 4. And instead of the 5 it could also be the…

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;
Enter fullscreen mode Exit fullscreen mode

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"),
Enter fullscreen mode Exit fullscreen mode
  • 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()
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
  • Sarah : Thanks!
  • Sarah : commit

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)
     }
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
abhinav1217 profile image
Abhinav Kulshreshtha • Edited

Amazing explanation on how to convert legacy java projects to kotlin. I have in past converted small spring project into kotlin using intelliJ and have followed most of steps similar to your approach, But now I have a better understanding of sequence of steps I should have taken. Thank you for all the link to commits too. I wouldn't have understand few stuffs if you hadn't provided that.

I also thank you for link to Stevey's article. I enjoyed reading it too.

Can you explain a bit more about List.fold(), I read kotlin documentation on it and then tried going through that commit but have no clue what happened there. There were so many checks in before code, how are they handled by fold(), Its trippy.

Collapse
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard • Edited

Thanks a lot for your comment, this article was fun to write but I had no idea if it would be useful.
Fold() can be used for example to reimplement the sum of elements in a list. You give it an initial value (0), and a function that you apply to an accumulator (your sum so far) and each next element

fun main() {
    val ints = listOf(1, 4, 6, 8, 13)
    val sum = ints.fold(0) { sum, elem -> sum + elem }
    println(sum) // 32
}
Enter fullscreen mode Exit fullscreen mode

See pl.kotl.in/5WMtht8FA

Before we treated what happened if you had 0, 1 or more than 1 element. Instead we could handle the case with 0 elements or more than 0. Next step was to use fold().

Collapse
 
abhinav1217 profile image
Abhinav Kulshreshtha

Ok, So isEmpty check is replaced by having an initial value as parameter of fold. case for 1 or more is handled by lambda. I think I understand it now. So those check-steps are not needed anymore.

acc will be initialized as empty setOf string because that is what is passed inside fold(), c will be current iterated value. and then you call combineSolutions with acc and mapPin of c.

Man lambdas are trippy.

Thread Thread
 
jmfayard profile image
Jean-Michel 🕵🏻‍♂️ Fayard

Exactly 👏🏻