Kotlin - the better Java? Java Core & JVM Languages

Insights from the other side

Moritz Kammerer
Jul 7, 2022

The Kotlin programming language saw the light of day in February 2016 with version 1.0. In 2019, Google declared Kotlin the "first-class citizen" of Android development, helping it achieve a breakthrough. But what makes Kotlin special? Why is it worth taking a look at even for Java veterans who have nothing to do with Android? This article will cover this and other questions.

First off: I am not a Kotlin fanboy and I think Java is a very successful programming language. My daily work revolves around backend development on the JVM, mainly in Java.  However, I think it’s fine to think outside the box more often and look at newer languages on the JVM. This article assumes that you already “speak” Java, and shows some interesting Kotlin features from this point of view. I will focus on what I consider to be the most important features of Kotlin: functions, variables, null safety, object-oriented programming, functional programming, and interoperability with Java.

Kotlin is developed as open source software by JetBrains under the Apache 2.0 license. JetBrains also distributes the well-known IDE IntelliJ IDEA, which has good support for Kotlin. There are also plug-ins for Eclipse and NetBeans. Kotlin has been under development since 2010, but only gained some popularity in 2016 with version 1.0.

The final breakthrough, at least in Android development, came in 2019 when Google declared Android development “Kotlin-first” at Google I/O.

Kotlin is a modern, statically typed programming language that combines concepts from object-oriented and functional programming. Special attention is paid to compatibility with Java code – this is also one of the differentiating features between Kotlin and Scala. Developers can easily use Java libraries in Kotlin and do not have to convert data types, such as lists, for example. It’s also possible to use Kotlin code from within Java. The Kotlin compiler generates bytecode for the JVM (Java Virtual Machine). So, programs written in Kotlin can be executed with a standard Java installation. The only requirement is a JVM that is at least version 1.6.

STAY TUNED!

Learn more about JAX London

In comparison to Java, Kotlin markets itself as being “concise”, without much boilerplate code. Kotlin code is around 40 percent shorter than comparable Java code and is much more expressive [1]. Kotlin also has an improved type system, which is mainly aimed at avoiding NullPointerExceptions. But let’s take a closer look at these promises.

Hello World, in Kotlin

We start with a classic “Hello World’:

package javamagazine

 fun main(args: Array<String>) {
  print("Hello world")
}

Just like Java, Kotlin code is managed in packages. In the above example, you will immediately notice that it isn’t necessary to create a class to define the main function.

Functions are declared with the keyword fun (short for “function”). For the parameters, first the name of the parameter (args), then its type (Array<String>) is specified. It’s exactly the opposite of Java. Arrays are expressed as generic types and not with square brackets after the type (for example: String[]), as it is in Java.

You don’t need semicolons after statements in the function body. They are optional in Kotlin. The function contains a function call of print(…) without object instance. So, it is possible to call functions without an associated object instance in Kotlin. There are several functions in the standard library that do not belong to any class. Here, print(…) is an abbreviation for System.out.print(…), which probably looks familiar. If you execute this code, “Hello World” appears on the console, as expected.

Type interference and functions

In Listing 1, variables are declared with the keyword val. Kotlin can determine the type of the variable automatically by type inference (in this case Int is the Kotlin equivalent of Java’s int). But if you don’t like this, you can explicitly specify the type separated by a colon: val operand1: Int = 1. Variables defined with val are automatically immutable, similar to final in Java. Further assignments would trigger a compile error. You should make as much as you can immutable: This is an important concept in Kotlin.

Listing 1

fun main(args: Array<String>) {
  val operand1 = 1
  val operand2 = 2
  val sum = operand1 + operand2

  print("$operand1 + $operand2 = $sum")
}

Type inference also works when several variables are used together. In the example, the result type of the + operator determines the type of the variable sum (Int).

In the print(…) statement I used another feature of Kotlin, called string interpolation. It embeds the variables operand1, operand2, and sum into a string. When the program is executed, 1 + 2 = 3 appears on the console.

Listing 2
private fun sum(operand1: Int, operand2: Int = 0): Int = operand1 + operand2

 fun main(args: Array<String>) {
  val result1 = sum(1, 2)
  println("1 + 2 = $result1")
   val result2 = sum(1)
  println("1 + 0 = $result2")
}

