Blog

Kotlin Gems : Features I Wish I Discovered Sooner

02 Sep, 2024
Xebia Background Header Wave

Ever heard of “Delegation” or “Value Classes” in Kotlin? If not, you’re missing out on some powerful features that can make your development smoother and more efficient.

Throughout my career, I’ve made and reviewed thousands of pull requests. While the code is often clean and functional, I’ve noticed it could be even better if developers were more familiar with some of Kotlin’s advanced language features and libraries.

Many developers transitioning from Java (or other JVM-based languages) to Kotlin follow a natural progression—one that I, too, have experienced. However, there are features, syntactic sugars, and conventions that often go unnoticed.

I’ve been compiling a growing list of these overlooked aspects, and I’ve decided it’s time to share it. I’ll first mention the items applicable anywhere in your codebase, from the easiest to apply to the most expensive. Then, I’ll cover a couple of items that are exclusive to test code.

Variables and Immutability

I find it delightful to emphasize the importance of immutability. Whenever possible (almost everywhere), avoid mutable variables to promote thread safety, predictability, and clear intent.

val, var dilemma

I’ve often seen developers choose var when they could have used val.
While variables are one of the first concepts we learn in programming, the idea of constants or values (val in Kotlin) is often overlooked.
Avoid using var as much as possible. and use val instead as it brings immutability with no cost.

Data Classes

As of Java 17 (originally in version 14), you have the luxury of declaring records. But in the Kotlin world, this concept was introduced in the initial phases of the language’s design. Data Classes reduce boilerplate code (or injecting dependencies like Lombok) and make your instances with the following functionalities:

  • equals()/.hashCode() pair. This assures you it compares the content when using the equals sign or function instead of reference comparison.
  • toString() of the form Person(name=John, age=42).
  • copy(), One of my favorites as it promotes immutability since you can copy a specific object and only assign the properties needed.
