Kotlin Best Practices for Java Developers
Evolution: we could say that it’s the act of change to fit with the environment. Species evolve to adapt to their environments. For example, dogs evolved from wolves to get easy access to food. It’s something that applies to all species, including human beings. Nowadays, we also apply this concept to all the artificial stuff developed by us. And we are the reason why these things changed; we make them evolve to make our lives easier. The same happens to software languages. Over time, they change. We create new languages based on older ones, and then we replace the older ones. We can see this occurring with Kotlin and Java. We might say that Kotlin is an evolution of Java, a new species. Kotlin was created in 2011 by Jetbrains and has replaced Java as the official programming language for Android. So, it’s not hard to imagine that many Java developers switch to Kotlin out of necessity rather than for pleasure. This is a paradigm shift for Java developers, even though Kotlin takes many characteristics from Java. It has some differences that make our lives easier, which means developers need to evolve. This article is for Java developers who are trying to get into the Kotlin language.
If you, like me, have been a Java developer for many years, you probably have many ingrained habits which make it difficult to switch to a new language. These habits represent an obstacle to completing your evolution as a Kotlin developer. This was the case when I started coding in Kotlin. Just because it looks like Java, doesn’t mean it’s Java. Although it inherits some syntax from Java, it has a lot of new ways to simplify stuff. Here’s a list of recommendations to continue with your Kotlin evolution.
Here we have some examples inspired by my experience and from reviewing other devs’ code. For each example: the first part is an example whose implementation smells like Java, but is in fact valid Kotlin syntax. In the second part, I suggest an alternative for how to reduce the verbosity and improve code readability, while of course respecting Kotlin’s syntax and taking advantage of all its benefits.
Let’s start looking at them one by one:
Avoiding return type
fun sum(a: Int, b: Int): Int {
return a + b;
}
I want to start with this example because it’s the most common case among all developers. Kotlin accepts omitting the `return` clause and removing the brackets. This creates a more explicit function. We are highlighting the importance of the function that is the math operation. We are also simplifying the function like this:
fun sum(a: Int, b: Int): Int = a + b
Reduce the number of returns clauses
fun canYouDrinkBeer(age: Int): String {
if (age < 18) {
return "You can't"
} else if (age < 21) {
return "Depends of State!"
} else {
return "Cheers!"
}
}
We can use the feature to assign a result to a function using the `=` sign. We can combine this with the capacity to return a value based on an `if/else` structure. This gives us a clearer way to handle the same problem.
fun canYouDrinkAlcohol(age: Int): String =
if (age < 18) {
"You can't"
} else if (age < 21) {
"Depends of State!"
} else {
"Cheers!"
}
If you want something simpler, you can improve the above code by replacing the `if/else` with a `when` clause.
fun canYouDrinkAlcohol(age: Int): String =
when (age) {
in 0..18 -> "You can't"
in 18..21 -> "It depends of State!"
else -> "Cheers!"
}
Use more data classes
class Foo {
val a: Int
val b: String
override fun equals(other: Any?): Boolean {
..
}
override fun hashCode(): Int {
..
}
}
We can reduce this code using data classes. When you declare the properties, those will be part of the equals and hashCode functions. You are avoiding a lot of code such as getters and setters, hashCode, equals, and toString functions. Don’t confuse data class with Java record class. They are not the same because data class also works for mutable objects.
data class Foo(
val a: Int,
val b: String
)
Using nullability as structures control
fun findById(id: Int): Value {
val result = repository.findCustomQuery(id)
if (result == null) {
throw Exception("Value not found")
} else return result
}
The nullability is a Kotlin feature that takes you to a higher level. Here, we can take care of null values at compiling time. There is an important character that will help you with that: the wildcard `?`. The following code is doing exactly the same as the code above. Use the single wildcard `?` and combine it with the operator `?:`(Elvis operator) to verify if the result is null.
fun findById(id: Int): Value =
repository.findCustomQuery(id) ?: throw Exception("Value not found")
}
In this case, if the `findCustomQuery`function returns null, the function executes the code that is before the elvis operator.
Use extensions to simplify validations
fun findById(id: Int): Value {
val result = repository.findById(id)
if (result.isPresent) {
return result
} else throw Exception("Value not found")
}
Extensions are one of the most important features for clarity because they can help you to make a function look like it’s part of a class, without actually being part of it. You can create a method and make it part of String class. It gives you the illusion that a function is part of a class that is not part of its source code.
fun Optional.ifPresentThen(): T? = this.orElse(null)
fun findById(id: Int): Value =
repository.findById(id)?.ifPresentThen() ?: throw Exception("Value not found")
Use runCatching to handle exceptions
fun somethingGetsWrong(): Result {
return try {
anErrorOccurs()
Result.Sucess
} catch (ex: Exception) {
Result.Error
}
}
Sometimes, we can use functional programming on a simple task, such as catching an exception and making your code look more functional.
fun somethingGetsWrong(): Result =
runCatching {
anErrorOccurs()
Result.Success
}.getOrElse {
Result.Error
}
There is a limitation with this. There is no final clause equivalence for this.
Reduce the number of variables declared explicitly
fun BufferedImage.rotate(): BufferedImage {
val width = this.width
val height = this.height
val dest = BufferedImage(height, width, this.type)
val graphics2D = dest.createGraphics()
graphics2D.translate((height - width) / 2, (height - width) / 2)
graphics2D.rotate(Math.PI / 2, (height / 2).toDouble(), (width / 2).toDouble())
graphics2D.drawRenderedImage(this, null)
return dest
}
Use scope functions to reduce the number of variables declared in a function.
private fun BufferedImage.rotate(): BufferedImage =
BufferedImage(height, width, type).apply {
this.createGraphics().let { it ->
it.translate((height - width) / 2, (height - width) / 2)
it.rotate(Math.PI / 2, (height / 2).toDouble(), (width / 2).toDouble())
it.drawRenderedImage(this@rotate, null)
}
}
The usage of let, also, apply and run functions is a great way to not declare variables and reduce the number lines. In this example, we are not declaring some references. We are using the scope functions to maintain the value’s reference without declaring the variable.
You can use these recommendations to create more readable code and take advantage of Kotlin’s benefits. Keep in mind that all languages are in constant evolution. It may be that next year or even next month there will be new features that will reduce these examples even further.