The first line in Listing 2 declares a function named sum. The modifier private indicates that the function can only be used in the same file. The return type of the function is also specified, here it is Int. The return type is specified with a colon after the parameter declaration. In the example, the short notation for functions is also used, which doesn’t use curly brackets. It is introduced directly with = after the signature. This notation can be used for all functions consisting of only one line of code. The operand2 parameter is also set to 0 by default, meaning the caller does not have to specify it explicitly. Optional parameters are a feature many Java developers have always wanted.

In the main function, you see the call to sum first with arguments 1 and 2 are explicitly set. The second call to the sum function omits the second argument so that the default value of the parameter (0) is used. When the code runs, the console shows:

1 + 2 = 3
1 + 0 = 1

Null Safety

In my opinion, the best Kotlin feature is the improved null handling. A common error in (Java) programs is the NullPointerException. This happens when a variable contains null, but the developer did not handle the case and accessed the variable anyway. Kotlin prevents this by extending the type system: nullable variables have a different type than variables that cannot be null. This example illustrates this more clearly:

Listing 3
fun formatName(firstName: String, midName: String?, lastName: String): String {
  var result = firstName.toLowerCase()
  result += midName.toLowerCase()
  result += lastName.toLowerCase()

   return result
}

Listing 3 declares a function that takes three parameters: firstName of type String, midName of type String? and lastName of type String. The type String? is not a typo. It’s Kotlin’s syntax for a nullable string. When a type ends with a question mark, it means that null is allowed as a value. If there is no question mark at the end of the type, then null is not a valid value for it.

The variable result was declared with the keyword var – this means that the variable is mutable (the opposite of immutable). 

Listing 3 also contains an error that Kotlin detects, preventing a NullPointerException. If the midName is null, calling the toLowerCase() function will cause a NullPointerException.

Since Kotlin knows that midName can be null (by type String?), the error is detected at compile time and compilation is aborted.

In order to be able to compile the code, you will need the change contained in Listing 4.

Listing 4
fun formatName(firstName: String, midName: String?, lastName: String): String {
  var result = firstName.toLowerCase()
  if (midName != null) {
    result += midName.toLowerCase()
  }
  result += lastName.toLowerCase()

  return result
}

YOU LOVE JAVA?

Explore the Java Core Track

The if now “proves” to Kotlin that the developer has handled the null case, and that the call toLowerCase() function is possible within the if. In the type system, the variable midName in the if block has changed type from String? (nullable) to String (non-nullable). This concept is what Kotlin calls Smart Casts. It can be found in other places, such as Typecasts. The call to the function can now take place like this:

val name = formatName("Moritz", null, "Kammerer")
val name2 = formatName("Moritz", "Matthias", "Kammerer")
val name3 = formatName(null, "Matthias", "Kammerer")

Line 1 sets the midName to null, which is allowed by the type String? Line 2 sets the midName to Matthias. Line 3 does not compile because the type of the first parameter is String – and null is not allowed as a value for String.

Kotlin’s null handling is not just allowed for reference types, but also for primitive types such as int, boolean etc. Unlike Java, Kotlin does not distinguish between primitive and reference types – so it is also possible to call functions on int, boolean functions, etc. (Listing 5).

Listing 5
val count = 1
val enabled = true
val percentage = 0.2

 
println(count.toString())
println(enabled.toString())
println(percentage.toString())

By the way, the types of the variables in the example are Int, Boolean and Double – there are no lowercase variants like int, boolean, or double like there is in Java. If necessary, the compiler takes care of autoboxing.

Now, let’s take a look at what Kotlin has to offer in terms of object orientation.

Object-oriented programming in Kotlin

Listing 6 defines a new class called Square. This class has a property length of type Int, which is immutable. A property is the combination of a field and its access functions. Kotlin automatically generates a getter function and a constructor parameter for length. If you used a var instead of val, then Kotlin would generate a setter function, too. The area() function is also defined, which uses the length property to calculate the area.

Listing 6
class Square(val length: Int) {
  fun area(): Int = length * length
}

Interestingly, this class is automatically public, so a missing modifier means public, and not package protected as it does in Java. Additionally, the class is automatically marked as not inheritable (final in Java). If you do want this, then you can use the keyword open. A caller can now use the class like this:

val square = Square(4) 
val length = square.length
val area = square.area()
println("Length: $length, Area: $area")

