Skip to content

Everything possible to do with kotlin with just the compiler and an idea

General purpose languages tends to offer everything possible in a very flexible way so the programmer can enjoy solving the same problem again and again.

There are exceptions, of course, but that's not the case of kotlin.

Kotlin has so many features that one could be easily overwhelmed by it.

The docs

  • The kotlin manual should be handy for minor questions on daily work.
  • Kotlin reference also helps, but keep in mind that sometimes it tell you to read the java documentation
  • The playground helps if you past the phase of use the compiler on command line all the time.

Environment setup

We'll start simple, but don't get fooled, we'll get enterprise code soon.

We'll need kotlin compiler available from command line.

To get it, first we need to install the JDK, any modern version (17 and beyond) will serve. Technically speaking, java 11 would be good too, but it's done and gone for good.

If you don't want to create an oracle account, there areother JDK providers and the result will be pretty much the same.

If using linux there is a good chance that your distribution already offers a jdk out of the box, from the official repo.

To check if JDK installation went well, call java and javac commands in the terminal:

bash
$ java -version
openjdk version "17.0.9" 2023-10-17
OpenJDK Runtime Environment (Red_Hat-17.0.9.0.9-4) (build 17.0.9+9)
OpenJDK 64-Bit Server VM (Red_Hat-17.0.9.0.9-4) (build 17.0.9+9, mixed mode, sharing)

$ javac -version
javac 17.0.9

Now we're good to install the command line compiler!

As the official docs state, if using mac, use brew; if linux use sdkman, if windows, good luck!

The compiler is called kotlinc and the expected terminal output follows:

bash
$ kotlinc -version
info: kotlinc-jvm 1.9.22 (JRE 17.0.9+9)

Running scripts

The compiler enters into interactive mode if no args are passed, and also has a evaluation mode:

bash
$ kotlinc -e 'println("hello!")'
hello!

Project: Basic output - Hello world

Kotlin files ends with the .kt extension and in order to be executable they must declare a main function:

bash
echo 'fun main () { println("hello!") }' > HelloWorld.kt
kotlinc HelloWorld.kt

kotlin HelloWorldKt
hello!

kotlin needs the entry point function to be called main to produce executable artifacts, either classes or jar files.

Since kotlin runs on top of jvm:

bash
java HelloWorldKt
hello!

See project 001 for further details.

Project: Files and packages

One important thing to master is how kotlin manages the source code modularization. It's pretty much what java and python and other languages does.

Given two distinct kotlin files:

kotlin
// file1.kt
fun hey() = "hey!"
kotlin
// file2.kt
fun main() { println(hey()) }

You will need to compile the scripts:

bash
kotlinc *.kt

It will produce two .class files:

bash
$ ls
file1.kt  File1Kt.class  file2.kt  File2Kt.class  META-INF  README.md

As you can see, the kotlin compiler "helped" you and used proper camel case on the file names.

To run this code:

bash
$ kotlin File2Kt
hey!

Any attempt to run File1Kt ends up in error since that class has no main function to serve as entrypoint:

bash
$ kotlin File1Kt
error: 'main' method not found in class File1Kt

Packages

The concepts of modules and packages have some similarities but they have some key distinctions. Take this example:

kotlin
package project003

fun main () {
  println("hi from package")
}

While modules, in most languages, has a 1:1 equivalence with folders in the file system, packages does that but tries to stay away from folders because some systems might not have this concept.

Anyway, if your script declares a package and you are not in a mainframe, by compiling it the output will be stored in a folder structure representing the declared package:

bash
$ kotlinc file3.kt
$ ls project003
File3Kt.class
$ kotlin project003.File3Kt
hi from package

See project 002 for further details.

Control flow

Things happen from top to bottom, from left to the right, unless it's async or just a function declaration, then it will happen only when it's called. Then again, top to bottom, left to the right.

But mind the unnamed blocks! The following code is invalid:

kotlin
// flow.kt
fun main() {
    val (first, second) = listOf(1,2,3)
    
    {
     println("Hello, world!!! $second")
    }
}

Kotlin thinks the block is the body of a lambda then bails out with a syntax error:

bash
Expression is treated as a trailing lambda argument; consider separating it from call with semicolon
Unresolved reference: second

You do as the output suggests and try to compile again:

bash
flow.kt:3:8: warning: variable 'first' is never used
  val (first, second) = listOf(1,2,3);
       ^
