Notes on Kotlin for Java Programmers

Categories: Java

About this Article

I am an experienced Java developer, have done a little Scala development, and am now learning Kotlin. These are some brief notes about the primary similarities and differences between Java, Scala and Kotlin.

This article is not a tutorial on Kotlin; instead it presents Kotlin in comparison to Java.

There is nothing original here; it is mostly taken from the excellent Kotlin reference docs and just summarized. Where the details are complicated, the original reference docs are the best source of info.

Important! There are far better ways to learn Kotlin than reading this page; these notes are really only for my own use. Depending on your learning style, I would recommend:

Reading the language reference suited my style of learning best; it presents the whole Kotlin language feature by feature in a reasonable order. This does assume you’re fluent in similar languages - but so does this article. The “Learn Kotlin” page covers only the highlights, while the Koans approach is very different: learning by doing.

Table of Contents

Overview

There are five major languages that run on the JVM: Java/Scala/Kotlin (statically typed), and Clojure/Groovy (dynamically typed).

Kotlin sits between Java and Scala in complexity; it basically includes the 50% of Scala’s features that bring 90% of its benefits, while leaving out the features that make it difficult to use.

The result is a quite elegant language that is relatively easy to learn for experienced Java developers - and even easier for Scala users. It also compiles fast, and produces small binaries (no bigger than Java, and much smaller than Scala).

Kotlin borrows several features from functional programming languages, and can be used as one (particularly in combination with a library such as Arrow) but there are some aspects of functional languages that it doesn’t support (eg no way to ensure a function is pure). IMO it is fair to say its focus is really on being a better Java.

Language Evolution

Suggestions for modifying the Kotlin language are tracked via KEEP - the Kotlin Evolution and Enhancement Process. This works similarly to Python PEPs.

Kotlin’s compiler and standard libraries are completely open-source. Its development is currently mostly driven by a team from Jetbrains (the makers of the Intellij IDEA IDE) but as Google recommend and support Kotlin as the primary development language for the Android OS, it is certain to be supported long-term even if Jetbrains lose interest.

Installing Kotlin

The easiest way to install is with sdkman, which installs Kotlin at user level.

Most Linux distros also have a Kotlin package in their standard package repositories.

Basics

Compiled Form

Like Java, Kotlin is a strictly-typed compiled language. There are several supported “targets” when compiling:

  • JVM bytecode
  • Android bytecode
  • Javascript
  • WebAssembly
  • Native machinecode (eg x86 or ARM)

When compiling Kotlin code to JVM bytecode, the results are very similar to class-files created from Java source-code and integration between classes generated by Kotlin and Java is very good (much better than Scala/Java). Even a single “module” can consist of a mix of Kotlin and Java code.

When compiled to Javascript, the output can be loaded into a web-browser for execution client-side, or into a server-side engine for a nodejs-like architecture. I don’t know much about this though, and am not sure how popular it is.

When complied to WebAssembly, the output can be run in any compatible browser, or run in a WebAssembly runtime on any platform (similar to a JVM).

Kotlin as Scripting Language

It is possible to use Kotlin code for simple scripting tasks that might otherwise be done in Bash or Perl. Only a single file is supported, it does not have a package-declaration, and by convention it has suffix .kts. Scripts are run with:

  • kotlinc -script {filename} {args}

Directory Structure

Like Java, Kotlin organises code into classes and packages.

Java is very strict about file and directory layout; each top-level class must be in a file whose name matches the classname. The directory structure must also match the package-structure.

Kotlin just sees a program as a bunch of files, each starting with a package-declaration followed by one or more class/object/function/property declarations (see later for more on these package-level items); it doesn’t care at all about the filenames or directory structure. A single file cannot declare multiple packages, but multiple files can contribute definitions to the same package.

Although the compiler supports a free-form directory structure, it is convention for Kotlin source to be stored in a directory-tree that follows the package hierarchy. However the top levels of the directory tree are typically omitted; in a typical maven project, directory src/main/kotlin thus directly contains files whose package-declaration is the “base” package for the project.

Files use the suffix “.kt”.

Core Syntax and Simple Differences

Kotlin naming conventions basically follows the Java rules for package, class, function, variable and constant names.

Single-line and block comments are like Java, except that block comments nest (making commenting-out code much easier). See later for info on function, class and package documentation.

Braces are generally used in the same way as with Java. The most significant syntactic difference is that semicolons are in general unnecessary (unless multiple statements are on the same line).

Keyword new is not used; instead the type-name acts as a factory-method: foo = Foo().

Kotlin if-statements are similar to Java, but can also be used as expressions, for example: val foo = if (true) a else b.

The Kotlin language does not have “primitive types” (eg int, char); value types have methods like any other type. At runtime, the JVM primitive forms (on-stack rather than on-heap) are automatically used except when object-references are needed (eg when storing into a collection). The built-in value-types are:

  • Byte, Short, Int, Long, Float, Double, Char, Boolean.

Arrays are normal objects of type Array (rather than being an odd primitive/object hybrid as in Java). They are declared like ints : Array<Int> and instantiated via Array<T>(size, lambda) or various functions such as arrayOf(...). Array instances provide get and set methods in addition to [] access. They also provide method size and a few others. These arrays hold only objects; for an array of primitives there are special types (ByteArray, IntArray, etc).

Unlike Java, the Kotlin operator == is equivalent to method .equals (but null-safe). To check identity, use three chars: ===.

Classes and interfaces work similarly to Java; the declaration syntax is somewhat different but recognisable. See later for more details.

Enums are roughly similar to Java. One of the few uses of semicolons in Kotlin is to terminate the comma-separated list of enum values; this is needed only when the enum type has methods or properties.

The base type for Kotlin objects is Any (roughly equivalent to Java’s Object).