Line 1 creates a new instance of the Square class with length = 4. Kotlin doesn’t have a new keyword, which I think is a bit of a shame. I’ve often searched Java code for new to find object instantiations. Of course, an IDE would also help out here. But with code reviews on GitHub or GitLab, for example, you don’t necessarily have that.

Properties are simply accessed using the dot operator, analogous to field access in Java. Under the hood, the getter function is called. Calls to functions look exactly like they do in Java.

Listing 7 shows an inheritance hierarchy: the Square class inherits from the abstract Shape class, which defines an abstract function called area() that is implemented in Square.

Listing 7
abstract class Shape {
  abstract fun area(): Int
}

class Square(val length: Int): Shape() {
  override fun area(): Int = length * length
}

In Kotlin, there is no extends keyword; you use a colon. Additionally, override is a keyword, instead of an annotation as it is in Java. It is also possible to define several public classes in a file and give the file a random name. In some situations, this has its advantages, but it can also lead to disadvantages, like more difficult code navigation. Kotlin’s coding conventions recommend that if the classes semantically belong together and the file does not have too many lines, then it is okay to write the classes in one file [2].

Interestingly, Kotlin does not know the keyword static. So, no static functions can be defined, and instead, the two concepts of singletons and companion objects take their place. A singleton, called an object in Kotlin, is a class that cannot be instantiated and exactly one instance always exists: 

object FixedSquare: Shape() {
  override fun area(): Int = 25
}

Singletons are defined with the keyword object and callers do not need to (and cannot) create instances of them. The function call looks like a call to a static function in Java: 

val area = FixedSquare.area()

Now, If you want to equip a class with a “static” function, it looks like:

class Square(val length: Int): Shape() {
  override fun area(): Int = length * length

  companion object {
    fun create() : Square = Square(5)
  }
}

The static functions of the class, in this case, create(), are in a companion object. The method of calling it looks like static in Java.

val area = Square.create()

Personally, I think the solution with the companion object is a bit strange, and I’m not alone in this [3]. The designers’ justification for this decision is that there are top-level functions and you hardly need static functions anymore. A companion object is a full-fledged object, so it can also implement interfaces and be stored in a reference, for example.

On the other hand, I think the behavior of if, try, etc is solved excellently. These control structures are statements in Java, not expressions. The difference is that a statement is a piece of code that has no return type, while an expression returns a value. How many times have we written code like this in Java:

boolean green = true;
String hex;
if (green) {
  hex = "#00FF00";
} else {
  hex = "#FF0000";
}

