arrow
Back to blog

The First Release of Kotlin 1.4 or How Kotlin Is Heading Its Way to a 'Better Java'

clock

8 min read

Kotlin is often referred to as a Java substitute. Meanwhile, Google defines it as the top-class language for Android development and offers Kotlin courses for programmers. With such a reputation, it’s hardly surprising that at Dashdevs, it’s also the language-of-choice for most Android applications.

To be sure that we provide our clients with best-in-class services and cutting-edge tools, we continuously keep abreast of new developments and releases. On the 23rd of March, JetBrains introduced the first preliminary release of Kotlin 1.4, namely Kotlin 1.4-M1, and our Android app development team is ready to share its insights into the main innovations of the 1.4-M1 version.

What’s important, you can already configure Kotlin Plugin Updates to Preview 1.4.x in IntelliJ IDEA or Android Studio. So, let’s start.

1. Language improvements

1.1 New Kotlin compiler

Even during the Kotlin Conf in December 2019, it was clear that the new release was aimed at improving the quality and speed of work. Kotlin developers at JetBrains believe that Kotlin 1.4 should focus on “quality and performance” rather than new “big features.”

One of the ways to achieve that is to significantly rewrite the Kotlin compiler and so ensure a tangible increase in productivity that will be notable even along with the simultaneous growth of a large number of Kotlin-based projects. It’s worth mentioning that the implementation of the renewed compiler will be performed over a period of several years. And only some parts of the updated solution will appear in the 1.4 version.

However, if you’ve already worked with the new type inference algorithm (PSA), you might have noticed the changes, since it’s a part of the new compiler.

Finally, the approach to all other innovations will remain unchanged, and that is both versions will be available for some time, literally the old and the new one in a pilot mode.

1.2 Compilation speed-up

The speed of code execution has increased significantly compared to previous versions, and this is only according to the preliminary tests. We expect that the release version will demonstrate even a higher increase, but productivity gains will vary depending on a platform. For instance, for the front-end part, the performance increased four times.

Compilation can be seen as a pipeline that accepts source files and transforms them into executable code step-by-step to ensure smooth operation in the background. Meanwhile, the so-called “front-end compiler interface” can be referred to as the first big step in the implementation of such a pipeline since it analyzes the code, performs type checking, and more. This component of the compiler also works inside the IDE when it detects errors, makes a transition to disposal, or searches for references within your project. The solution isn’t complete yet, and we believe that it will get most of the attention during future releases.

1.3 New library format

KLib — a new library distribution format — will help Kotlin language achieve its cross-platform goals since it allows you to create multiplatform libraries. KLib libraries will contain serialized IR code added as a dependency, so the compiler will find it and generate executable code for the desired platform. So far, this feature is included in Kotlin version 1.4 as a pilot, and the full release is expected in future versions.

2. New language features

One of the key updates that should be mentioned here is the use of the improved type inference algorithm in the Kotlin 1.4 version. You might have tried the alpha version in the 1.3 release, but now it is used by default.

2.1 SAM for Kotlin classes

The SAM conversion allows you to pass a lambda when an interface with a “single abstract method” is expected. Previously, you could use SAM conversions only while working with Java abstract classes and interfaces, but now you can apply them to Kotlin interfaces and methods as well.

Kotlin now supports a SAM conversion for interfaces. However, we should note that it works differently than in Java since you need to explicitly mark the functional interface by adding the keyword fun before the interface name. After that, you can pass the lambda as an argument whenever such an interface is expected as a parameter.

fun interface Action {
    fun run()
}

fun runAction(a: Action) = a.run()

fun main() {
    runAction {
        println("Hello")
    }
}

Initially, Kotlin was able to perform SAM conversions for Java interfaces, but it didn’t always work correctly, thus preventing software engineers from comfortable working with existing Java libraries. In fact, if you called a Java method that took two SAM interfaces as parameters, both arguments must have been either lambdas or regular objects. It was impossible to pass one argument as a lambda and another as an object. Fortunately, the new algorithm solves this issue.

2.2 Automatic type inference in more use cases

Currently, the new algorithm is able to infer types where the old type inference algorithm required you to define them explicitly. To demonstrate the feature, we’re using the following example, where the type of the lambda parameter it is properly inferred to String?.

val rulesMap: Map Boolean> = mapOf(
    "weak" to { it != null },
    "medium" to { !it.isNullOrBlank() },
    "strong" to { it != null && "^[a-zA-Z0-9]+$".toRegex().matches(it) }
)

If we compare the latest Kotlin version 1.4 with 1.3, then with the latter, you’ve had to provide an explicit lambda parameter or replace it with a Pair constructor with explicit arguments.

2.3 Callable references with smart casts

The previous version of Kotlin (1.3) didn’t allow you to access a reference during the “smart casting” of a type. However, after the release of the 1.4-M1 version, you can:

