From Java to Kotlin - OOP, Companion Objects & Destructuring in Kotlin

Constructors with default values

The primary constructor is part of the class header. Secondary constructors are declared in the class body.

class User constructor(nickname: String) {
  init {
    // code to execute on Class creation
  }
}

// using default values
class User(val nickname: String, val isSubscribed: Boolean = true)

Secondary Constructors

If a class has a primary constructor, each secondary constructor needs to delegate to the primary constructor

class Person(val name: String) {
  constructor(name: String, parent: Person): this(name) {
    parent.children.add(this)
  }
}

Properties

These can be declared as mutable, using the var keyword or read-only using val. To use a property we refer to it by name, as if it were a field in Java

class Address {
  var name: String = ...
  var street: String = ...
  var city: String = ...
  var state: String? = ...
}

Declare and init propertiers from the primary constructor

class Person(
  val firstName: String,
  val lastName: String,
  var age: Int) {
}

Visibility Modifiers

If you omit a modifier, the default visibility in Kotlin becomes public. Declarations are also final by default.

A constructor can also be private for Singleton or Factory methods

class C private constructor(a: Int) {
  ...
}

Data classes

Data classes are intented for types that are meant to be data containers and nothing more.

They avoid the need to define the methods equals() hashCode() toString()

data class Customer(val id: Int, val name: String, var address: String)

It’s strongly recommended that you use read-only properties, making the instances of the data class immutable.

Sealed classes

They’re used for representing restricted class hierarchies. A sealed class is an abstract class that restricts the possibility of creating subclasses. They’re the solution when you want a class that cannot be extended.

It’s a rather more powerful Enum option as unlike Enum, the derived classes of a sealed class can have many instances.

// declaration
sealed class Expr
data class Const(val number: Double): Expr()
data class Sum(val e1: Expr, val e2: Expr): Expr()
Object NotANuber: Expr()

// usage
fun eval(expr: Expr): Double = when(expr) {
  is Const -> expr.number
  is Sum -> eval(expr.e1) + eval(expr.e2)
  NotANumber -> Double.NaN
}

Singletons

They’re way easier than in Java

// you can call Singleton.doSomething() and each time
//    you'll see the counter increasing.
object Singleton {
  private var count = 0
  fun doSomething(): Unit {
    println("Do something with count ${++count}")
  }
}

The Object keyword is used to declare a class and create an instance of a class at the same time.

Companion Objects

Classes in Kotlin cannot have static members. If you need something like a static function that needs access to the internal members of a class, you can write it as a companion object inside that class.

class A {
  // the companion's name can be omitted
  companion object {
    fun bar() {
      println("Companion object called")
    }
  }
}

// invoke
A.bar()

factory design pattern example

interface StudentFactory {
  fun create(name: String): Student
}

class Student private constructor(val name: String) {
  companion object: StudentFactory {
    override fun create(name: String): Student {
      return Student(name)
    }
  }
}

// invoke
Student.create("Mario C.")

Destructuring Declarations

Sometimes it’s convenient to destructure an object into a number of variables. This creates multiple variables at once.

val (name, age) = person
println(name)
println(age)

Returning two values from a function

A compact way of returning two values from a function in Kotlin is to declare a data class and return its instance

data class Result(val result: Int, val status: Status)
fun function(...): Result {
  // computations
  return Result(result, status)
}

// invoke
val (result, status) = function(...)

Collections and for-loops

Iterating over a collection with for-loops can be done via destructuring declarations

// this is an example for a Map
var map: HashMap<Int, Person> = HashMap()
map.put(1, person)

for ((key, value) in map) {
  println("Key: $key, value: $value")
}

Underscore and destructuring in Lambdas

In case we don’t need all values obtained in a destructuring declaration, we use underscore instead of the variable name.

val (_, name, age) = person

// if the fields we don't need are at the end, we just omit them
val (id, name) = person
destructuring in lambdas

we can also use the destructuring declarations syntax for lambda parameters. But be aware of the difference between Kotlin’s declare two parameters and declare a destructuring pair.

{ a -> ... } // one parameter
{ a, b -> ... } // two parameters
{ (a, b) -> ... } // a destructured pair
{ (a, b), c -> ... } // a destructured pair and another param.