The problem here is that in Java an if cannot return a value. Java solves this problem, but only for if, with the ternary operator ? (e.g. String hex = green ? “#00FF00” : “#FF0000”). In Kotlin, this operator is not needed (Listing 8).

Listing 8
val green = true
val hex = if (green) {
  "#00FF00"
} else {
  "#FF0000"
}
// Or shorter:
val hex = if (green) "#00FF00" else "#FF0000"

The concept is not limited to if, it also works with try-catch (Listing 9).

Listing 9
val input = "house"

 val result = try {
  input.toInt()
} catch (e: NumberFormatException) {
  -1
}

Here the function toInt() is called on the string input. If input is not a valid Integer, then the function throws a NumberFormatException. The exception is caught with the enclosing try-catch, which returns -1 in the event of an error. Since try-catch is an expression, it returns a value. So, the value (either the integer value of the string or -1 in case of an error) is assigned to the variable result. I miss this feature a lot in Java. In fact, Java 12 brought something similar with switch expressions. Speaking of exceptions, unlike Java, Kotlin does not have checked exceptions.

 

In Listing 9, the toInt() function is called on a string. This function does not exist in the JDK on the String class. So where does it come from? The solution to our mystery is called extension functions.

Extension Functions

In Kotlin, you can extend functions to any types, even if they are not defined in your own code. 

private fun String.isPalindrome(): Boolean {
  return this.reversed() == this
}

In the above example, the isPalindrome() function is defined on the String type. This function returns true if the string is a palindrome. A palindrome is a word that can be read both from the front and the back, for example: “Anna” or “Otto”.

The string to be checked can be found in the reference this. Unlike Java, in Kotlin, strings (and all other objects) can be compared using the == operator. In Kotlin, the == operator calls equals() and does not compare the references of the objects, as it does in Java. If you need the reference comparison in Kotlin, the === operator exists. The function in the example above is written in the longer notation, with curly braces and an explicit return. Since the function only consists of one line, you could also use the short notation. Now, you can call the extension function as follows:

import javamagazin.isPalindrome

 

val palindrom = "anna".isPalindrome()
println(palindrom)

It looks like String has another function called isPalindrome(). Extension Functions are a useful tool for adding further functions to specified types, making the code more readable. By the way: The caller has to import the extension function explicitly via an import statement in order to use it.

Functional Programming

Functional concepts arrived with the streams in Java 8. Kotlin also allows you to use a functional programming style (Listing 10).

Listing 10
val names = listOf("Moritz", "Sebastian", "Stephan")
val namesWithS = names
  .map { it.toLowerCase() }
  .filter { it.startsWith("s") }
namesWithS.forEach { name -> println(name) }

The first line of Listing 10 creates a new list with three strings. Kotlin infers the type of the list as List<String>.

The map function transforms each element into a string with lowercase letters. Lambdas are specified with braces. If the Lambda receives only one parameter (in this example, the string that is transformed), then the name of the parameter is not necessary. In this case, if you want to access the parameter, you can use the variable named it.

After the map function, the filter function will only select the strings beginning with an S. The result of this whole pipeline is stored in the variable namesWithS. The type of this variable is also List<String>. Unlike Java, the functional operators work directly on lists, sets, etc., and not on streams, which then have to be converted to lists, sets, etc. If you want the same behavior as Java streams, especially the lazy evaluation, then Sequences are available.

In the last line, a Lambda is defined with an explicit parameter name of name. The names are output on the console:

sebastian
stephan

Other functional constructs such as zip, reduce, and foldLeft can also be used with Kotlin via predefined functions (Listing 11).

Listing 11

val firstNames = listOf("Moritz", "Sebastian", "Stephan")
val lastNames = listOf("Kammerer", "Weber", "Schmidt")

firstNames
  .zip(lastNames)
  .forEach { pair -> println(pair.first + " " + pair.second) }

Listing 11 creates a list of type List<Pair<String, String>> from two lists (Type List<String>) using the zip function. Kotlin already brings along a few generic container types such as Pair, Triple, etc. If you prefer to use meaningful names instead of pair.first and pair.second, this is also possible, but it’s a little more work:

data class Person(
  val firstName: String,
  val lastName: String
)

In this example, we use another great Kotlin feature: Data Classes. These classes automatically generate getter (and setter, if needed), hashCode(), equals(), and toString() functions. A very useful copy() function is also created, which can be used to make a copy of the data class and change individual properties. Data Classes are comparable to Records, which found their way into Java 14 [5]. A Data Class can now be used as follows:

firstNames
  .zip(lastNames)
  .map { Person(it.first, it.second) }
  .forEach { person -> println(person.firstName + " " + person.lastName) }

The map function transforms the Pair<String, String> into a Person instance, and in the forEach function, now you can use the meaningful names. Data Classes are useful for more than just functional programming. For example, I use them often for DTOs (Data Transfer Objects), classes that consist only of fields and access functions.

Kotlin designers have also fixed an oversight of Java developers: lists, maps, sets, etc. are all immutable by default in Kotlin, so they have no add-, remove-, set-, etc. functions. Kotlin still shows its pragmatic core here, because there are also mutable lists in the form of MutableList, MutableSet, MutableMap, etc. These mutable variants inherit from their immutable counterpart.

Interoperability with Java

One important point about Kotlin is its cooperation with existing Java code. For example, Kotlin does not introduce its own collection framework, so Java functions that expect lists can also be called with “Kotlin lists”. In general, calling Java code from Kotlin is not a problem. In some places, the compiler performs some magic. For example, the getters and setters of Java classes can be used as properties in Kotlin:

public class Person {
  private String firstName;
  private String lastName;
  private LocalDate birthday;
  // Constructor, Getter and Setter
}

Written in Java, this class is easy to use in Kotlin:

val person = Person("Moritz", "Kammerer", LocalDate.of(1986, 1, 2))

println(
  person.firstName + " " + person.lastName + " was on " +
  person.birthday + " born"
)

The getFirstName() function in Java is available as a firstName property in Kotlin. Types from the Java library, such as LocalDate in the example, are supported, in addition to custom code written in Java. You can even mix Kotlin and Java together in one project. This feature is great because it allows you to convert existing projects to Kotlin little by little (if you want to) instead of one big bang migration. You could also get to know Kotlin a bit by continuing to write your production code in Java, while testing it in Kotlin. IntelliJ also offers an automatic Java-to-Kotlin converter. It doesn’t produce optimal Kotlin code, but it’s quite useful.

The null handling is also very interesting when mixing Kotlin and Java code. Kotlin interprets the various nullability annotations (@Nullable, @NotNull, etc.) of Java. But if these annotations are missing, then Kotlin’s null safety will fail. In this case, Kotlin introduces platform types for Java code, such as String!. The exclamation mark means that Kotlin cannot detect whether the type can be null or not. Then, the Kotlin programmer can pretend that variables of this type are not nullable and should expect NullPointerExceptions at runtime. Let’s look at an example to make this clearer:

public class Person {
  private String firstName;
  private String middleName; // Nullable!
  private String lastName;
  // Constructor, Getter and Setter
}

In this class, defined in Java, middleName is nullable by domain definition. If you use this class in Kotlin, the Kotlin compiler will first look for nullability annotations. Since this class has no such annotations, then the type of firstName, middleName, and lastName is String!

That means that these variables could be null or not. When using this Java class in Kotlin, NullPointerExceptions are possible again:

val person = Person("Moritz", null, "Kammerer")
println(person.middleName.toLowerCase())

The listing above calls the function toLowerCase() on the variable middleName. This code compiles and executes, but at runtime, it throws a NullPointerException:

java.lang.NullPointerException: person.middleName must not be null

The NullPointerException has a meaningful error message and shows exactly what is null. Once again, Kotlin’s pragmatic nature is evident in the platform types. Its designers could have assumed that all types whose nullability is not known to be nullable. But this would have led to lots of (most likely, unnecessary) null checks if you wanted to use Java code in Kotlin. Interoperability with Java would be a lot more difficult. I think the current solution is a suitable trade-off. Many Java libraries and frameworks also already use the nullability annotations, such as Spring. When working with these libraries in Kotlin, you don’t notice that they are written in Java, and null safety is given again.

STAY TUNED!

Learn more about JAX London

Conclusion

I think Kotlin is a great programming language. As a Java developer, you will immediately feel at home. Many problems are solved pragmatically. Kotlin takes care of many of the smaller “problems” that Java has. However, in all fairness, Java either has good workarounds for these “problems” (such as code generation via the IDE or Lombok) or the language developers have already created solutions for them (Switch Expression, Multiline Strings, Records, helpful NullPointerExceptions).

One big advantage of Kotlin that is not found in Java is the null handling. In my opinion, Kotlin created a perfect solution for this, as long as you have pure Kotlin code. If you need Java interoperability, to call your own Java code, or to use one of the many great libraries from the Java environment (like Spring Boot), this also works well. But you have to be careful with the platform types.

As a backend developer, I’ve implemented several projects using Spring Boot and Kotlin, and it’s been a lot of fun. Kotlin is by no means limited to Android development. In Kotlin, you will feel at least as productive as you do in Java. The standard library has many useful little helpers. Granted, the Kotlin compiler is slower than Java’s, but its features make up for it. From a software engineering perspective, the language also does a lot right. It has excellent IDE support and can use the Java-standard build tools Maven and Gradle. However, support for static code analysis tools like SonarQube still leaves a lot to be desired.

Kotlin has a few more great features to offer that I didn’t describe here due to lack of space. If you want to learn more, I recommend the Kotlin Koans [6]. You can learn Kotlin through small problems, without having to leave the browser. 

And to answer the question in the article’s headline: Java is a great programming language and has recently turned on the feature tap properly. However, don’t turn your nose up at Kotlin. It will please users with well-thought-out features oriented towards real-world problems. So: Kotlin or Java? The answer is, as always in software engineering: “It depends”.

Links & Literature

[1] https://kotlinlang.org/docs/faq.html

[2] https://kotlinlang.org/docs/coding-conventions.html

[3] https://discuss.kotlinlang.org/t/what-is-the-advantage-of-companion-object-vs-static-keyword/4034

[4] https://openjdk.java.net/jeps/361

[5] https://openjdk.java.net/jeps/359

[6] https://play.kotlinlang.org/koans/overview

Top Articles About Java Core & JVM Languages

STAY TUNED!

JOIN OUR NEWSLETTER

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Big Data & Machine Learning
Saving, processing & more

JOIN OUR UPCOMING EVENTS IN LONDON!