Several keywords used by Java as class, method or member modifiers are instead provided by Kotlin as annotations from package kotlin.jvm. As is clear from the package-name, these annotations are only valid in code that is compiled to JVM bytecode (eg not available when compiling to Javascript):

  • Java transient is @kotlin.jvm.Transient
  • Java volatile is @kotlin.jvm.Volatile
  • Java synchronized modifier is @kotlin.jvm.Synchronized (and synchronized blocks are similar to Java: synchronized(someobj) { ... }).

Reserved words can be used as normal names in Kotlin syntax by enclosing them in backticks. This is particularly useful when Kotlin code needs to call into Java code which uses a Kotlin keyword as a method-name. Example: foo.`in`(bar). Backticks can also be used to create function (or even variable) names that include spaces; this is however not recommended with one exception: unit test names are often descriptive strings with spaces in them.

Functions can be marked with modifier tailrec; if they are indeed tail-recursive then the recursion is rewritten to a loop.

Javadoc

Instead of Javadoc, Kotlin uses “kdoc” format, which is somewhere between Javadoc and Markdown; see the reference-docs for details.

Kotlin provides application dokka for gathering kdoc from all classes and building a site. For overall documentation on a whole “module” (maven or gradle project), a single markdown file is supported; the dokka plugin in your build definition must be configured with the name of this file (eg in the maven plugin config).

Sadly, Dokka does not support any kind of per-package documentation file (see this discussion). It also appears to ignore any kdoc placed against package-declarations; at least I found no way to get such documentation into the generated output files.

Dokka supports code-bases with a mix of Kotlin and Java files; it extracts Javadoc from Java files and includes it in the overall output. Sadly Dokka ignores package-info.java files.

In fact, dokka (version 0.10.0) is rather feature-poor and difficult to use, and its html output format just looks ugly; from my limited experience it is currently the weakest part of the Kotlin experience. However as an external project, it may improve.

Local Variable Declarations

Variables are declared within functions using syntax var|val {name} : {type} = {value}.

Keyword var indicates the name is rebindable while val is like final in Java; the name is not rebindable. This does not imply that the referenced object is immutable.

A type-specification is optional when the type can be deduced from the initial value (type inference).

The value is required unless the declaration is an abstract property declaration (see later for information on class properties).

A question-mark following the type-specification indicates that the variable is nullable; see later for more details.

Note that var or val used within a class-body or primary-constructor-parameter-list or at file-level does not declare a variable but instead declares a property; see later for more information.

Operator Overloading

Kotlin supports a basic form of operator-overloading, ie functions with names that are from a small set of traditionally built-in operator chars. In general, operators are equivalent to a specific function-name which can be overridden, eg:

Simple cases:

  • !a is transformed to a.not()
  • a + b is transformed to a.plus(b)
  • a in b is transformed to b.contains(a)
  • a += b is transformed to a.plusAssign(b)
  • a > b is transformed to a.compareTo(b) > 0 where the compareTo function must return an integer

Self-modification cases:

  • ++a is transformed to {a = a.inc(); return a}
  • a++ is transformed to {tmp = a; a = a.inc(); return tmp}

Also:

  • a[i] is transformed to a.get(i)
  • a[i] = b is transformed to a.set(i, b)
  • a(i) is transformed to a.invoke(i)

The declaration of such methods must be marked with keyword operator, eg operator fun not() ...

The invoke function can be particularly useful; it makes it possible for any object to act as a function, just by implementing that method. An object with such a method is called a ‘function object’. In fact, that is exactly what a lambda is: an object with an invoke-function whose signature is the param-list of the lambda and whose body is the body of the lambda. A reference to a method of an existing object is also a function object: val foo = SomeType::someMethod (unbound) or val bar = someInstance::someMethod (bound).

Nulls

A variable of a specific type cannot be assigned the value null unless it is declared with :T?, ie the variable has type “T or null”.

The ? suffix can be considered equivalent to Java’s Optional<T>.

After an if (var != null) the compiler treats the variable as effectively non-nullable (ie definitely-not-null).

There are a few null-aware operators that make code much shorter:

  • Expression val result = x ?: expr is a convenient shorthand for result = if (x != null) x else expr
  • Expression val result = x ?.somemethod() is short for result = if (x == null) null else x.somemethod(..)

These operators are similar to Java’s Optional class. For example:

  • x?:1 is like Optional.ofNullable(x).getOrElse(1)
  • x?.method() is like Optional.ofNullable(x).map(X::method)
  • x?.let {..} is like Optional.ofNullable(x).map({..})

Operator !! converts a variable of nullable type to a non-nullable, with possible NullPointerException:

val foo : Int? = 1
val bar : Int? = null

var r0 : Int = foo    // does not compile
var r1 : Int = foo!!  // succeeds because foo is not null
var r2 : Int = bar!!  // throws NullPointerException

The !! operator can also be considered similar to Optional.ofNullable(x).get() - which throws an exception if x is null.

Return values from Java code are treated specially; they are technically nullable (or at least the Kotlin compiler does not know if they are nullable or not) but treating them as such would lead to really ugly code. They are therefore handled as a special “platform type” which may be treated like a Kotlin non-nullable but might throw an exception at runtime. See the official documentation on “platform types” for more details.

Lambdas

Kotlin provides syntax similar to Java lambdas: params -> body or { params -> body } depending on context.

When passing a lambda to a function as the last parameter, the lambda can be placed in braces outside the parameter-list. And if the function has only one parameter, the function parameter round-brackets can be omitted completely:

  • list.forEach({item -> println(item)}) // seldom seen
  • list.forEach() { item -> println(item) } // seldom seen
  • list.forEach { item -> println(item) }

When the lambda takes one argument, the varname can be omitted in which case name it is used by default:

  • list.forEach { println(it) }

