Getting Started
Installing Kotlin
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).
Compiling Code
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
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
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 costs1.
Applications
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 method2.
fun main() {
val message="Hello Kotlin!"
println(message)
}
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!
Modularization
Packages
Classes in Kotlin can be grouped together into namespaces, or packages
. You specify a namespace for a file using the package
keyword at the top of the file. The naming convention for packages is to use a reverse-DNS name (since it’s a unique identifier for a company or organization), and then some logical grouping beneath that.
For example, we could use ca.uwaterloo
as our top-level DNS name for this course. To make unique package names beneath that, we might use :
ca.uwaterloo
- ca.uwaterloo.cs346
-- ca.uwaterloo.cs346.graphics // graphics classes for this course
-- ca.uwaterloo.cs346.networking // networking classes for this course
Classes in the same namespace are visible to one another.
If you want to access a class that is in a different namespace, you need to import
that class into your file. For example, if we have an ErrorMessage
class that is in a different package, we need to import is so that we can use it.
// this is a logging class, so we'll place in the logging package
package ca.uwaterloo.cs346.logging
// we want to use this class from a different package
import ca.uwaterloo.cs346.errorhandling.ErrorMessage
// we could also import all graphics classes from this package
import ca.uwaterloo.cs346.graphics.*
class Logger {
val error = ErrorMessage()
error.printMessage()
// ....
}
Type Aliases
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>>
Modularity
Packages work well within an application’s source code, since they let you carefully partition your classes. However, they aren’t particularly helpful if we want to publish a library, or share our functionality with some other application. The fundamental problem with packages is that cannot discriminate internal and external access, so you might end up accidentally exposing internals of your package.
How do we get around this? Kotlin also supports modules
, which allow us to have rules on how our packages will be exposed outside of our application.
The rule-of-thumb is that every project should have a single module that describes how it should be exported. To define a module, add a file named module-info.java
under the src/main/java'
folder in your project. You may need to create this folder and add the file manually to your project3.
Here’s the syntax, continuing the example from above:
/// module-info.java file
module ca.uwaterloo.cs346 {
requires kotlin.stdlib;
requires javafx.graphics;
requires javafx.controls;
exports ca.uwaterloo.cs346;
exports ca.uwaterloo.cs346.logging;
exports ca.uwaterloo.cs346.graphics;
}
module
is the module name. The convention is to name this the same as your top-level package.requires
indicates any module that your project uses that we need to include. IntelliJ will prompt you in your source code if you forget to include something.exports
indicates that it is a package that you wish to be externally visible. Make sure to export each relevant namespace.
Using Libraries
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.
Kotlin Standard Library
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.
Using Java Libraries
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 of those classes in the current 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.
-
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. ↩︎
-
We already have
src/main/kotlin
for our source code. Why are we adding a Java file? Modularity is a Java/JVM feature that was introduced in Java 9, and we’re leveraging it for JVM builds. Your compiler will handle mixing Java and Kotlin files just fine. ↩︎