7 things any Java developer should know when starting with Kotlin

Frank Scheffler
Digital Frontiers — Das Blog
9 min readOct 31, 2018
source: JarkkoManty, via pixabay (CC0)

As a long-term developer and architect, I have mainly been focussed on Spring-based Java applications within the last years. This year, I had the chance to start over with Kotlin in one of my customer projects and fell in love with it right from the start. In this post I want to share some of the key aspects I appreciate most in Kotlin as a former Java developer.

Named Parameters

Let’s start with a simple, often underrated, difference between Java and Kotlin: named parameters. Java does not allow us to explicitly name parameters in function calls, relying solely on positional parameters instead. In addition to that, Java does not have a concept for dealing with default parameter values, not even null. Hence, you are always forced to supply all parameters in the correct order, making refactorings cumbersome. While most IDEs nowadays support us by highlighting the names of the underlying parameters, when calling functions from within our code, this cannot fully compensate the downsides.

In Kotlin parameters can either be passed by position or by name, but not both at the same time. When using named parameters, which I strongly suggest, the order of the parameters has no relevance, allowing for much easier refactorings in the long run. Moreover, Kotlin allows default value declaration (including null), which enables callers to omit those parameters, if necessary.

In comparison to other languages, there are no limitations in Kotlin when using mixed style parameter definitions in the function declaration. However, when calling functions with alternating required and optional parameters, using positional parameters enforces us to provide all parameters up to the last required one, that is even those having default values. Accordingly, named parameters are highly preferable in Kotlin, since only required parameters can be supplied.

Immutability

While immutability is essential in (functional) programming to avoid side effects, Java only provides limited support for ensuring immutability during compile time. In fact, marking all fields as final is the only way to ensure an object cannot be modified after construction. Unfortunately, Java Bean style based frameworks, such as JPA, often expect object properties to be mutable via setters. Furthermore, dealing with multiple equally typed properties of an object is cumbersome and error-prone with respect to object construction or duplication, since Java only knows positional parameters in constructors. Finally, Java does not enforce a consistent concept of object equality, leaving it to the developer to implement both equals() and hashCode(), which is why frameworks such as Lombok are commonly in-use.

Code, which isn’t allowed to alter state outside of its scope, is inherently free of side effects

Kotlin introduces first class compiler support for distinguishing between mutable and immutable properties, values, and references:

Values (including references) marked as read-only cannot be altered after they have been assigned, which is why they should always be preferred in favor or mutable state. When combining multiple attributes, Kotlin provides so-called data classes, which support the concept of immutability at the class level. While these classes allow for mutability as well, their built-in equality functions and copy constructors make them and ideal candidate for representing immutable state in an object-oriented manner.

Accordingly, with Kotlin immutable state should be the first (if not only) choice when it comes to defining an object model representing the domain of your application. Modifying the state of existing objects can be restricted to copy constructors, bringing back control to the code really in charge of manipulating state.

Null Safety

When it comes to software quality the “much-loved” NullPointerException (NPE) is something every programmer has to cope with once in a while, although the exception itself is something the JVM actually uses to avoid program abortion by the operating system, which would be inevitable, if we really were to dereference null pointers. This prevention, however, is limited to the runtime of your software, while programmers most would prefer to prevent these kind of issues at compile time already. In fact, the Java language itself provides no means whatsoever to tell the compiler, whether a reference is null safe or not, leave aside simple checks for initial variable assignments along different execution paths in the code. Additional frameworks, such as Java Bean Validation, are often used to compensate for this, for instance at the REST API layer when using Spring MVC and appropriate @NotNull annotations. Still, there is no strict warning or even error, if programmers forget about these add-on validations.

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965.” (Tony Hoare)

With the introduction of java.lang.Optional in Java 8, things slightly improved, as long as potential null-values were properly wrapped as Optional. This is helpful in cases where both null and non-null values may occur, for instance when returning results from database queries, which may or may not be existent. However, it isn’t of much help in cases, where null must be avoided at all costs, e.g. when a method simply cannot deal with null arguments. As the name suggests, the value is simply marked as optional, still leaving it to the consumer to deal with the “null case”.

Kotlin provides null safety at the language level, that is by clearly stating if values can be assigned null or not. Hence, the null safety is enforced by the compiler, reducing the risk of runtime NPEs, as long as pure Kotlin code is involved.

This implies that, as a Kotlin programmer, one has to define which objects may or may not be null, including class properties, function arguments, and return values. This requires extra effort when defining interfaces and object structures, but highly pays off when it comes to the actual implementation. The compiler will prevent dereferencing null values and automatically adds additional null checks into the byte code accessing those functions or objects. Moreover, java.lang.Optional has few or no benefits in Kotlin, because any property has built-in null awareness, unless you explicitly want to express unavailability of values.

The only caveat with null safety is when interacting with the Java platform. Since no information is available about the null safety of values returned from Java methods, Kotlin’s null checks are relaxed and it is up to the programmer to decide, if null should be allowed or not.

Personally, this is the main reason why I strive to avoid mixing Java and Kotlin code too much. Once you are familiar with Kotlin, the business code of your application should be written mainly in Kotlin, leaving the null safety checks to the interaction with 3rd-party APIs, if needed.

String Interpolation