The body of a lambda may access variables in the outer scope when it (eventually) runs - ie it is a closure.

And just for interest, in the case where the lambda body just invokes one function with the same params as the lambda (which is true for our example above):

  • list.forEach(::println)

The Kotlin standard library provides lots of functions that take lambdas as parameters.

Scope Functions

Kotlin provides a family of methods that are available on every object which allow applying a lambda to the object in various ways:

  • let, run, with, apply and also

For two methods (let and also), the object on which the method was invoked is passed as the (sole) param of the lambda (and as usual can be referred to as it if the lambda does not explicitly declare a param-list).

For the other scope-functions, the lambda which is invoked takes no parameters; instead the object on which the method was invoked is available within the lambda body as this. The latter results in slightly more elegant code when the lambda body accesses methods or properties on its parameter.

For two of the methods (apply and also), the return-value of the lambda is the object on which the method was invoked while for the other three (let, run, with) the return-value of the lambda is whatever the body returns. The former is useful when chaining calls to the original object.

Interestingly, because the scope-functions are implemented as extension functions, they can be invoked on null-references; within the lambda it or this (depending on scope-function) is null.

Examples:

  • "hello world".let(::println)
  • "hello world".let { println(it) }
  • "hello world".apply { println(this.capitalize()) } – where “this” in lambda is not needed
  • maybeNullString.?let { println(it) } – print string iff the value is not null
  • maybeNullString.let { println(it) } – print string or “null”

The with method supports a simple but useful syntax:

with(someobj) {
  method1()
  for(i in 1..4) {
    method2()
    method3()
  }
}

Within the with-block, the specified object is “this”, ie the first object on the method-resolution list.

Type Casts

The operator is tests the type of a reference and implicitly casts the reference to the tested type. Explicit casts are therefore seldom needed. Example:

  • val r = if (s is String) s.trim() else s

When necessary, keyword as can be used for explicit type-casts. The details are a little complicated; see the official docs. Example:

  • val x = y as String?

Due to the JVM’s type-erasure, it is not possible to use if (x is SomeGenericType<ParamType>); the best that is available is is SomeGenericType<*>. An exception is for “inline functions” which can use keyword reified, but inline functions should be used sparingly; see the official docs for more info.

Exceptions

Exceptions work similarly to Java. However:

  • all exceptions are unchecked, ie the compiler never forces you to catch exceptions
  • and therefore “throws clauses” are never needed on function declarations

One of the major reasons for not having typed exceptions in Kotlin is that they don’t work well with higher-order functions; a function that accepts and executes a lambda must declare any exceptions that the lambda may throw - which would strongly limit the lambdas that the function could accept and require throws-clauses on lambdas. Java already has this problem.

This does not mean that Kotlin functions should throw unchecked exceptions as a regular thing; instead Kotlin functions should generally indicate failure via some mechanism other than an exception, reserving exceptions only for errors that are truly “exceptional” and which are expected to be handled “high up” in the software stack rather than being dealt with by the caller (eg out-of-memory). Kotlin’s standard library follows this approach.

Try/catch statements are expressions (ie return a value) in Kotlin. The value is the last statement in the try-clause, or the last statement in some catch-clause.

For-loops

Work like Python (and other langs):

  • for (item in items) ...
  • for (index in items.indices) ...

The equivalent of Java’s for (int x = 0; x < 10; x+=2) is for (x in 0 until 10 step 2) - where “until” is an infix function that returns a collection-like object.

When-expressions

Kotlin when-expressions are the equivalent of Java switch-statements, but somewhat more elegant. The when-expression can optionally take a parameter in which case the condition-expressions can be a constant, is-expression or in-expression.

val obj = 17

val result1 = when {
  obj < 10 -> "small"
  obj < 20 -> "medium"
  else -> "large" 
}

val result2 = when (obj) {
  in 0..10 -> "small"
  in 11..20 -> "medium"
  else -> "large"
}

when (result2) {
  "small" -> println("too small")
  "large" -> println("too large")
  "medium" -> println("just right")
  else -> println("completely unexpected")
}

String Templates and Raw Strings

Any literal string can use $varname to reference variables in the current scope. In fact, even expressions can be evaluated eg println("sum of $a and $b is ${a + b}").

Literal strings do replace “escape chars” with their equivalents (eg \n with a LF). They are also terminated by the first unescaped quote, and must fit on a single line.

A “raw string” delimited by triple-quotes does not process escape-chars, are terminated only by a triple-quote, and can span multiple lines. However they still are treated as string-templates, ie $var or ${expr} is still evaluated.

Map Access

The Kotlin standard library provides two different interfaces to the standard Java HashMap: a mutable interface and a read-only interface.

Due to operator-overloading, maps can be accessed as if they were arrays:

map[key] = value  // invokes map.set(key, value)

Key/value pairs are often created using infix-function Tuple.to eg name to "Mary"; this returns an instance of type Pair which the Map factory-method mapOf accepts:

val opposites = mapOf("left" to "right", "up" to "down")
println(opposites)

Access Control

Scope keywords public and protected have the same meaning as in Java.

Keyword private is expanded slightly; access is allowed from any code in the same file, not just the same class.

Unlike Java, public is the default visibility, and therefore does not need to be used in most cases. However when generating a library rather than an application, it is strongly recommended that every declaration have an explicit access-scope, including public where appropriate. This coding convention is intended to reduce the number of “accidentally public” declarations.

Scope keyword internal has no equivalent in Java; it grants access to “the set of all source-code files fed to a compiler as a batch”. When compiling using the Kotlin plugin for Gradle or Maven, this is all source-code in the project. When compiling using the Kotlin support in the IDEA compiler, it is similar: the set of files in an IDE “module”.

There is no “package-scope”; classes that need to interact can be defined in the same file and use private scope, or in different files and use scope internal.

Scope internal is intended to solve a problem with Java’s package-scope. When implementing a library, it is common to need code in one package to call code in another package. Unfortunately in Java that can only be done by making the callee public - at which point the code also becomes visible to users of the library. OSGi solves this for Java by having each library explicitly declare which packages are part of the public API; code in other packages is not visible to users even when the classes/methods are public. However OSGi is not widely used.

As the JVM has no concept of scope internal, the JVM bytecode generated by the Kotlin compiler for internal classes and methods is marked as public. However the method-names are modified so that they are not easily called from code outside the module. The exact algorithm is an internal implementation detail and could change with new Kotlin releases, but the current implementation appears to:

  • leave class names unchanged
  • leave constructors unchanged
  • but rename every method to {originalMethodName}${modulename}

And of course the “backing fields” for properties already have auto-generated names, so are not easy to access.

Just as a side-note: Java 11 introduces the concept of nests to the JVM, where one class can (at bytecode level) declare a list of other classes which can access its private fields and methods. This feature was introduced primarily to allow inner-classes to access fields of their enclosing class. However nests do not seem to be useful for implementing “internal scope” - ie the current “name-mangling” implementation appears to be a long-term solution.

Function Declarations

An abstract function is declared via

  • fun {name}({params}) : return-type

A concrete function is declared via

  • fun {name}({params}) : return-type {...}

When a function consists of just a single expression then it can be written as:

  • fun {name}({params}) [: return-type] = ..expr..

A function with a block body is required to use the return keyword unless its return-type is Unit. The single-expression form of function definition does not require the keyword return and the return-type can be omitted as it can be inferred from the body. An abstract function declaration must always include the return-type.

Function parameters can have default values; such params usually follow all params without default values.

Functions can be invoked using “named arguments”, eg somefun(a=1, b=2)

Functions with two params can be declared as infix, and then used as lhs {funcname} rhs.

Function declarations can be nested, ie a function can have a “local helper function” that only it can see/access.

Type Unit is equivalent to Java void.

Higher Order Functions and Function Types

A function can be represented as an object, and passed around by reference.

The function type (Int, Int) -> String is equivalent to a declaration of form:

interface IntIntToString {
  fun invoke(i:Int, j:Int) : String
}

Functions can accept references to other functions as parameters:

fun useHigherFunction(fn: (Int, Int) -> String) {
  println(fn(1,2))
}

fun intCombiner(i: Int, j:Int) : String {
  return "$i and $j"
}

useHigherFunction(::intCombiner)
useHigherFunction { i, j -> "lambda of $i and $j" }
# and other options possible

A function-type (Int, Int) -> String can also be written as Int.(Int) -> String. When declared in this form, it can be called in either of two ways:

  • fn(1, 2) or
  • 1.fn(2)

In either case, within the function implementation, keyword this refers to the first parameter (1 in our example).

Java also supports “higher order functions”; it just lacks a syntax for inline function type declarations, instead requiring explicit definition of a Single Applicable Method interface:

// Java
public interface IIS { String apply(int i1, int i2); }
String higherOrderDemo(int v1, int v2, IIS fn) { return "result: " + fn.apply(v1, v2); }

IIS fn = (i, j) -> String.format("i1=%d, i2=%d", i, j);
String result = higherOrderDemo(12, 13, fn);

// Kotlin
fun higherOrderDemo(v1:Int, v2:Int, fn:(Int,Int)->String) = "result: " + fn(v1, v2)

val fn = { i:Int, j:Int -> "i1=$i, i2=$j" }
val result = higherOrderDemo(12, 13, fn)

Generics

These are reasonably similar to Java. Like Java, angle-brackets are used for type-parameters, eg List<Int>.

Unlike Java, “type inference” is used for generic type parameters, ie List<Int> can be written as just List if the fact that the element-type is Int can be determined by the compiler from context.

Kotlin has a different solution for variance, ie Java’s <? extends T> and <? super T> syntax. Here, Kotlin basically uses the same approach as C#: <in T> and <out T>. Kotlin also has “star projections” which look like SomeGenericType<*> and are roughly equivalent to Java’s “raw types” while still staying within the generic type-checking system. The details are somewhat complex; see the official docs for the full explanation.

Packages and imports

Kotlin packages are similar to Java packages.

Code imports types from packages in a similar way too. A Kotlin file does import many more packages by default (Java only imports java.lang.*):

  • kotlin.*, kotlin.annotation.*, kotlin.collections.*, kotlin.comparisons.*, kotlin.io.*, kotlin.ranges.*, kotlin.sequences.*, kotlin.text.*, java.lang.*, kotlin.jvm.*

The Kotlin standard libraries must therefore be in the classpath of any Kotlin program. Fortunately they are not particularly large - certainly smaller than those of Scala.

Top-Level Declarations

Unlike Java, a Kotlin file can contain package-level property-declarations and function-declarations as well as class-declarations and object-declarations - ie declarations can be placed following the package-declaration and outside of any class Foo {..} scope. And in fact, a “main-method” that provides the entry-point to a Kotlin program must be declared in this way.

The ability to define properties and functions directly within packages supports traditional functional-programming style; a Kotlin program can be written with just DTOs (data classes), variables and functions without any object-oriented features at all (though IO is done via the Java standard libraries and thus requires at least a minimum of object-oriented code).

When a file contains package-level declarations which are not classes, Kotlin creates a JVM class with name {filename}Kt with package-level function-declarations from the package implemented as static functions, and backing-fields for properties implemented as static members. This default class-name can be overridden with annotation @file:JvmName({desiredClassName}).

Destructuring Declarations

The fields of a data-class can be extracted in a simple assignment, eg:

  • val (name, age) = someuser
  • val (ret1, _, ret3) = somefunction()

