Kotlin titles itself as “a modern programming language that makes developers happier.” As utopian as it might sound, there must be something to this slogan. After all, Kotlin has become the second most popular language on the JVM (Snyk, 2020)!

Tempted by the growing interest in Kotlin and, naturally, the promise of happiness, we couldn’t wait to try it out.

But there are more reasons for this decision — the profile of Evojam also gave us a push towards the programming language. The company’s motto is “Keep Growing,” and what encourages growth more than learning something new?

At Evojam, there are both Java and TypeScript specialists. Kotlin connects these two worlds. Java developers can learn a lot from TypeScript experts, for example, on the topic of destructuring.

And so we introduced Kotlin for a commercial project the moment such an opportunity presented itself.

It's been a month since we made a switch from Java to Kotlin. In this article, we want to share with you our insights and conclusions based on this time.

I invited my fellow developers, so they can put their own perspective on using Kotlin.

Evojam-software-frontend-backend-developers.png

How Kotlin Works With Frameworks and Tools

Lukasz: Kotlin works well with the Spring Framework — there's virtually no difference between Java and Kotlin here. Other Java frameworks such as Micronaut also try to support Kotlin. This makes it simple to rapidly introduce the language into a project as you can use familiar toolkits.

Magdalena: Speaking of seamless integration, IntelliJ IDEA even suggests changing the language when you copy Java code to Kotlin class.

How Kotlin Handles Nullability

Lukasz: The syntax of Kotlin clearly shows that Java code is susceptible to nulls. While migrating a class from Java to Kotlin — which is one of the functions of IntelliJ IDEA — I suddenly realized that the NullPointerException (NPE) lurks around every corner.

Krzysiek: I've also noticed that Kotlin takes null safety more seriously. The programming language uses the safe navigation operator (?.) or optional chaining, as TypeScript developers tend to call it. Such a solution helps prevent potential errors when the given value becomes null and we try to recall its method.

searchText?.takeIf { it.isNotEmpty() }

In practice, it seems less complicated than wrapping the object in the Optional class, which is common for Java.

There's also the elvis operator (?:), which helps indicate an alternative value, for example, when the condition isn't met or the field doesn't exist.

val status = dto.status ?: this.status

Magdalena: On top of that, the way in which Kotlin handles the possible nullability of some objects makes you aware of their commonness. You can come across null at basically every step!

It's not the case for Java — everything can be null there, but it's not that visible. You have to remember about it yourself and constantly check if an object is null to avoid NullPointerException.

Let's look at two examples.

1. Java:

class Car {
       Insurance insurance;
       Car(Insurance insurance) {
           this.insurance = insurance;
       }
       public Insurance getInsurance() {
           return insurance;
       }
   }
   class Insurance {
       String number;
       Insurance(String number) {
           this.number = number;
       }
       public String getNumber() {
           return number;
       }
   }
   void foo() {
       Car car1 = new Car(new Insurance("FOO333"));
       String number1 = car1.getInsurance().getNumber(); //number1 = FOO333
       Car car2 = new Car(null);
       String number2 = car2.getInsurance().getNumber(); //number2 will throw NullPointerException
   }

2. Kotlin:

class Car(val insurance: Insurance?)
   class Insurance(val number: String)
   fun foo() {
       val car1 = Car(Insurance("FOO33"))
       val number1 = car1.insurance?.number //number1 = FOO333
       val car2 = Car(null)
       val number2 = car2.insurance?.number //number2 = null
   }

Kotlin is the clear winner in the null safety category.

Kotlin Function Basic Syntax

Krzysiek: If you've ever had a chance to write frontend code, especially the immutable one, you must grasp the importance of the ES6 destructuring assignment in JavaScript. Ever since I found out that such syntax exists in Kotlin, I've set my heart on trying it out.

Unfortunately, it turned out to be quite a disappointment. Destructuring in Kotlin is seriously limited.

First, while destructuring, you have to keep an eye on the order of arguments. You can say that Kotlin warns you when you make a mistake — as depicted in the second screenshot below — but it's a small consolation.

Second, you can't provide a default value in case the one from destructuring doesn't exist.

data class Article(val id: Int, val name: String)
val firstArticle = Article(1, "Me")
val (id, name) = firstArticle
kotlin-function-syntax.png
kotlin-function-syntax.png

Jakub: But there are also advantages like extension functions.

Application-specific convenience functions can make the lives of your team members better. Kotlin encourages additions to third-party classes without altering their bytecode. You can inject methods and properties using extension functions. Let's have a look at an example:

class CustomFilters(private val yearMin: Int?,          // (1)
                    private val yearMax: Int?) {        // (2)

    fun Query.include(criteria: Criteria?): Query {     // (5)
        criteria?.let { addCriteria(it) }
        return this
    }

    fun filter(query: Query) {
        query.include(yearMinCriteria())
             .include(yearMaxCriteria())
    }

    fun yearMinCriteria(): Criteria? {...}              // (3)

    fun yearMaxCriteria(): Criteria? {...}              // (4)
}

We have recently implemented a mechanism for list filtering this way. If you look at lines (1) and (2), the class has two optional integer fields. The filter() method then extends any database query to include min and max year conditions. If either value is empty, we want to skip the corresponding condition.

