Learning Kotlin
Kotlin is a modern language designed by JetBrains in 2011. Originally designed as a drop-in replacement for Java, Kotlin has a number of language features that make it desireable for building applications. We’ll focus on Kotlin/JVM, which gives us the ability to build programs that will run anywhere where we have a JVM installed (including Windows, macOS, Linux).
- It has a very clean syntax, and supports quality-of-life features like default arguments, variable argument lists and rich collection types. It’s syntax closely resembles modern languages like Swift or Scala.
- It’s a hybrid language: it can be used for declarative programming or class-based object-oriented programming. It also supports a number of functional features, especially with the use of collection classes. This allows a programmer to use the best appropach for a particular task.
- Kotlin is statically compiled, so it catches many potential errors during compilation (not just at runtime). It’s also strongly typed, with type inference.
- Critically, it supports compilation to a number of targets: JVM for Windows/macOS/Linux Desktop, Android native1, or Web. It can also build to native macOS and Windows (with some restrictions).
- It has outstanding tools support with IntelliJ IDEA.
For reference, see Kotlin documentation online.
You need the Kotlin compiler and runtime. We’ll run on the Java JVM. You can either install the command-line tools, or install IntelliJ IDEA and run everything from within the IDE (which is recommended).
See the Getting Started section for details on installing IntelliJ IDEA and the Kotlin Compiler.
Compiled languages require an explicit step to compile code and generate native executables. This is done ahead of time, and the executables are distributed to users. e.g. C++
- The compilation cost is incurred before the user runs the program, so we get optimal startup performance.
- The target system architecture must be known ahead of time, since we’re distributing native binaries.
Interpreted languages allow developers to distribute the raw source code which can be interpreted when the user executes it.
- This requires some ‘runtime engine‘ that can convert source code to machine code on-the-fly. Results of this operation can often be cached so that the compilation cost is only incurred when it first executes.
Some languages can be compiled to a secondary format (IR, ”intermediate representation”) and then interpreted. Languages running on the Java Virtual Machine (JVM) are compiled ahead of time to IR, and then interpreted at runtime.
Kotlin can be compiled or interpreted!
Kotlin/JVM
compiles Kotlin code to JVM bytecode, which can run on any Java virtual machine.Kotlin/Android
compiles Kotlin code to native Android binaries, which leverage native versions of the Java Library and Kotlin standard libraries.Kotlin/Native
compiles Kotlin code to native binaries, which can run without a virtual machine. It is an LLVM based backend for the Kotlin compiler and native implementation of the Kotlin standard library.Kotlin/JS
transpiles (converts) Kotlin to JavaScript. The current implementation targets ECMAScript 5.1 (with plans to eventually target ECMAScript 2015).
There are three primary ways of executing Kotlin code:
- Read-Evaluate-Print-Loop (REPL): Interact directly with the Kotlin runtime, one line at-a-time. In this environment, it acts like a dynamic language.
- KotlinScript: Use Kotlin as a scripting language, by placing our code in a script and executing directly from our shell. The code is compiled automatically when we execute it, which eliminates the need to compile ahead-of-time.
- Application: We can compile standalone applications, targetting native or JVM [ed. we will use JVM in this course].
REPL is a paradigm where you type and submit expressions to the compiler one line-at-a-time. It’s commonly used with dynamic languages for debugging, or checking short expressions. It’s not intended as a means of writing full applications!
$ kotlin
Welcome to Kotlin version 1.6.10 (JRE 17.0.2+8-86)
Type :help for help, :quit for quit
>>> val message="Hello Kotlin!"
>>> println(message)
Hello Kotlin!
KotlinScript is Kotlin code in a script file that we can execute from our shell. This makes Kotlin an interesting alternative to a language like Python for shell scripting.
$ cat hello.kts
#!/usr/bin/env kotlin
val message="Hello Kotlin!"
println(message)
$ ./hello.kts
Hello Kotlin!
Kotlin compiles scripts in the background before executing them, so there’s a delay before it executes [ed. I fully expect that later versions of Kotlin will allow caching the compilation results to speedup script execution time].
This is a great way to test functionality, but not a straight-up replacement for shell scripts, due to the runtime costs2.
Kotlin applications are fully-functional, and can be compiled to native code, or to the JVM. Kotlin application code looks a little like C, or Java. Here’s the world’s simplest Kotlin program, consisting of a single main method3.
fun main() {
println("Hello Kotlin!")
}
To compile from the command-line, we can use the Kotlin compiler, kotlinc
. By default, it takes Kotlin source files (.kt
) and compiles them into corresponding class files (.class
) that can be executed on the JVM.
$ kotlinc Hello.kt
$ ls
Hello.kt HelloKt.class
$ kotlin HelloKt
Hello Kotlin!
Notice that the compiled class is named slightly differently than the source file. If your code isn’t contained in a class, Kotlin wraps it in an artificial class so that the JVM (which requires a class) can load it properly. Later when we use classes, this won’t be necessary.
This example compiles Hello.kt
into Hello.jar
and then executes it:
$ kotlinc Hello.kt -include-runtime -d Hello.jar
$ ls
Hello.jar Hello.kt
$ java -jar Hello.jar
Hello Kotlin!
Kotlin has full access to it’s own class libraries, plus any others that are imported and made available. Kotlin is 100% compatible with Java libraries, and makes extensive use of Java libraries when possible. For example, Kotlin collection classes actually use some of the underlying Java collection libraries!
In this section, we’ll discuss how to use existing libraries in your code. We need to talk about namespaces and qualifying classes before we can talk about libraries.
You can declare a namespace in Kotlin by using the package declaration to the top of a source file. Classes or modules in the same package have full visibility by default.
Names of packages are always lowercase and do not use underscores (org.example.project
). Using multi-word names is generally discouraged, but if you do need to use multiple words, you can either just concatenate them together or use camel case (org.example.myProject
).
For example, in the file below, contents are contained in the ca.uwaterloo.cs346
package. The full name of the class could be qualified as ca.uwaterloo.cs346.ErrorMessage
. If you were referring to it from a different package, you would need to use this fully qualified name.
package ca.uwaterloo.cs346
class ErrorMessage(val msg:String) {
fun print() {
println(msg)
}
}
fun main() {
val error = ErrorMessage("testing an error condition")
error.print()
}
To use a class in a different namespace, we need to import the related class by using the import
keyword. This applies to any class that we wish to use. In the example below, we import our ErrorMessage class into a different namespace so that we can instantiate and use it.
import ca.uwaterloo.cs346.ErrorMessage
class Logger {
val error = ErrorMessage()
error.printMessage()
}
Type aliases provide alternative names for existing types. If the type name is too long you can introduce a different shorter name and use the new one instead. This can make your code much more readable!
Note that type aliases do not introduce new types. They are equivalent to the corresponding underlying types, so there is no runtime overhead.
typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>
The Kotlin Standard Library is included with the Kotlin language, and contained in the kotlin
package. This is automatically imported and does not need to be specified in an import statement.
Some of the features that will be discussed below are actually part of the standard library (and not part of the core language). This includes essential classes, such as:
- Higher-order scope functions that implement idiomatic patterns (let, apply, use, etc).
- Extension functions for collections (eager) and sequences (lazy).
- Various utilities for working with strings and char sequences.
- Extensions for JDK classes making it convenient to work with files, IO, and threading.
Kotlin is completely 100% interoperable with Java, so all of the classes available in Java/JVM can also be imported and used in Kotlin.
// import all classes in the java.io package
// this allows us to refer to any class in this namespace
import java.io.*
// we can also just import a single class
// this allows us to refer to just the ListView class in code
import javafx.scene.control.ListView
// Kotlin code calling Java IO libraries
import java.io.FileReader
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.IOException
import java.io.FileWriter
import java.io.BufferedWriter
if (writer != null) {
writer.write(
row.toString() + delimiter +
s + row + delimiter +
pi + endl
)
Importing a class requires your compiler to locate the file containing these classes! The Kotlin Standard Library can always be referenced by the compiler, and as long as you’re compiling to the JVM, the Java class libraries will also be made available. However, to use any other Java or Kotlin library, you will need to take additional steps. We’ll discuss this when we cover build systems and Gradle.
-
Kotlin has been adopted as the “official” language for Android development! ↩︎
-
Scripts will be compiled and cached locally, but there’s still some runtime performance issues. ↩︎
-
This chapter focuses mainly on the Kotlin language. In the next chapter, we’ll dive deeper into constructing applications. ↩︎