where the type of someuser has methods that supply the necessary data. Actually, any type can be used on the right-hand-side of a destructuring expression as long as it provides methods with signature operator fun component{N}() : <T> where an N is available for each target variable on the LHS of the expression, and <T> matches the type of the target variable. Kotlin data-classes provide such methods automatically; they can be implemented for other types either via normal methods or via extension-functions.

The standard library provides classes Pair and Triple, which makes it elegant to implement and invoke functions that return 2 or 3 values. For more complex return-types, dedicated data-classes should be defined; “data classes” have a very compact syntax and so there is little reason to return plain tuples.

Instances of Pair are usually created by infix function Tuples.to, eg 1 to 2.

The Kotlin standard library provides extension-functions component1() and component2() for standard Java type Map.Entry, allowing for ((key, value) in map) ....

An underscore can be used if you don’t need the value of a specific “component”.

Destructuring is also useful for lambda-parameters:

  • map.mapValues { (key, value) -> ... }

Kotlin does not have the more extensive destructuring/pattern-matching features of Scala.

Extension Functions

Functions can be added to an existing class:

  • fun {sometype}.{funcname}(..) {..}

These functions are available anywhere that the extension-function-declaration is visible (ie has been imported). The functions are not actually part of the class; they are simply syntactic sugar for

  • fun {funcname}(this: sometype, ...) {..}.

When invoking such a function, someobj.{funcname}(..) becomes {funcname}(someobj, ..).

By convention, extension functions used or provided by a package are declared at the top of the file, ie right after the package declaration. However for extension-functions used only in one class, they can be declared in the scope of that class.

Interestingly, this can also be used to add extension properties to the target class via syntax:

  • val {sometype}.{propname}: {proptype} get() {..}

However an “extension property” cannot have a backing-field.

Extension functions are dispatched based on the static type of the expression they are invoked on, not its runtime type. This means that an extension function can even be invoked on a null instance of a type:

fun MyType?.myfunc() = if (this == null) println("isnull") else println("notnull")

val myobj : MyType? = null
myobj.myfunc()  // prints "isnull"

The Use Function

Any class which implements java.io.Closeable automatically gets an extension function named use which invokes a lambda then closes the resource (whether an exception occurred or not). This is Kotlin’s equivalent of Java’s try-with-resources. Example:

val stream = Files.newInputStream(Paths.get("/some/file.txt"))
// method use runs the lambda then invokes `close` on the reader (even on exception)
stream.buffered().reader().use { reader -> println(reader.readText()) }

Annotations

Annotations are very similar in Kotlin and Java.

One significant difference is for array-typed properties; Java uses array-initialisation syntax prop = {a, b, c} while Kotlin uses prop = [a, b, c]. Both Java and Kotlin allow the default “value” property to use var-args format instead.

In addition, Java has special-case handling for annotation attributes whose value is an array; a single element can be passed directly. Kotlin does not support this; an array-typed annotation property must always be given an array-typed value even when it only contains one element.

Varargs Parameters

Varargs-methods use keyword vararg rather than Java’s .... An array can be passed to a varargs method by prefixing it with the “spread operator” *:

fun varargsTest(vararg items: String) {
}

varargsTest("alpha", "beta")

var stringArray = arrayOf("first", "second")
varargsTest(*stringArray)

Classes

Like Java (and unlike Scala), Kotlin is single-inheritance. Mixins are not supported directly, though the by keyword can be used to produce a similar effect.

Simple Class Declarations

Somewhat like Scala, the signature for the “primary constructor” of a class is listed in the class declaration. The primary constructor does not have a “function body” in the traditional sense; any “body logic” for the primary constructor must be defined either via init {..} blocks in the class body, or via a secondary constructor.

The parent-class (if any) follows the constructor; any parameters must be provided immediately.

And implemented interfaces follow the parent-class.

Example:

class Foo constructor(id: Int, name: String) : ParentClass(id), Interface1, Interface2 {...}

Annotations and visibility-modifiers may be placed before keyword constructor.

As a special case, if there are no annotations and visibility-modifiers on the constructor then keyword constructor can be omitted.

A secondary constructor looks a lot more like a Java constructor. However it must first delegate to the primary constructor, and then may run additional code - like this() or super() in Java, except mandatory.

class Person(val name: String) {
  init { logic associated with the primary constructor }

  // secondary constructor
  constructor(name: String, ...) : this(name) {
    // some body logic (in a style more familiar to java developers)...
  }
}

Methods on a class are final by default; they must be prefixed with modifier open to make them overridable. And in the subclass, Java annotation @Override is replaced by keyword override which is mandatory.

A class is also implicitly final unless explicitly declared as open class {name}

Class Properties

Property Declarations, Getters, Setters, and Backing Fields

A val or var declaration at package-level or in a class body declares a property, which is a somewhat abstract concept. The same keywords in a function-body simply declare a variable, not a property.

A property declaration with an explicit initialisation value is given an implicit getter-method (and an implicit setter-method when using var). The declaration can be followed by explicit getter or setter method declarations which replace the default implementations. Kotlin allocates a “backing field” for the property if the property uses the default getter or the default setter, or references the backing field via keyword field in a custom getter or setter. This “backing field” has an auto-allocated name and cannot be directly accessed except by keyword field from within the getter/setter implementation.

A private property with default getter/setter is optimised, being effectively the same as Java field access.

A declaration without an explicit initialisation value (a “virtual property”) must be followed by a getter-method declaration and optionally a setter-method declaration.

Like methods, a property can be overridden in a subclass (if it is declared open) - though this is rarer than overriding normal methods.

Primary-constructor parameters which are marked var or val automatically declare properties which are initialised from the value passed to the constructor (yay - no more boring Java constructors). Parameters that are not marked as either var or val are accessible from within variable-initialisation expressions and init-blocks within the class body, but are not properties. Keywords var/val are not permitted in the parameter-list of secondary constructors.