val jack = Person(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

jack.toString() // Person(name=Jack, age=1)
olderJack.toString() // Person(name=Jack, age=2)

Data Class Considerations

It is important to understand that the functionalities mentioned above only consider the properties defined on the constructor of the data class.
This means the following code might behave against the expectations of some people:

data class Person(val name: String, val age: Int) {
    var mood = "normal"
}

val jack = Person(name = "Jack", age = 1)
val upsetJack = jack.copy()
upsetJack.mood = "upset"

jack.toString() // DOES NOT serialize the 'mood' property
jack == upsetJack // computed to TRUE

Immutable Collections

In Kotlin, collections such as List are immutable by default. This means they don’t provide functions such as add or remove, and when you need to add or remove items from them, you need to create new collections.

This is quite handy since it promotes immutability, which leads to better predictability and clear intent.
However, sometimes your algorithm and logic force you to work with mutable collections.
In such cases, you can use the mutable version of the collection.
An example is MutableList, which does provide functionalities to modify the list.

Inline Value Classes

When a property has specific meaning, it’s better to reflect this fact in your code, whether you’re following Domain-Driven Design (DDD) principles or not. Inline Value Classes in Kotlin can help you achieve this.

Why Not Use Simple Primitive Types?

Primitive types work, but they don’t restrict invalid values. It’s better to use types that enforce business rules and convey meaning.

Why Not “Wrap It Inside a Class”?

Wrapping a value inside a class introduces runtime overhead, such as additional heap allocations. More critically, you lose runtime optimizations, especially if the value is a primitive type.

Why Not Use “Type Alias”?

The main issue with type aliases is that they remain assignment-compatible with their underlying type.
Additionally, type aliases do not allow you to implement domain-related logic, such as validation during construction or conversion.

Inline Value Classes in Practice

Let’s say you’re building a CMS for a college where student IDs are properties of several entities. The IDs follow this format:

[major_code{2}][freshman_year{2}][random_number{3}]

You can now define the following value class:

@JvmInline
value class StudentId(val id: Int) {
  init {
      require(isValid(id)) {
          "Student Id '$id' is not valid"
      }
  }

  companion object {
      private fun isValid(id: Int) = id in 1000000L..9999999
  }
}

data class Student (
  val id: StudentId,
  val name: String,
)

Inline Value Class Considerations

Usage in Java

If you define a value class in Kotlin and want to use it in your Java code, you’ll need to apply the @JvmName annotation to disable name mangling.
Name mangling is Kotlin’s mechanism for ensuring the JVM can differentiate between the value class and its underlying type, allowing for function overloading and other features.

Spring, JPA, and many other Java frameworks and libraries, process Kotlin’s compiled code. By default, the names of properties are not the exact names you assign; they include a name-hashcode (e.g. {... "id-9ycq1": 1234567 ...} instead of {... "id": 1234567 ...}) due to the name mangling of the get function.
This means you might run into some issues when working with Inline Value Classes. Namely:

Jackson

Jackson is a widely used Object Mapper in many Java frameworks such as Spring. This Java library might not work perfectly with Inline Value Classes without the required configuration or dependencies.
The problem, of course, comes from name mangling, as by default, the names of properties of value class types have an additional hash value.
This issue can be mitigated by adding the jackson-module-kotlin dependency.
Your application would work fine after adding this dependency, but you must employ the Jackson Object Mapper from the Kotlin module com.fasterxml.jackson.module.kotlin.jacksonObjectMapper instead of com.fasterxml.jackson.databind.ObjectMapper.

The Gson library doesn’t have this caveat.

JPA and Hibernate

Be careful when using value classes in JPA entities.
It should work by default unless you need to introduce a nullable property of type Value Class.
In this case, you need to ensure that you introduce a converter for the nullable type.
For example, consider the following piece of code:

@Entity
data class Student (
  @Id
  val id: Long,
  @Column
  val studentId: StudentId?,
  @Column
  val name: String,
)

In which StudentId is a value class. This runs into an error that looks like the following:

...
org.hibernate.type.descriptor.java.spi.JdbcTypeRecommendationException: Could not determine recommended JdbcType for Java type '....StudentId'
...

The solution would be to introduce a @Converter:

@Entity
data class Student (
  @Id
  val id: Long,
  @Column
  @Convert(converter = NullableStudentIdConverter::class)
  val studentId: StudentId?,
  @Column
  val name: String,
)

@Converter
class NullableStudentIdConverter: AttributeConverter<StudentId?, Int?> {
  override fun convertToDatabaseColumn(studentId: StudentId?) = StudentId?.id
  override fun convertToEntityAttribute(id: Int?) = id?.let { StudentId(it) }
}
Swagger OpenApi Documentation

As Swagger is implemented in Java, in your auto-generated documentation, you might encounter the mangled version of the names of the functions having a Value Class as an input.
This happens for the operationId in the schema specification, which can be addressed by overriding the operationId from the @Operation annotation whenever needed.

There would be more items in this list, but the rule of thumb would be to:

  1. Make sure you use the Kotlin-friendly modules whenever provided.
  2. Keep an eye on the name mangling mechanism and its side effects.

Better Practices in Scope Functions

The practice of chaining gained popularity with the introduction of Java Streams. In Kotlin, you can use chaining and scope functions to create distinct code blocks, improving readability and reducing the risk of certain human errors. However, sometimes we aren’t fully familiar with all of these functions and their potential.

There’s almost no way to overuse scope functions, so if a task can be accomplished using them, it’s usually a good idea to do so.

.let and .run

These two scope functions are well-known, yet they are often underutilized.
A common pattern where they are frequently overlooked is when a function has multiple exit points.
Although having multiple exit points in a function is sometimes unavoidable, it increases the cognitive load on the reader, who must trace through the code to ensure all scenarios are covered.
Using .let or .run can help streamline this logic. Consider the following example:

fun calculateAge(student: Student?): Period? {  
    if (student == null) return null  
    return LocalDate.now().until(student.dateOfBirth)  
}

Versus when let is employed:

fun calculateAgeWithLet(student: Student?) = student?.let {  
    LocalDate.now().until(it.dateOfBirth)  
}

Or alternatively when run is employed:

fun calculateAgeWithLet(student: Student?) = student?.run {  
    LocalDate.now().until(this.dateOfBirth)  
}

I find the latter versions much more concise and readable.

.also

Sometimes you need to perform an auxiliary task, such as logging or persisting an audit log, right before your code block ends. While you could save the return value, perform the task, and then return the value, it’s much easier to use the .also function. This extension function allows you to execute the task without affecting the return value, making your code cleaner and more efficient.

fun calculateAgeWithLet(student: Student?): Period? = student?.let {  
    LocalDate.now().until(it.dateOfBirth)  
}?.also { writeToLog("Student '$student' is '$it' old.") }

.apply

apply is an extension function that can reassign variables of an object and return the modified version.
It is highly used for object configuration.
For example:

val mapper = jacksonObjectMapper().apply {
    val module = KotlinModule()
    module.registerSerializer(Student::class, CustomStudentSerializer())
    registerModule(module)
  }
mapper.writeValueAsString(student)

In the example above, the mapper object is first created, and then the configuration is applied to it in its specific block. In this block of code, the reference object this refers to the instance of jacksonObjectMapper.

Scope Function Considerations

Nesting

Nesting scope functions is generally discouraged. When you nest these functions, the it or this references may change meaning within different nested blocks, which reduces readability and increases the risk of bugs due to human error.

Be Consistent

Chaining run with let or similar can be confusing due to different reference usages (this vs. it). Stick to one style for better readability.

Non-Extension run

There are two versions of the run function in Kotlin. One is a non-extension function, meaning it can be used at the beginning of a block without requiring an object reference:

  run {
    // some work gets done here.
    this // might not exist or refer to the same object as outside of the block.
  }

The other run function is .run, which is an extension function that uses this as the reference within its block.

Notes on Null Safety Operators

The Elvis Operator

The Elvis operator ?: is a powerful tool for handling null conditions in your code.
It improves readability by making your code more concise while ensuring that operations or functions return a meaningful non-null value.
Consider the following assignment:

val dateOfBirth: Period = if(student != null) student.dateOfBirth else throw IllegalStateException("some meaningful error message here")

This can be rewritten as:

val dateOfBirth = student?.dateOfBirth ?: throw IllegalStateException("some meaningful error message here")

Which is more readable, concise and to the point.

The !! Operator in Your Main Code

Apologies to the NPE11-lovers, but I recommend banning the use of this operator in your main code (not in tests, as test code is supposed to be completely deterministic).
Instead, use the Elvis operator or other methods to handle nullability.
The reason is that you should control the nullability of your object/value at the point of declaration.
If that’s not an option, return or throw meaningful custom messages when encountering null states.
The !! operator attempts to ensure that the optional value is non-null, but if it isn’t, it throws the infamous NullPointerException.
By that time, it’s too late—you should have handled nullability before reaching this line of code!

Safe Casts

In Kotlin, you can use the reserved word is to check whether an object is of a type or not.
You can also use the as reserved word to cast an object to a specific type if the type is a supertype.
For example:

val person: Person? = if (student is Person) student as Person else null

You can rewrite the code above to:

val person: Person? = student as? Person

I find the latter version much more readable and, crucially, less vulnerable to human errors as you are not repeating the object and type over and over.

Delegations

One of Kotlin’s powerful features that I wish I had discovered sooner is delegation, achieved using the by keyword.

Delegation is useful in many scenarios. For example, if you’re using Spring JPA (Java Persistence API) and define a JpaRepository, the interface, typically implemented by a JPA provider like Hibernate, already covers many of your needs. However, suppose you want to add custom functions to this repository. In Kotlin, interfaces can implement their own functions, unlike in Java.

The challenge is that the JPA repository expects function names to correspond meaningfully to the underlying entity and its properties, and it tries to implement them automatically. This can lead to compile-time errors. You have at least two options:

  1. Add a Component: Create a separate component that internally calls the necessary functions from the repository. However, this approach requires adding an extra line of code every time you need to call a new function.

  2. Use Delegation: Create a component that inherits the repository interface and delegates the implementation of the JpaRepository to Hibernate. With Kotlin’s delegation feature, you can build a Spring component that provides all the functionality of the repository interface while adding new functionality. Here’s an example:

@Repository  
interface StudentRepository : JpaRepository<Student, Int>

@Component  
class StudentExtendedRepository(jpaRepo: StudentRepository) : StudentRepository by jpaRepo {  
  fun sayHello() = findAll().map { "Hello ${it.name}" }  
}
...
// This works:
studentExtendedRepository.sayHello()
// So does this:
studentExtendedRepository.findAll()

Property Delegation

Delegations alone and especially Property Delegation deserve its own article as it has lots of variants and use cases.
I highly recommend reading the official documents about it.

Here, I would like to mention a couple of the property delegations that I find quite interesting.

Observable Properties

You can add the observable standard delegate to log changes on a property or apply necessary operations after a change is applied.
If you need to intercept the new value and override it before assignment, you should use the vetoable standard delegate instead.

class User {
    var name: String by observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first" // prints <no name> -> first
    user.name = "second" // prints first -> second
}

Delegating to Another Property

Here I would say no more but to point you to the code example from the official documents.
The code is so simple and comes with well-named values, I think it would be better to leave the explanation to Kotlin instead of English.

var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt

    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

I find delegation to another property in the same class very useful in cases where you are marking a specific property as deprecated but still need to hold onto it for a while.
Your codebase doesn’t need to take care of the deprecated property; rather, you can simply indicate that it reads the value from the new one.

Delegation Considerations

Be Mindful of Who Implements What

When transitioning from traditional inheritance, it’s easy to fall into a common trap:

assuming that when your derived class or interface overrides a val or fun, other functions in the base class will automatically use the overridden version.

However, with delegation, non-overridden functions in the base class continue to use their original implementation, as they are independent objects.
This can lead to unexpected behavior, so it’s quite important to understand how delegation works in this context. To better understand this, consider the following example:

interface Base {
  val message: String
  fun print() { println(message) }
}

class Derived(b: Base) : Base by b {
  override val message = "Derived"
}

fun main() {
  val b = object : Base {
    override val message = "Base"
  }
  val derived = Derived(b)
  derived.print() // prints > Base
  println(derived.message) // prints > Derived
}

Forever Now!

Check out the Kotest extensions, and I guarantee you’ll find at least one interesting tool you can use in your project.

One standout is the Instant extension, which replaces the now static functions in java.time to make your tests more predictable.

Here’s an example from the Kotest extensions documentation:

val foreverNow = LocalDateTime.now()

withConstantNow(foreverNow) {
  LocalDateTime.now() shouldBe foreverNow
  delay(10) // Code is taking a small amount of time to execute, but now changed!
  LocalDateTime.now() shouldBe foreverNow
}

Forever Now Considerations

Race Conditions

This extension is highly sensitive to race conditions because it overrides the static now method, which is global to the entire JVM instance. If you’re running tests in parallel while using this extension, the results may be inconsistent.

Race Conditions Again

Be aware that time-dependent race conditions in your code might behave very differently when time doesn’t progress as expected. Additionally, if your logic requires different now values during its lifecycle, this extension may not be the best choice.

Opening Packages with --add-opens

When using this extension in Kotlin, you might encounter an error like the following:

 ...
 Request processing failed:
 java.lang.reflect.InaccessibleObjectException: Unable to make private static int java.time.OffsetDateTime.compareInstant(java.time.OffsetDateTime,java.time.OffsetDateTime) accessible: module java.base does not "opens java.time" to unnamed module
 ...

This error indicates that the extension cannot replace the now functionality because the java.time package isn’t open. The simplest way to resolve this is to make the package open using Java arguments. For example, in Gradle Kotlin, you can add:

tasks {  
    test {  
        jvmArgs(  
            "--add-opens",  
            "java.base/java.time=ALL-UNNAMED",  
        )  
    }  
}

JSON Assertions

One of the coolest matcher modules in the Kotest framework is the JSON Matcher. When writing API or integration tests, you often need to ensure that a JSON string’s contents are accurate. This set of matchers provides powerful tools to verify that an entire JSON string, or just a portion of it, matches an expected JSON structure. Two particularly useful matchers are:

  • shouldEqualJson: Verifies that a string matches a given JSON structure.
  • shouldEqualSpecifiedJson: Verifies that a string matches a given JSON structure but allows additional unspecified properties.

There is more!

The list could go on, but I want to name some of the features briefly as they are quite interesting to look at. I won’t attempt to cover the entire topics here. They do deserve their own dedicated articles.

  • Coroutines and Reactive Programming in Kotlin.
  • Ktor: Create asynchronous client and server applications.
  • Kotlin Native: Make native executables for Windows/Linux/macOS instead of using JVMs.
  • Dokka: An API Documentation Engine for Kotlin.
  • Inline functions: Eliminate the overhead to call functions by inlining lambda expressions and more!

Conclusion

Kotlin offers a wealth of powerful features that can greatly enhance your coding experience from the importance of immutability, and inline value classes to the powerful delegation mechanisms and scope functions.

I think by adopting these techniques, you can write cleaner, more efficient, and more maintainable code.
Don’t be afraid to dive into these tools and integrate them into your projects.
You might be surprised at how much they can improve both your code quality and productivity.

Keep exploring, and happy coding!


  1. NullPointerException 

M. Masood Masaeli
Masood Masaeli is a software consultant at Xebia Netherlands As a team member and consultant, he likes to promote professionalism together with understanding and kindness in teams and organizations.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts