DEV Community

Pavol Rajzak
Pavol Rajzak

Posted on

Kotlin

In this year's KotlinConf keynote Andrey Breslav mentioned how pleasantly surprised he was by the results of Google search for "Kotlin love" keywords. Developers love working with it, that's why it ranked 2nd in "Most loved language" category of StackOverflow Developer survey.

The question is: why is that? What makes the language lovable?

It's really hard to pick up the best features of Kotlin, since one can easily end up writing about everything. But it would just mean rewriting their awesome documentation :). The most important is what can actually be used in solving real world issues™.

I was working on a internal time-tracking application. Backend is written in Java and it has a fairly complex logic when it comes to operations with time. For instance, the working day is defined as 8 hours, and anything that's above it is considered an overtime.

Note: This is actually a simplification of this problem, but let's go with it for the sake of this example

Data classes

In Java, it is implemented using various libraries and utility functions on top of complex structures. What I really like about Kotlin, is that it's very expressive, while it stays readable. For instance, if we would like to define the structure for Time, we could write a simple data class

data class Time(val hours: Int, val minutes: Int)
Enter fullscreen mode Exit fullscreen mode

This will give you an immutable data structure, with equals/hashCode/toString/getters and setters (and other) functions implemented out of the box. It's something Java people would call a POJO, but defined in one line.

Operators overloading

The next thing, useful in our scenario would be comparing one Time to other. For instance 1:30 is more than 0:45 and so on. In Java, you can implement Comparable and override compareTo function. The same works in Kotlin, but with one big difference - you are able to compare instances by using equality operators.

data class Time(val hours: Int, val minutes: Int) : Comparable<Time> {
    override fun compareTo(other: Time): Int {
        return when {
            this.hours == other.hours -> this.minutes - other.minutes
            else -> this.hours - other.hours
        }
    }
}

println(Time(1,30) > Time(0,45)) // true
Enter fullscreen mode Exit fullscreen mode

You can also override other operators. For instance we would need the minus operator. You can just add this to Time class:

operator fun minus(other: Time): Time {
    return convertToTime(convertToDouble(this) - convertToDouble(other))
}
Enter fullscreen mode Exit fullscreen mode

and then

println(Time(1,30) - Time(0,45)) // Time(hours=0, minutes=45)
Enter fullscreen mode Exit fullscreen mode

Companion objects

You can see that I've used some utility functions convertToTime and convertToDouble. If you are working with Java, you are used to create your own XYZUtil classes, so instead of creating TimeUtil class, we can use the companion object.

data class Time(val hours: Int, val minutes: Int) : Comparable<Time> {

    companion object {
        @JvmStatic
        fun convertToTime(time: Number): Time {
            // implementation
        }
        @JvmStatic
        fun convertToDouble(time: Time): Double {
            // implementation
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Java interoperability

As you can see, I've annotated the methods with @JvmStatic annotation. Kotlin does not have a static methods, but to provide perfect interoperability with Java, it can make them visible by using this annotation. Thus, you can call this in Java:

public class JavaClass {
    public static void main(String[] args) {
        System.out.println(Time.convertToTime(2.75));
    }
}
Enter fullscreen mode Exit fullscreen mode

You can expose also constants and other fields, that would be visible in Java.

Null safety

Kotlin does not allow you to use null values by default. However, if you for some reason (e.g. Java inter-op) want to allow them, you can define the parameter types with ?. Let's define another constructor in Time that accepts only hours:

data class Time(val hours: Int, val minutes: Int) {
    constructor(hours: Int?) : this(hours ?: 0, 0)
}
Enter fullscreen mode Exit fullscreen mode

We define it using constructor keyword and specifying parameter as Int?. But if this value is null, we would like to provide zero as a default. You can use so called elvis operator ?: which will evaluate the expression and if it is null, it will use the right hand value as a fallback.

Smart casting

In Java, it is very annoying to do the "check-than-cast" routine, where you first check whether object instance is instanceof some class and then manually cast it. Let's look at the Kotlin implementation of convertToTime function defined above:

fun convertToTime(time: Number): Time {
    return when (time) {
        is Double -> {
            val fullHours = Math.floor(time).toInt()
            Time(fullHours, ((time - fullHours) * 60).toInt())
        }
        is Float -> convertToTime(time.toDouble())
        is Int -> Time(time)
        else -> Time(time.toInt())
    }
}
Enter fullscreen mode Exit fullscreen mode

You might not know this, but function Math.floor is expecting Double as a parameter. We are not casting it explicitly, but since we're doing it in on of the branches of when (i.e. switch in Java), it is automatically smart casted to Double. And IDE will highlight this for you, so that you are aware of it! The same goes to call is Int -> Time(time) which is smart casted to Int and uses the constructor defined in the previous section.

Extension functions

We've talked about writing utility functions on top of our newly created classes by using companion objects. But what if we would like to add functionality to existing classes?

For instance, we would like to provide a toString implementation for our Time class, that would print 00:45 instead of the data class default Time(hours=0, minutes=45):

override fun toString() = "${hours.padZeros(2)}:${minutes.padZeros(2)}"
Enter fullscreen mode Exit fullscreen mode

hours and minutes are Int properties of Time class, but function padZeros does not exist in Int class. If you really want to use this across the whole project, you can define an application-wide extension function:

fun Int.padZeros(length: Int) = "${if (this.sign < 0) "-" else ""}${this.absoluteValue.toString().padStart(length, '0')}"
Enter fullscreen mode Exit fullscreen mode

The this in the function above refers to instance on which the function is invoked, so you can use it however you like.

Note: As you see, Kotlin allows powerful String templating using syntax known from other languages: "${someVar}" :)

Ranges

Let's put it all together in a next example. We would like to have a class for Workday, which can tell us when the person started and concluded his day at work. One of the specialties of this class, would be determining, whether person was at work at specific time.

We can achieve this by using a ClosedRange interface, which allows us to use this syntax:

println(Time(10, 30) in Workday(Time(8), Time(16)))
Enter fullscreen mode Exit fullscreen mode

The implementation is pretty straight-forward thanks to conventions used for ClosedRange. It expects that the implementation provides the start and endInclusive properties of specified type (which in our case is Time)

class Workday(override val start: Time, override val endInclusive: Time) : ClosedRange<Time>
Enter fullscreen mode Exit fullscreen mode

One important note is that this works because ClosedRange expects that the type is implementing Comparable interface, which our Time class does.

Higher order functions

Kotlin supports some idioms of functional programming by treating functions as a first-class citizens in language. This means, that you can use them as either parameters, return types or declare them as variables.

For instance, we would like to add functionality which will check the constructor parameters once the class is initialized:

class Workday(override val start: Time, override val endInclusive: Time) : ClosedRange<Time> {

    init {
        // higher order function
        require(isValid()) { "Range `${this.start} to ${this.endInclusive}` does not define a valid workday." }
    }

    fun isValid() = start >= Time(0, 0) && endInclusive <= Time(23, 59)
}
Enter fullscreen mode Exit fullscreen mode

There are several things going on here at once. First, we define a function isValid which validates that start and end times are within a day range. Then, in init block (which is called after constructing instance) a require function is called, which is defined in Kotlin stdlib as this:

public inline fun require(
    value: Boolean,
    lazyMessage: () -> Any
): Unit
Enter fullscreen mode Exit fullscreen mode

In other words, it will evaluate a first parameter, and if it is false it will invoke a second parameter which is a function (declared using lambda expression syntax) that defines the message used in IllegalArgumentException. So this call

Workday(Time(-1), Time(12))
Enter fullscreen mode Exit fullscreen mode

will end up like this:

Exception in thread "main" java.lang.IllegalArgumentException: Range `-01:00 to 12:00` does not define a valid workday.
    at Workday.<init>(WorkingTime.kt:62)
    at WorkingTimeKt.main(WorkingTime.kt:83)
Enter fullscreen mode Exit fullscreen mode

Note: In Kotlin, if the last parameter is a lambda expression, you can write it after the function call (as in our example). But it is the same as require(isValid(), { "Range ${this.start} to ${this.endInclusive} does not define a valid workday." })

Wrapping it up

There are many hidden gems, that you will discover when working with Kotlin. It enables you to write concise and readable code while working with known technologies (e.g. Java, JVM and it's frameworks). If you would like to discover them, you can start by going through Kotlin koans, a set of exercises that go through most of the language features - and you can do it in browser!

Edit: Here's a Gist to a full source code used in this article. ;)

Top comments (0)