package foo

private var myPackageProperty = "someprop"

class Foo(val name: String) { // name becomes a public read-only property of Foo
  val PI = 3.141 // read-only property

  var autoincrementer = 0 // read-write property
    get() {
      return ++field // increments the property each time get is called
    }
}

A property which is initialised with the return-value of a Java method (including code from the Java standard library) cannot use type-inference, as the return value is a “platform type” whose nullable/not-nullable state is not known.

Const Declarations

The modifier “const” can be applied to a val declaration at package-level or within an object (singleton); it is not allowed at class-level or function-level. Compilation will fail unless the initialisation-expression can be evaluated at compile-time. A const property cannot have a custom getter (and no setter at all) and thus acts more like a plain variable than a property.

Property Access Syntax

Properties can be accessed via syntax val foo = instance.property, ie with what looks like Java field-access syntax. All such accesses are compiled to JVM bytecode as calls to the getter. Setters work similarly.

Kotlin code can invoke getters and setters in pure Java classes with the field-access-like syntax.

Java code can access properties from a Kotlin class using methods get{name}() and set{name}(val).

Accessibility of Properties

The accessibility of the property getter/setter (ie whether public, protected, private or internal) can be set in a few ways:

var prop1 = "prop1"  // public getter and setter
protected val prop2 = 2 // protected getter, no setter
var prop3 = true; protected set // public getter, protected setter

Initialising Properties from Constructor Parameters

As alternative to declaring properties in the constructor-params, properties can be declared in the class body and initialised to the value of constructor-params. Variable references within the class body resolve first to parameters then to properties, which makes the syntax quite nice. This approach allows the property-name to be different from the param-name, allows more control over accessibility, and supports custom getters and setters for the property.

class Item(desc: String, count: Int) {
  val itemDescription = desc  // property name differs from constructor-param-name
  var count = count; private set // property has different accessibility for getter and setter
}

Lateinit

A property can be marked “lateinit” in which case it does not need to be initialised with a value on the same line as the declaration; this is effectively like declaring the property nullable and initialising it with null but promising that the value will be set to a non-null value before it is used; if code does access it before initialisation then an exception is thrown. This keyword is particularly useful with dependency-injection frameworks which do not fully support initialisation of instances via the class constructor (Spring for example). The property must be mutable (var not val).

Because lateinit properties need to be var, it can be useful to reduce the accessibility of the setter method.

// silly example of lateinit usage
class MyLateInitDemo {
  lateinit var myLateValue: String; private set // Non-nullable and not initialised!

  @PostConstruct
  fun postConstruct() {
    myLateValue = "defined"
  }
}

Data Classes

Kotlin “data classes” make it easy to define DTO-like types - and are similar to those in Scala, Java’s record type (since Java 15), or Java’s Lombok utility.

data class Customer(val name: String, var email: String) { ... }

The primary-constructor-parameters are usually marked with var/val to make them properties. A data-class often needs no body (in which case the braces can be omitted).

Unlike regular class-declarations, appropriate equals/hashcode/tostring methods are auto-generated.

A copy method is also generated which allows creating a new instance of the type with identical values except for the fields listed in the copy-method named arguments eg somedataobj.copy(name=foo, length=17). This is particularly useful for a data-class that has only val-properties (and is thus immutable). If you have a complex structure of immutable objects nested within immutable objects and wish to create a copy with some new values for deeply-nested properties then you may wish to look at the lenses feature of the arrow library.

A data-class cannot be declared open - ie it is equivalent to a Java “final class”.

The parameters of a data-class may have default values.

A data class may also declare properties in its class-body. These are not included in the auto-generated equals/hashcode/tostring/other methods.

Libraries which instantiate a type with no parameters initially (eg JPA) may require data-classes to define a default-value for every parameter - or see the standard Kotlin compiler plugins which can help with this.

Sealed Classes

In Java, a class can either be final (no subclasses allowed) or not (infinite subclasses allowed). A Kotlin sealed class sits between: subclasses can only be declared in the same file.

A class-declaration marked as sealed is implicitly abstract, ie cannot be instantiated directly; only its subclasses may be instantiated.

These are very useful in when-clauses (like Java switch-statements) which test the type of an object as there is no need for an else clause; the compiler knows when all possibilities have been checked.

A sealed class is known in functional-programming literature as a Sum Type (a subtype of Algebraic Data Types) - because the set of possible values for a value of the base type is the sum (union) of the set of possible values for each of the subclasses.

Object Declarations

Singleton types are declared with syntax

  • object SomeType {..} or
  • object OtherType : BaseClass(), Interface1, Interface2 {..}

This declares a type similar to using the “class” keyword. It also causes a singleton instance of that type to be created when it is first referenced (lazy initalisation). This singleton instance can then be accessed via the typename, eg SomeType.someFunction() or SomeType.SOME_CONSTANT.

Kotlin does not have the Java keyword static; things that would be static in Java are instead best placed on a singleton. There are significant benefits to this; it is far more elegant than static!

As an alternative to using singleton objects, properties and functions that have no associated object can be declared directly within a package, ie not within a class or object declaration. However using an object makes it possible to pass sets of properties/functions around via a single reference to the enclosing object.

Companion Objects

An object declaration nested within a class that is marked with qualifier companion has additional behaviour.

Variable and function references from the associated class are first resolved against the class itself, and if not found then against the companion-object.

A class can have only one companion object; it can have other nested object-declarations but their methods are NOT automatically in the resolution-path.

When the typename is omitted for the companion-object then its name is {parentType}.Companion.

Example:

class Foo {
  companion object {
    const val MAX = 20
    fun doSomething(value: int) { .. }
  }

  object OtherNestedObject {
    fun doSomethingElse(value: int) { .. }
  }