Dealing with Strings in Java has never been a great pleasure: starting from the plus operator, which bears some serious performance penalties, to using StringBuffer and finally the String.format() function. It’s no wonder, one witnesses so many StringUtil(s) classes, when working with Strings in plain Java.

Kotlin greatly simplifies String handling by introducing interpolation, as known from many other programming languages. Expressions can directly be embedded into Strings using ${expr}, where curly braces are optional, if you refer to simple variables. Referring to non-string objects implies that toString() is called automatically, which further reduces boiler-plating in the code.

Interpolation is even more powerful, when it comes to iterations. Since, for and while loops in Kotlin are not expressions, they cannot be used in strings. However, often it is sufficient to iterate over collections, producing partial strings per element, which are then joined into a complete string, as you would do using theStringBuffer. This can be achieved using the Kotlin extension function Iterable<T>.joinToString(), which automatically separates elements using a comma delimiter by default, e.g. as follows:

As can be seen, dealing with Strings in Kotlin is a lot more fun than in standard Java and there is usually no need to use an explicit StringBuffer or other common Util classes. Triple quoted strings (aka raw strings) can be used to avoid unnecessary escaping, and can even be nested when using joinToString().

Useful Standard Library Functions

While functions are the core concept in functional programming, when it comes to object oriented programming languages, they often need to operate on existing objects and classes. Introducing new functionality into existing classes in Java is only possible when actually extending the class. This is not always possible and may be tedious when you want to introduce standard functionality into a whole set of classes.

Kotlin provides so-called extension functions, which may add new functionality to existing classes, even final ones. Kotlin’s standard library contains a very powerful set of standard functions, which are added to any class, by default. The following represents a small excerpt of what I think are the most commonly used ones.

let and also function

Both also and let enable you to operate on a given object using nested scoping with the object passed in as it parameter (which can be assigned a different name, if needed). The difference is that let expects you to return a (usually different) result object, so it can be considered as a transformation function mainly, when chaining function calls. also always returns the original reference, which is fine, when you simply want to operate on the object.

apply function

apply is very similar to also, however, it sends in the object reference as this rather than it . This may be problematic, if you need access to an outside scoped this , in which case also may be preferable. apply is very useful when lots of function calls need to be applied to the very same object, which would require repeating the reference every time, otherwise. Therefore, my preferred use case is AssertJ’s soft assertions, e.g. as follows:

All assertThat statements within the scope of apply automatically apply to the SoftAssertions object, which in turn is returned from the function call, so it can immediately be used to trigger the assertion process using assertAll().

use function

The use function resembles Java’s try-with-resources and can be used on any Closeable with equivalent semantics. It ensures the resource is closed, after leaving the scope. Thus, I/O operations may safely be incorporated into function chains, if needed.

Streams and Collections

Processing object collections has always been a major aspect of any enterprise software. Most of the time this involves translating objects between different representations, when passing them through the typical layers of a software architecture, for instance from the data to the view layer. Historically, mutable array lists (or other collections) and standard for-loops were used in Java to perform these tasks, which contradicts the idea of functional programming without side effects, let alone the aspect of increased memory consumption and garbage collection with increasing collection sizes.

Java 8 introduced streams, which strive to tackle both aspects: first by using lambdas for expressing the transformations in a functional way and secondly by lazily (and optionally in parallel) consuming stream sources, if possible. Unfortunately, the laziness is rarely needed, still bedeviling collection processing, by forcing us to create streams first. Moreover, the built-in stream processing functions are very rudimentary, as they always strive to not break the lazy processing mantra.

Kotlin provides first-class functional transformation support for every type of collection without requiring you to use Java streams, at all. Accordingly, the standard transformation in Kotlin is eager, but using asSequence() one may opt-in for lazy processing, if really needed. The following table lists some of these functions to give you an overview of the superb support built into Kotlin, when it comes to dealing with all kinds of collections:

  • map(), mapNotNull(), mapKeys(), mapValues()
    standard transformations from one representation into another, for lists and maps, respectively
  • first(), last()
    extract the the first or last element matching the given predicate function
  • single()
    extracts the only element matching the given predicate, failing if more elements match
  • associate(), associateBy()
    transformation from list to map entry by associating keys with values
  • mapIndexed(), filterIndexed(), forEachIndexed()
    perform operations using an additional index variable, thus avoiding extra for loops, as in Java
  • plus(), drop(), dropLast()
    add to or remove elements from a list

All these functions, by default, return immutable collections, thus ensuring operations are free of side-effects. toMutableList() and toMutableMap() can be used to obtain mutable copies, if needed.

Enumerations and Sealed Classes

Finally, Kotlin provides us with a superior approach to regular enumerations, which of course are supported as well. While Java enums allow us to define properties, different attributes for different enumeration instances are not supported.

Abstract data types are best defined using sealed classes in Kotlin.

In Kotlin so called sealed classes, can be used instead to build enumerations with flexible sub-classes. Still, the compiler ensures that only those classes directly nested/extended within the sealed class, are considered a valid member of the “enumeration”.

As shown in the example, the sealed class Operation contains variously typed elements, including objects (aka singletons). Using when enables the compiler to check, if an object belongs to the sealed class, including complete branch coverage, if necessary. The is keyword is optional for singletons, because only one instance exists. Further, the implicit smart cast, allows the subtype to be accessed without further ado.

Thanks for reading! Your feedback is welcome. You can get in touch with me on Twitter and GitHub.

--

--