Profinit Academy combines the benefits of traditional training with the best of e-learning. It is a combination of watching training videos, self-study from in-house materials, and practical exercises and consultations with a supervisor. The Academy focuses on a specific group of technologies (e.g. Big Data, AWS, etc.), and Kotlin Academy is a comprehensive body of knowledge that guides the participant through the core areas of the Kotlin language.
Kotlin Academy is designed for all levels of knowledge. It will be especially appreciated by junior colleagues who are just starting out with Kotlin. By completing the academy, one will have a sufficient level of knowledge to fully participate in a project written in Kotlin.
The Academy is structured into several areas according to difficulty. The individual chapters contain an overview of the topics to be covered. The topics are based on official Kotlin documentation supplemented with personal observations or recommendations. For more complex topics, there are multiple references looking at the subject from different perspectives. At the end of most sections, there is a link to Kotlin Koans, where participants can test their newly acquired knowledge with a series of simple examples.
In the following paragraphs, we’ll cover a few key concepts of Kotlin and how it compares to Java to give you a better idea of how the language works and how it can enhance your development career.
What’s under the hood
As many of you know, Kotlin compiles to bytecode and runs on top of jvm, just like Java. Java developers have an understanding of how demanding individual operations are and what happens in the background. This may not be the case with Kotlin at first glance. Let’s take a look at what some Kotlin constructs look like when translated into Java.
Intellij IDEA offers the possibility to decompile from Kotlin to Java using the menu
Tools -> Kotlin -> Show Kotlin bytecode -> Decompile
Static function
The Kotlin language has abandoned the static modifier known from Java. Instead, it offers other ways to work with variables and functions without creating instances. One option is using a companion object.
class Foo { companion object { fun debug(message: String) { print("Received: $message") } } }
When used in Kotlin it behaves as expected, and the debug method can be accessed by calling Foo.debug(). In Java, it is not so nice, and if we really needed to call this method in Java, we would have to access the debug function via Foo.Companion.debug(), see the result below after decompiling from Kotlin to Java.
public class Foo { public static final Foo.Companion Companion = new Foo.Companion(null); public static final class Companion { public final void debug(@NotNull String message) { Intrinsics.checkNotNullParameter(message, "message"); String var2 = "Received: " + message; boolean var3 = false; System.out.print(var2); } private Companion() { } public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } }
The solution to the optimal call from Java is to add the @JvmStatic annotation. This generates a static method in Java, which is then used as Foo.debug().
public final class Foo { public static final Foo.Companion Companion = new Foo.Companion(null); @JvmStatic public static final void debug(@NotNull String message) { Companion.debug(message); } }
In the isolated world of Kotlin, you don’t have to deal with this. However, if your code written in Kotlin could be used in a Java call, it is a good idea to keep similar annotations in mind.
When
The when function in Kotlin is widely used, but not everyone knows how it really behaves. Can you guess what would be printed to the console if we called the following function with argument 15?
fun printPrimes(num: Int) { when { num % 5 == 0 -> print("Divisible by 5") num % 3 == 0 -> print("Divisible by 3") num % 2 == 0 -> print("Divisible by 2") } }
The correct answer is only “Divisible by 5”. Depending on the version of Java you decompile this into, you will get either a switch function or an if else tree. In both cases, only the first occurrence is looked for, and the others are ignored.
public final void printPrimes(int num) { String var2; if (num % 5 == 0) { var2 = "Divisible by 5"; System.out.print(var2); } else if (num % 3 == 0) { var2 = "Divisible by 3"; System.out.print(var2); } else if (num % 2 == 0) { var2 = "Divisible by 2"; System.out.print(var2); } }
Streams vs Iterable
Since Java 8, programmers are used to using streams for all operations on collections. In Kotlin, this is even easier because the standard library provides all the functions that streams in Java provide directly over collections.
val values = listOf(1, 2, 3) values.filter { it > 1 }.map { it.toString() }.first()
The behaviour of this expression is such that a new list of values is returned at each step. Each subsequent operation is always performed over the entire list again. If we look at what this looks like in Java, it’s a bit frightening.
List values = CollectionsKt.listOf(new Integer[]{1, 2, 3}); Iterable $this$map$iv = values; Collection destination$iv$iv = new ArrayList(); Iterator var7 = $this$map$iv.iterator(); Object item$iv$iv; int it; while(var7.hasNext()) { item$iv$iv = var7.next(); it = ((Number)item$iv$iv).intValue(); if (it > 1) { destination$iv$iv.add(item$iv$iv); } } $this$map$iv = (List)destination$iv$iv; destination$iv$iv = new ArrayList(CollectionsKt.collectionSizeOrDefault($this$map$iv, 10)); var7 = $this$map$iv.iterator(); while(var7.hasNext()) { item$iv$iv = var7.next(); it = ((Number)item$iv$iv).intValue(); String var12 = String.valueOf(it); destination$iv$iv.add(var12); } CollectionsKt.first((List)destination$iv$iv);
For collections that either have many elements or are subject to multiple operations, it is recommended to use sequences. A sequence in Kotlin is equivalent to streams in Java, where the operation is performed optimally with respect to the terminating condition.
All you need to do extra is convert your list to a sequence. Sequences are referred to as lazy and each operation is decorated with another sequence containing that operation. The result is computed only when the terminating operation is determined.
val values = listOf(1, 2, 3) values.asSequence().filter { it > 1 }.map { it.toString() }.first()
List values = CollectionsKt.listOf(new Integer[]{1, 2, 3}); SequencesKt.first(SequencesKt.map(SequencesKt.filter(CollectionsKt.asSequence(values), (Function1)null.INSTANCE), (Function1)null.INSTANCE));
The future in the world of Java
Kotlin earned its place in the world of serious programming languages in 2017 when it was named the official language for Android app development by Google. In 2019, Kotlin even replaced Java as the preferred programming language for the Android platform.
I first encountered Kotlin on a project at CSOB in 2019, and I’m still working on the same project. The Kotlin language enchanted me at first sight with its lightness and elegance, which I didn’t know in the Java world until then. The Kotlin language offers new ways to think about problems and their solutions. The developer is much more likely to find themselves in a situation where the same thing can be written in more than one way, and it is then entirely up to the developer to justify their solution. Of course, this can also be seen as a negative. If a team of developers is not coordinated, it is easy to get into a situation where each member does what they want and the result is unreadable ballast. Each team should set development guidelines to follow. Within these guidelines, each developer is then free to implement as he or she pleases.
So far, Kotlin’s future looks promising. New versions are constantly being released that fix bugs found or add new features. Kotlin developers should be on the lookout for the upcoming Kotlin 2.0 release, which will introduce Kotlin’s K2 compiler. This will cut the Kotlin code compiling time by up to one half and enable the development of new functionalities that, up to now, were not compatible with the old compiler.
A look at Google trends comparing both languages worldwide over the last 5 years shows Kotlin holding its position. Nevertheless, this is just a graph of Google search instances, and may not indicate anything about the actual number of active users.
Java aims for advancement with each of new its versions, coming up with something new each time. In the latest versions, Java brought us new developments that developers working in Kotlin are already well familiar with. From version 17, Java includes an improved version of the switch function or sealed classes. From version 19 onward, Java has offered the novelty of virtual threads, which compete with Kotlin’s coroutines. However, virtual threads are still in an experimental phase and their use is not yet established across the entire community. We’ll see what the new LTS version of Java 21 brings in September.
Author: Lukáš Rubeš