sealed class Animal
class Cat : Animal() {
    fun meow() {
        println("meow")
    }
}

class Dog : Animal() {
    fun woof() {
        println("woof")
    }
}

fun perform(animal: Animal) {
    val kFunction: KFunction<*> = when (animal) {
        is Cat -> animal::meow
        is Dog -> animal::woof
    }
    kFunction.call()
}

fun main() {
    perform(Cat())
}

You can utilize various member references animal :: meow and animal :: woof as soon as the animal variable has been smart cast to a definite type of Cat or Dog. After checking the types, you can get access to member references corresponding to the subtypes.

2.4 Improved inference for function references

With the release of Kotlin 1.4, the use of function references with default argument values becomes more convenient. For example, a reference to the function foo can be applied as accepting a single Int argument type or accepting no arguments at all:

fun foo(i: Int = 0): String = "$i!"

fun apply1(func: () -> String): String = func()
fun apply2(func: (Int) -> String): String = func(42)

fun main() {
    println(apply1(::foo))
    println(apply2(::foo))
}

2.5 Combining positional and named arguments

In general, Kotlin programming language used to forbid mixing arguments with explicit names (“named”) and ordinary ones without names (“positional”) unless you placed named arguments solely after all the positional ones. Unfortunately, too often, such an approach turned into a problem for our Android development team. For instance, when all arguments remained in their positions, but you needed to specify a name for one argument in the middle.

The 1.4 version is expected to fix this problem and make Android development with Kotlin more convenient, so now you can write like this:

fun f(a: Int, b: Int, c: Int) {}

fun main() {
    f(1, b = 2, 3)

2.6 Support of a trailing comma in Kotlin compiler

Kotlin compiler now allows you to leave a trailing comma after a constructor, function, lambda parameters, and many other places where it used to be forbidden. This small syntactic change should prove to be incredibly convenient and important.

class FirstClass (
   val foo: Int
)

class SecondClass (
   val foo: Int,
)

We have two simple classes, and each of them is declaring a property inside the constructor. The property in SecondClass comes with a trailing comma. It looks weird, doesn’t it? Well, let’s check what happens when we complement these classes with more properties:

class FirstClass (
-   val foo: Int
+   val foo: Int,
+   val foo2: String
)

class SecondClass (
   val foo: Int,
+   val foo2: String
)

See how concise the changes in the second diff look. When we added the property to FirstClass, we had to add a comma at the end of the existing property. Consequently, the version control system marked the line as changed. This is inconvenient since our goal was only to add a property and not to make changes to the existing code.

The SecondClass example does a much better job: since the existing property declaration already included a trailing comma, we didn’t need to change any lines of code to declare a new property.

2.7 Optimized delegated properties

The type of delegated property wasn’t taken into account when parsing the delegate expression, following the keyword “by”. For example, the following code hasn’t compiled before, but now the compiler defines the parameter types “old” and “new” as String?:

import kotlin.properties.Delegates

fun main() {
    var prop: String? by Delegates.observable(null) { _, old, new ->
        println("$old → $new")
    }
    prop = "abc"
    prop = "xyz"
}

3. Kotlin library. What’s underway?

Kotlin core libraries work on all platforms. This includes kotlin-stdlib that handles all major types and collections, as well as kotlinx.coroutines, kotlinx.serialization, and kotlinx.io. However, what is still not implemented is date support. During the Kotlin Conf, it was noted that DateTime support is essential for the multiplatform world, and this is something that is already in the pipeline. Meanwhile, durations have already been added to the stdlib, DateTime support is expected in future releases.

Another important addition to the Kotlin libraries is Flow, which represents a Reactive Streams implementation based on coroutines. Except for ergonomics, it also offers additional speed. For instance, some tests prove that Flow can double the speed of existing popular Reactive Streams implementations.

Final Thoughts

As you can see, the new version doesn’t bring any fundamental changes or significant innovations. In most cases, these are bug fixes and the implementation of features requested by the community. This time, the Kotlin development team places its stake on quality and productivity improvement, showing that Kotlin language is maturing. Nevertheless, they also promise more tangible changes in future releases.

When it comes to some coming plans, the 1.4 version will include at least one more release, namely 1.4-M2. With it, the team will cover several top-of-mind challenges, so let’s take a quick look at those updates:

  • Complement the common library with missing functionality to enhance Kotlin multiplatform capabilities;
  • Add new extension functions for arrays to ensure a more consistent experience while working with various container types, streamlining the Android apps development process for complex software products;
  • Expand the collections API with new functions to cover a larger number of real-time cases.

Though the full 1.4-M2 release is not officially introduced yet, its early version is already available in the Playground, so you can try out enhanced Kotlin programming right now. You can also find out more about the differences between Kotlin vs Java from one of our previous articles.

Share article

Table of contents