flow.kt:5:3: warning: the lambda expression is unused. If you mean a block, you can use 'run { ... }'
  {
  ^

Variables

There is a few ways to declare variables:

kotlin
var x: Int // declaration 
x = 11 // attribution
var y = 12 // shorthand with type inference

kotlin is strongly typed, so either you declare with type explicitly or you use the shorthand version. Just var x is illegal

There is also other two kinds of declarations: val and const val

kotlin
const val z = 10

fun main() {
  val x: Int // declaration 
  x = 11 // attribution
  val y = 12 // shorthand with type inference
  // y = 13 // ERROR, illegal
  println("Our values are $x $y $z")
}

The difference between val x and const x is that const is checked at compile time.

If, when, while, for and so on

Conditionals are pretty straightforward:

kotlin
fun isEven(x: Int): Boolean {
  if(x % 2 == 0) {
    return true
  } else {
    return false
  }
}

There are nice syntax sugar:

kotlin
fun isEven(x: Int): Boolean {
  return if(x % 2 == 0) true else false
}

The switch has other meaning in kotlin so the keyword for it is when:

kotlin
fun isWorkday(day: String): Boolean {
  return when(day) {
    "Saturday", "Sunday" -> false
    else -> true
  }
}

It can yeld values.

While loops are... while loops:

kotlin
var x = 10
while(x > 0) {
  x--
}

See the docs for complete control flow reference.

Primitive data types: String, Int and so on

Functions (or methods)

  • use the fun keyword

  • parameters can have default values

  • if last argument is a closure, you are allowed to use block syntax outside the regular argument list. Weird but you get used to it.

    kotlin
    fun main() {
      val items = listOf(1, 2, 3).map { it *2 } // closure/lambda
      println(items) // prints [2, 4, 6]
    
      // longer format:
      val more = listOf(7,11,13).map(fun (prime: Int): Int { return prime*prime })
      println(more) // prints [49, 121, 169]
    }

Basic input

Now we know kotlin types and how to communicate using prints to console, let's ask for user input.

Project: Program arguments

One way to get user input is by passing program arguments:

kotlin
fun main(args: Array<String>) {
  for(arg in args) println("Received $arg")
}

Project: Environment variables

Another way is to get input is to check environment variablesfrom the shell:

kotlin
fun main() {
  println("current PATH is ${System.getenv("PATH")}")
}

Most modern operating systems offers a way to set an environment to the current running process so the user can tweak some of its behaviors.

Project: Interactive prompt - Check triangles

Another way to get user input is via interactive command line prompt.

This project samples how to do that.

To read from command line kotlin can either use the readLine function or use The java.util.Scanner class from jdk.

Complex data types: List, Map

List and Map are the main container types for daily usage and they work as expected: lists are indexed by numbers, starting from zero, maps are indexed by text keys or something else.

Usually we'll create lists with the listOf helper function and maps with the mapOf helper function:

kotlin
val numbers = listOf(1,2,3) // immutable
// val numbers = ArrayList<Int>() // mutable
// numbers.add(1)
// numbers.add(2)
// numbers.add(3)
val languages = mapOf("script" to "javascript", "compiled" to "java") // immutable
// val languages = HashMap<String,String>() // mutable
// languages.put("script", "javascript")
// languages.put("compiled", "java")

Please note that kotlin has mutable and immutable versions of lists and maps.

Classes

Kotlin is versatile and can go from full functional paradigm to full object-oriented style. If you know classes from other languages, you can expect similar stuff in kotlin; classes, interfaces, constructors, attributes, public and private visibility modifiers and so on.

There are a few special idioms that are worthy to mention:

Smallest possible class declaration

You can declare a class with no attributes and empty constructor like this:

kotlin
class Thing

This is useful, as we'll see in next chapters, trust me.

The most common way to use constructors

They are kinda inlined with class name declaration:

kotlin
class Thing(val attr: Int) // attr is public and immutable because of val

But you can add a second one:

kotlin
class Thing(val attr: Int) {
    constructor(att: Int, something: String) : this(att) {
        println(something)
    }
}

It's possible to make it even more verbose, like java:

kotlin
class Thing {

    var attr: Int

    constructor(attr: Int) {
      this.attr = attr
    }

    constructor(attr: Int, something: String) {
        this.attr = attr
        println(something)
    }
}

See more about classes here.

Inheritance and Interfaces

By default, kotlin classes are final. You have to explicitly declare them open so you can extend a class:

kotlin
open class Base(p: Int)

class Derived(p: Int) : Base(p)

To override methods they must be open to it as well:

kotlin
open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

Kotlin interfaces can declare methods that classes can override. Like java, it's the way to get polymorphism.

kotlin
interface MyInterface {
    fun bar()
    fun foo() {
      // optional body
    }
}

class MyClass : MyInterface {
  override fun bar(){}

}

You need polymorphism to proper implement the "I" in SOLID.

Basic I/O: Files

Input and Output are the very heart of any modern system, side by side with processing capabilities.

That part in kotlin is pretty much similar to what we see in other languages:

We can read, we can write, the mode can be either character or binary.

Character mode implies an encoding. Class names and other utilities get names like Reader or Writer.

Binary mode deals with bytes and we call them streams. Either InputStream or OutputStream, depending if data comes in or out of current process.

The most common source and destination for such streams, readers and writers are the files in the filesystem, the current and most popular data storage abstraction.

Project: Simple Agenda

In this example we read a file, transform them in Contacts and perform some operations. Then we save back to the disk every time a change happens.

Concurrency: thread, coroutines and so on

Kotlin has concurrency support built on top of jvm so expect good performance. You can use java threads:

kotlin
import kotlin.concurrent.thread

fun main() {
  for(i in 1..100) {
    thread {
      println("I am $i")
    }
  }
}

But the concurrency in kotlin really shines with coroutines:

kotlin
// sample-coroutine.kt
import kotlinx.coroutines.*

fun main() {
  runBlocking {
    for(i in 1..100) {
      launch {
        println("I am $i")
      }
    }
  }
}

Note: kotlinx samples needs an extra library, and the command line compiler is buggy. But coroutines samples will run fine on playground in jvm mode.

Next steps

This is the minimum needed to understand the foundations of the language. Now we are good to talk about project setup.