  fun fooFunction() {
    doSomething(MAX) // finds the method and constant on the companion
    OtherNestedObject.doSomethingElse(17)
  }
}

A companion object’s singleton instance is initialised when the enclosing class is loaded into memory.

Methods and members of the companion objects can be accessed from outside of the enclosing class simply via the enclosing class name, eg

  • Foo.doSomething(Foo.MAX)

or more explicitly via:

  • Foo.Companion.doSomething(Foo.Companion.MAX)

Object Expressions (anonymous objects)

Anonymous classes are declared with keyword object. What in Java would be new SomeType() {..} is in Kotlin object : SomeType() {..}.

This effectively creates a type when executed, and an instance of that type; no more instances of the type can be created as the type has no name.

The object acts as a closure, ie the body can reference variables in the enclosing scope.

Delegation

This is a very useful feature that has no real equivalent in Java.

Given a class declaration

class Derived(b: Base) : Base by b;

Then instances of Derived also implement interface Base, and by default those inherited methods simply forward calls onto the same method on the object passed to the constructor.

Type Derived can override methods defined in Base to easily implement AOP-style “around advices”.

A different kind of delegation can be applied to properties:

val someprop by somedelegate

Here, “somedelegate” is an object that must provide a function getValue and for var-properties also a function setValue. Reads and writes to the property are then forwarded to the associated delegate-object.

Usually, the delegate object is created via a function-call. Here is an example using the standard-library function lazy which accepts a lambda as parameter and returns a delegate object:

val memoizedFoo by lazy {...}

The delegate-object returns by function lazy invokes the lambda on first call to getValue() and caches the returned value.

See the Kotlin docs for more details.

Nested Classes

Classes can be nested as in Java. However nested classes are static by default; keyword inner is required in order for the nested class to get an implicit reference to its parent.

Kotlin Libraries

If you’ve used Scala, you’re aware that Scala comes with a huge standard library of its own.

Kotlin also provides its own libraries, but somewhat smaller and less complicated. The most significant part is the collections api, and the most important feature is that proper read-only interfaces are made available for the standard Java collections.

The Kotlin standard library does not implement its own collection types; instead it just provides new interfaces for the standard Java collection types. And most importantly, many of these interfaces are read-only, ie lack methods such as put/add. Because these interfaces only have “output values” they are able to be covariant, eg a List<String> can be passed around as a List<Any>. Java collections can’t be covariant because all standard interfaces include write APIs, and putting an Any into the list would be bad for obvious reasons.

Because Kotlin just provides new interfaces for existing collection types, integration of Java standard collection types and Kotlin standard collection types is basically transparent; there is no need to call methods to convert from one to the other.

WARNING: these read-only collections are backed by the standard Java mutable implementations; they are just being passed around using an interface that has no mutation methods. The underlying data for a reference of read-only type can change if:

  • code calls a mutation method on the original (mutable) object
  • the read-only-typed reference is forcibly cast to the mutable type
  • the reference is passed to a Java method which then mutates the object

This is why the Kotlin documentation calls these interfaces read-only and not immutable!

Functional programming often uses immutable collections, ie creates new collections by starting with an existing collection and logically cloning it. Functional languages therefore often use specialized implementations of collections that make this efficient; the new collection internally references the original one and contains just the changes. Sadly because Kotlin uses the Java collections, functional-style use of collections in Kotlin is far less efficient than in other functional languages - the underlying collection is often truly cloned. That’s a trade-off that the Kotlin developers chose in order to simplify Kotlin/Java interactions.

The collections library is imported by default, so many functions are available without specifying a package-prefix. Examples: listOf(1, 2, 3) and mutableListof(1, 2, 3).

The collections library defines a large number of functional-style operators (map, flatMap, filter, etc) which are available directly on the collection types without the indirection of “streams” and “collectors” that Java requires.

While the Kotlin standard libraries are required to be in the classpath of any Kotlin program, there are additional libraries:

  • kotlin-reflect provides classes in package kotlin.reflect for introspecting types at runtime
  • various official-but-optional libraries providing sub-packages of kotlinx.* (analogous to Java’s javax.* packages)

Reflection

The class-object associated with a Kotlin class is retrieved like: val fooKClass : KClass<*> = Foo::class. Note that the type is KClass<*> not Class<?>; the KClass provides method .java to convert it to a traditional Java class object.

As noted above, Kotlin does not include reflection APIs in the standard library; they are included in optional library kotlin.reflect.

Builders

The lambda-related features of Kotlin, when used with some carefully-defined functions, make it possible to use valid Kotlin code to build hierarchical data structures while having that code look more like declarations than procedural code.

Kotlin is not quite as elegant at this as Groovy (with Gradle files being the best example), but it does a reasonable job.

An HTML builder library is available in Kotlin standard extension library kotlinx.html. Libraries for building other hierarchical structures are available from third-parties.

Gradle supports build-files defined via Kotlin-based builders instead of Groovy-based builders.

Coroutines

Kotlin standard extension library kotlinx.coroutines provides coroutine support for Kotlin - lightweight userspace cooperative threading.

Core Concepts:

  • CoroutineScope - an object through which coroutines can be registered via the following methods:
    • launch(lambda) - schedules the lambda to run sometime, and returns a Job object representing the task; the Job supports join and cancel.
    • async(lambda) - returns a Deferred<T> which is a subtype of Job which has an await method that blocks the caller until the lambda has computed its return-value (similar to a future)
    • actor(lambda) - returns a “channel” object through which messages can be sent to the lambda
    • broadcast/produce/promise - yet more variants
  • Dispatcher - actually executes a coroutine in some thread

See the documentation on coroutines for more information.

Minor notes

SLF4J

When using SLF4j or Log4j, a logger is usually obtained by passing the enclosing class, eg:

