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!
- Mob-programming
Tools > Kotlin > Configure Kotlin in project
- Tell Java that
@ParametersAreNonnullByDefault
- PinGuesser:
Convert Java File to Kotlin File
- PinGuesserTest:
Convert Java File to Kotlin File
and manual fixes - Use the Kotlin standard library
- Replace stream() API with Kotlin stdlib
- Make val, not var
- Fail fast
- Functional style
- List.fold()
- Where do We Go From Here?
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.
- 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 converterConvert 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
- 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
- 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
- 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
andMap
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:
- 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 ofvar property
most of the time. - Peter : What's the difference?
-
Me :
val property
is read-only, it has no setter, it's like afinal 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
}
- 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)
}
- Me : commit
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
Top comments (4)
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.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
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().
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 callcombineSolutions
with acc and mapPin of c.Man lambdas are trippy.
Exactly 👏🏻