[Kotlin] Important Concepts to Get Started
In my opinion Kotlin is very similar to Java and almost feels like Java, but in a easier
and more fun way. The main Java frameworks (e.g.: Spring
, Quarkus
) have out-of-the-box
Kotlin support, enabling a very flat learning curve, since you already know the framework
and is just getting used to the new language syntax.
When I started learning Kotlin I was no beginner to Functional Programming. I already knew
functional constructs (map
, filter
, reduce
) but coming from a Java background, the
first thing that came to mind is “let’s convert it to a stream like we do in Java”. But
that’s far for the optimal Kotlin way of doing it, since it has native support to a lot
of functional constructs, including the basics we already have on Java Streams.
However, Kotlin is fundamentally different from Java in three subtle aspects:
- Functions are first class citizens;
- Immutability: Unless marked as
open
ormutable
, everything is immutable; and - True optionals.
Let’s dig a little deeper on how these differences affect our experience with Kotlin.
Functions are first class citizens
Let’s start with an example. Say you have some user input you need to perform validations. If we’re in Javaland, we would need to create a validator class:
// UserValidator.java
public class UserValidator {
private static int MAX_LENGTH = 20;
/**
* Validates a username.
* Shouldn't be more than 20 characters and no whitespaces.
*/
public static boolean isValidUsername(String name) {
return (name.length() < MAX_LENGTH && !name.contains(" "));
}
}
With Kotlin we just need to have a functions file, not even a class, and that is enough to have one validation function that can be used across all the application:
// UserValidatorFunctions.kt
const val USERNAME_MAX_LENGTH = 20;
fun isValidUsername(name: String): Boolean =
name.length < USERNAME_MAX_LENGTH && !name.contains(" ")
Having the function definition in Kotlin and using it is as simple as importing a function. There’s no need to call a static method inside a class which the sole purpose is to have helper functions, nor there’s a need to instantiate or inject a validator class. We can and SHOULD have helper function as Kotlin files, to be easily imported anywhere we need.
Moreover, testing becomes a lot simpler, since we don’t have classes and states, only a function:
class UserValidatorFunctionsTest {
@Test
fun shouldReturnTrueIfUsernameIsValid() {
val username = "some-valid-user"
assertTrue(isValidUsername(username))
}
@Test
fun shouldReturnFalseIfUsernameHasWhitespaces() {
val username = "some invalid-user"
assertFalse(isValidUsername(username))
}
@Test
fun shouldReturnFalseIfUsernameIsTooBig() {
// Validator has a limit of 20 characters
var username = ""
for (i in 0..(USERNAME_MAX_LENGTH + 1)) {
username += "a"
}
assertFalse(isValidUsername(username))
}
}
There’s even a more interesting use case of Kotlin functions. Remember when
you need to perform operations on Collections in Java? We usually have to
either create a Stream
or convert to stream.
// Calculating the sum of positive numbers smaller than 10
Stream.of(0, -9, -3, null, 2)
.filter(n -> n != null && n > 0 && n < 10 )
.reduce(Integer::sum)
// Or, if you have a list before
List.of(0, -9, -3, null, 2)
.stream()
.filter(n -> n != null && n > 0 && n < 10 )
.reduce(Integer::sum)
Without getting into details around the internals of Java and Java Streams, the overhead of using streams here (intermediary classes, etc) is quite heavy.
On the other hand we can perform those operations natively in Kotlin:
val numbers = listOf(0, -9 , -3, 1, null, 2, 3, 5, -2, 8, null, 11, 9, 20)
// Calculating the sum of positive numbers smaller than 10
print(
numbers
.filterNotNull()
.filter { it in 1..9 }
.reduce { total, next -> total + next }
)
NOTE: The it
in Kotlin refers to the argument of a single argument function.
It is the same as writing filter { n -> n in 1..9 }
.
There are lots to cover about functions in Kotlin, but that’s a topic for a dedicated post.
Immutability
Kotlin supports multiple flavors of immutability. We will go over a few of the most commom cases, with an explanation on how they work and what are the defaults.
Variables and Constants
The most commom being the use of
val
to assign immutable values.
val name = "Iago Lorencini Oliveira"
val favoriteNumber = 9
name = "Can't change it" // This is a compilation error
var favoriteNumber = 3 // This is also a compilation error
In other words, setting a val
signals to the compiler (and also who’s reading)
that the variable is immutable. Same is applied to class attributes:
data class User(
val username: String,
var age: Int // Notice the "var" here
)
val aUser = User("iago", 50)
aUser.age = 20 // No compilation errors, age is "var"
aUser.username = "change-it" // Compilation error, username is "val"
Using val
and var
effectively will hugely improve readability and maintainability
of the code, since it will be a lot more clear and less prone to bugs (such as
accidental state change).
Collections
The second use case of immutability is a bit tricky at first. I’ve spent countless hours trying to figure out why I couldn’t add items to a list on Kotlin, just to feel stupid when finding out that collections can be mutable or immutable.
Let’s break it down using an example:
// The default collection constructs will always return immutable collections
val listOne = listOf(1, 2, 3)
listOne.add(4) // Compilation error, method doesn't exist
// But if we need a mutable list, for example
val listTwo = mutableListOf(1, 2, 3)
listTwo.add(4) // This is fine now, because our list is mutable
The same principles apply to sequences and maps. They default to immutable unless the mutable construct is called.
It may be complicated at first, but having immutability here is extremely good because it prevents unintended changes on our collection state. And we can always explicitly declare mutable collections whenever needed.
data class User(
val username: String,
val roles: MutableList<String> = mutableListOf("User"),
) {
override fun toString(): String = "$username - $roles"
}
val user = User("Iago")
user.roles.add("Staff") // This works since it's a mutable list
print(user) // Prints "Iago - [User, Staff]"
Classes, Methods and Functions
The third case of immutability is around classes and methods. All
classes, functions and methods in Kotlin are defined as final
by default. In order to change that behavior and enable extensibility
we need to declare it as open
. Let’s take a look at the following
example:
open class Car constructor(val make: String, val model: String) {
open fun run(): String = "The $make $model is running"
}
class BrokenCar: Car {
constructor(): super("Broken", "Broken")
override fun run(): String = "This car is broken!!!"
}
val car = Car("Ford", "Focus")
println(car.run()) // The Ford Focus is running
val brokenCar = BrokenCar()
print(brokenCar.run()) // This car is broken!!!
True Optionals
This is one of the most amazing features of Kotlin. If you have experience
with Swift it will feel more or less like it. But Optionals in Kotlin
are built-in, meaning we don’t need to worry about NullPointerException
unless we explicitly allow it. The compiler will scream at you if you use
optionals in an unsafe way and you won’t be able to compile until you fix
your code (or throw the unchecked operator !!
, but PLEASE DON’T).
Let’s start with a small example:
fun sum(x: Int? = null, y: Int? = null): Int {
return if (x == null || y == null) {
0
} else {
x + y
}
}
println(sum()) // 0
println(sum(1)) // 0
println(sum(2, 3)) // 5
println(sum(null, null)) // 0
Let’s break down this function.
First, we have two arguments x
and y
. They are both optional integers (?
)
and they have a default value null
. In other words, we can call this function
passing zero, one or two arguments due to the arguments having defaults.
Also, we have a simple null check on the if
condition, so on the else we don’t
need to unbox the optional because the compiler knows it’s already been checked.
if we were to replace the function on the example with the one bellow the code would
throw a NullPointerException
:
fun sum(x: Int? = null, y: Int? = null): Int = x!! + y!!
println(sum()) // NullPointerException
That’s because the operator !!
unsafely unboxes the optional.
Now let’s explore a more interesting example:
interface UserService {
fun findUser(userId: String): UserDto?
fun fetchUserRoles(userId: String): Collection<UserRoleDto>?
}
@Path("/users")
class AdminUserResource @Inject constructor(val userService: UserService) {
@GET
@Path("/{userId}")
fun getUser(@PathParam("userId") userId: String): FullUserDto? {
val userDto: UserDto = userService.findUser(userId) ?: throw NotFoundException("user.not.found")
val userRoleNamess: List<String> = userService.fetchUserRoles(userId)
.map { it.name } ?: listOf()
return FullUserDto(userDto, userRoleNames)
}
}
In this case we want to throw a NotFoundException
in case we can’t fetch the user,
but we only want to set the role names as an empty list if we can’t get info about
the roles.
There are even other usages, such as building an object that depends on many optional
variables. When using the operator ?:
we will get the default value declared after
the operator if any of the properties in the chain is null. For instance:
data class A(val name: String = "A")
data class B(val name: String = "B", val a: A? = null)
data class C(val name: String = "C", val b: B? = null)
fun main() {
// Classes with references
val a = A()
val b = B(a = a)
val c = C(b = b)
// An orphan C without references to b or a
val c2 = C()
val x1 : String? = c.b?.a?.name
val x2 : String? = c2.b?.a?.name
val x3 : String = c2.b?.a?.name ?: "Don't know"
println(x1) // A
println(x2) // null
println(x3) // Don't know
}
I can tell from experience that it is very rare to find an NPE in production on a service developed with Kotlin. Even when I find one, it’s usually inside a Java class in a dependency somewhere.
Wrapping Up
Java and Kotlin have lots of similarities, but some differences that may be hard to get used to when learning one or the other. Hopefully this post will help you on discovering Kotlin or learning it.
Kotlin is a powerful, modern language and mature language and, the most important,
Kotlin is lots of fun
!
/iago