public class Foo {
  private static final Logger logger = LoggerFactory.getLogger(Foo.class)
}

In Kotlin the equivalent is:

class Foo {
  companion object {
    val logger = LoggerFactory.getLogger(Foo::class.java)
  }
}

Java Streams

The following code is fairly normal in Java:

SomeCollection results = collection.stream().map(..).collect(..)  

The Kotlin equivalent is simpler:

val results = collection.map {...}

Note that the lambda does not need to be inside the map-function parameter list (this is just syntactic sugar), and (more importantly) neither method stream() nor method collect is needed.

Java Incompatibilities

When mixing Kotlin and Java code, watch out for the following…

Invoking Kotlin functions with default parameter values from Java is not supported; the values must be explicitly provided - or annotation @JvmOverloads applied, which implicitly generates variants of the function with differing numbers of parameters.

Kotlin object-references must be explicitly declared as nullable or non-nullable, meaning that null-pointer exceptions are less common at runtime. Every Java method returning an object technically returns a nullable reference, ie theoretically every variable to which a Java returned value is assigned should have ? as a modifier. However this would make code very ugly; it is therefore allowed to omit the suffix - and this is commonly done when the programmer believes that the return-value is never null. This does mean that what looks like a non-nullable variable can potentially throw NullPointerException at runtime if an unexpected null was returned. The full story is somewhat complicated; see documentation on “Platform Types” for more details.

A comparison of Kotlin vs Java can be found in the docs. Far more detail can be found in the full reference docs under section “Java Interop”.

Kotlin vs Scala

Here are some things that Scala has which Kotlin does not..

Tuples

There is limited Tuple support in Kotlin - just Pair and Triple. Data classes can in some ways be considered a tuple with named fields, as destructuring-assignment can be used with them.

Custom Operators

In Scala, just about any sequence of characters can be used as a prefix, postfix or infix operator. Kotlin allows reusing the standard operators (about a dozen), but more cannot be defined. Kotlin also provides no ability to modify left/right associativity, precedence, etc.

This also means that the Lisp-derived syntax for building lists that Scala provides (val list = 1 :: 2 :: 3 :: Nil) is not possible in Kotlin.

Powerful Generics

Kotlin’s generics are no more or less powerful than Java’s generics - just easier to understand.

Scala has far more powerful generic types - but at the cost of a lot of complexity that programmers need to understand.

Scala works around the JVM’s “type erasure” problem by supporting Manifests for generic types; this allows a method called on a generic type to know the types associated with its parent. This feature can be very useful but unfortunately has no equivalent in Kotlin. Kotlin does support keyword reified which does something similar to manifests, but that only works for “inline” functions.

Pattern Matching and Destructuring

Kotlin has very basic destructuring-assignments, and its when-statements provide very limited pattern-matching.

Scala’s features are far more powerful, but also more complex to learn. Its approach to destructuring also creates intermediate arrays, ie is not quite as efficient at runtime.

Implicit Conversions

Scala provides “implicit conversions”; when an object of type T is used where a different type is expected, the compiler checks all imports to see if an “implicit conversion” is defined that can wrap type T to produce the necessary target type. Similarly, when a method M is invoked on a type that has no such method, then the compiler looks for implicit conversions that would produce a type that does have such a method.

This is extremely powerful, but:

  • can make code almost impossible to read, and
  • makes compilation extremely slow

Scala’s support for this means that external libraries can feel like a built-in feature of the language. Kotlin libraries cannot integrate so elegantly - but Java has even less support for extending classes, and is still successful.

Implicit Parameters

A Scala method can declare a parameter as “implicit”; when the caller does not provide a value for it then the compiler searches for an implicit value of the same type and passes that as the value.

This allows libraries to define very extensible APIs - but at the cost of difficult-to-read and difficult-to-compile code.

Mixins

Scala allows traits (similar to interfaces) to declare member variables. This is not quite “multiple inheritance” but close; Scala’s “linearization” allows the definition of “mixin classes” which provide “cross-cutting” functionality that can be usefully added to wide varieties of classes. Linearization also supports an effect similar to AOP “interceptors” by inheriting from a trait that overrides an inherited method. However this concept does introduce significant complexity into the Scala language, and can lead to the compiler producing an unexpectedly large number of JVM classes from a single input class.

Kotlin provides nothing similar to Scala’s linearization. It does provide delegation which might solve some of the same use-cases.

Exception-handling with Try

Scala’s standard library provides function Try which executes a block of code, and returns a wrapper-object holding either the result or the thrown exception. This allows error-handling code to use return-checks rather than exception-checks.

Kotlin’s Result type can be used to invoke functions which may throw exceptions, though its style is a little different. The arrow library does provide Try - though it recommends using other approaches instead.

Factory Methods on Companion Objects

Scala code can use an elegant syntax to provide a factory-method for instances of a type: define an “apply-method” on the companion object. Simply using the enclosing type-name as a function then invokes the factory, eg val x = List(1, 2, 3). Kotlin uses this syntax to invoke the built-in object construction logic (invoke the class constructor), so a factory-method has to look something like listOf(..).

Partial Function Application and Currying

Not sure how this is done in Kotlin; it certainly is not as integrated into the language as Scala.

Function Composition

Kotlin does not have methods for function-composition in its standard library; you will need some third-party library for that (eg Arrow).

Build Systems

It seems that the Kotlin world generally prefers Gradle over Maven.

Interestingly, the Gradle project also now supports defining build-scripts using a Kotlin-based DSL rather than a groovy-based one.

Conciseness

I recently converted a demo spring/jpa program from Java to Kotlin.

  • Java SLOC: 1368
  • Kotlin SLOC: 1075

Change: (1075 - 1368)/1368 = 21% fewer lines of code

That’s not world-changing, but definitely worth having.

Useful References