Methods in lines (3) and (4) can produce empty Criteria. This happens when a filter is not set. We then want the third-party Query class from the MongoDB driver to skip them. We also want to be able to chain our include() calls. This leads us to a custom extension to the Query class, which you can see in the (5) line.

It is very easy to write in Kotlin. The code we end up with is neat and easy to reason about.

Lukasz: Precisely! I'd also say that Kotlin uses syntax that makes applying HOF (a higher-order function) easy. You can declare functions outside of an object or classes. You can also substitute Java's Supplier, Consumer, and other interfaces for a simple and more intuitive () -> {}

if (Objects.nonNull(this.comments) && this.comments!!.isNotEmpty()) {
   this.comments!!.forEach(commentView(cdnSecret, user, this.vin))
}
        fun commentView(cdnSecret: String?, user: User?, vin: String?): (Comment) -> Unit {
            return { c: Comment ->
                if (c.attachmentLinks != null) {
                    c.attachments = c.attachmentLinks!!.stream()
                            .map { AttachmentLink.view(cdnSecret, user, vin).apply(it) }
                            .toList()
                    c.attachmentLinks = null
                }
            }
        }

Magdalena: To be fair, Kotlin is similar to Java in this sense — you can use the same trick in Java 11:

Optional<Opportunity> retryContact(LocalDate date) {
        return statusChange(date, Status.RETRY_CONTACT, () -> retry, x -> this.retry = x);
    }

But Kotlin definitely comes in handy when you use enum classes with values. It's less complicated than in Java:

enum class Status(val active: Boolean) {
        OK(true),
        NOT_OK(false),
    }

Switching from Java to Kotlin makes your life easier in yet another way — you don't need to name the lambda parameter. You can simply refer to it as... it.

return service.findOne(uuid)
                ?.let { it -> service.map(it, user) }
                ?.let { ok(it) }
                ?: ResponseEntity.notFound().build()
return service.findOne(uuid)
                ?.let { service.map(it, user) }
                ?.let { ok(it) }
                ?: ResponseEntity.notFound().build()

Immutability in Kotlin

Krzysiek: At Evojam, the immutability of the data is an important aspect of our development. When I started working with Kotlin, I immediately noticed the copy method — you can call it a tribute to immutability. As you probably guess, it creates a copy of the object on which you call it without copying the reference. It can be used on any data classes — simple classes whose main function is to store data.

    data class Article(val id: Int)
    val firstArticle = Article(1)
    val secondArticle = firstArticle.copy()

This method allows you to easily modify the newly created object:

val secondArticleWithProperId = secondArticle.copy(id = 2)

Lukasz: My conclusions are similar — Kotlin handles immutability like a pro. Instead of mutating the variables, you can use the data class and the copy method.

    val status = statusChange.status ?: this.status
    val actionDate = statusChange.actionDate ?: this.actionDate
    val history = updateHistory(status, actionDate, user) ?: this.statusHistory
    return purchaseWithStatus.copy(actionDate = actionDate, statusHistory = history)

Magdalena: Agreed. In addition, the visual representation makes it easier to read the code and see what is changing.

How Kotlin Works with Strings

Krzysiek: Being a frontend developer, I set great store by string interpolation. Java is not my ally in this matter. Good thing Kotlin joined the battle at my side — coming with a deep understanding of how I want to interact with it and code.

    val a = 10
    val b = 15
    val c = "Result is ${a + b}"

It's so quick and simple compared to formatting a text string in Java!

    val d = String.format("Result is %d", a + b)

How Kotlin Works With Collections

Magdalena: Switch from Java to Kotlin showed me that collections in these two languages differ.

In Kotlin, there's associateBy, which converts a List to a Map, instead of

stream.collect(Collectors.toMap(p -> p.id, p->p))
    val all = repository.findAll().associateBy( {it.car.vin}, {it} )

Package Scope

Lukasz: I feel like it's time to dive into the disadvantages of Kotlin. One of them is package scope — or, to be exact, the lack of such! If you don't think it's a big deal, see Jakub Nabrdalik's presentation — Keep IT clean: mid-sized building blocks and hexagonal architecture.

Static Methods

Lukasz: There's one more issue with Kotlin — using static methods is not as easy as in Java. You need to create a companion object or an object, which results in more code lines.

What is Kotlin better at than Java?

Lukasz: Functional programming is much easier in Kotlin.

Magdalena: I agree with Lukasz. And it gives more awareness about nullability.

Krzysiek: Just like Lukasz and Magdalena said — Kotlin is more FP-oriented than Java. Also, it implements patterns that are widely known from modern languages and it does it quickly. Java, maybe because of its popularity, welcomes such innovations way slower.

Jakub: I think that the question should be "how can Java be better than Kotlin?" I'd say if your development team is not skilled enough, Java’s most basic syntax may be useful.

What’s the biggest difference between Java and Kotlin?

Lukasz: Handling nullability!

Magdalena: I won't be original — handling nullability!

Jakub: Built-in null handling, similar to java.util.Optional and no need for Lombok.

Krzysiek: For me, it's working with collection types.

When is it worth switching from Java to Kotlin?

Jakub: When the team’s fluent in Java and works around the limitations of its syntax!

This article was written in collaboration with Jakub Jarzynski (Backend Developer), Krzysiek Antecki (Frontend Developer), and Magdalena Welik (Backend Developer).