CS349 User Interfaces
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Object-Oriented Kotlin

Introduction

Object-oriented programming is a refinement of the structured programming model, that was discovered in 1966 by Ole-Johan Dahl and Kristen Nygaard.

It is characterized by the use of classes as a template for common behaviour (methods) and data (state) required to model that behaviour.

  • Abstraction: Model entities in the system to match real world objects. Each object exposes a stable high-level interface, that can be used to access its data and behaviours. “Changing the implementation should not break anything”.
  • Encapsulation: Keep state and implementation private.
  • Inheritance: Specialization of classes. Create a specialized (child) class by deriving from another (parent) class, and reusing the parent’s fields and methods.
  • Polymorphism: Base and derived classes share an interface, but have specialized implementations.

Object-oriented programming has benefits over iterative programming:

  • It supports abstraction, and allows us to express computation in a way that models our problem domain (e.g. customer classes, file classes).
  • It handles complex state more effectively, by delegating to classes.
  • It’s also a method of organizing your code. This is critical as programs grow.
  • There is some suggestion that it makes code reuse easier (debatable).
  • It became the dominant programming model in the 80s, and our most used languages are OO languages. e.g. C++, Java, Swift, Kotlin.

Features

Classes

Kotlin is a class-based object-oriented language, with some advanced features that it shares with some other recent languages. The class keyword is used to define a Class. You create an instance of the class using the classname (no new keyword required!)

 // define class
 class Person
 
 // create two instances and assign to p, q
 // note that we have an implicit no-arg constructor
 val p = Person()
 val q = Person()

Classes include properties (values) and methods.

Properties

A property is a variable that is declared in a class, but outside of methods or functions. They are analogous to class members, or fields in other languages.

 class Person() {
   // string properties
   var lastName = "Vanilla"
   var firstName = "Ice"
 }
 val person = Person()
 
 // we can access properties directly
 // this calls an implicit get() method; by default this just returns the value
 println("${person.firstName} ${person.lastName}")

> Vanilla Ice

If you annotate your constructor parameters with val or var in the primary constructor, Kotlin will automatically create corresponding properties with the same name!

 class Person(val name: String, var age: Int) 
 val jeff = Person("Jeff", 50) 
 
 // constructor created public name and age properties
 println("${jeff.name} is ${jeff.age}")
 > Jeff is 50
 
 // `val name` so name is immutable and changing it would throw an error
 // jeff.name = "Jeffery"
 
 // `var age` so this is mutable and can be reassigned
 jeff.age = 49 

Properties all have implicit backing fields that store their data. We can override the get() and set methods to determine how our properties interact with the backing fields.

For example, for a City class, we can decide that we want the city name always reported in uppercase, and we want the population always stored as thousands.

 // the backing field is just referred to as `field`
 // in the set() method, we use `value` as the argument
 class City() {
   var name = ""
     get() = field.uppercase()
     set(value) {
       field = value
     }
   var population = 0
     set(value) {
       field = value/1_000
     }
 }
 
 // create our city, using properties to access values
 val city = City()
 city.name = "Halifax"
 city.population = 431_000
 println("$city.name has a population of $city.population thousand people")

> HALIFAX has a population of 431 thousand people

Behind-the-scenes, Kotlin is actually creating getter and setter methods, using the convention of getField and setField . In other words, you always have corresponding methods that are created for you. If you directly access the field name, these methods area actually getting called in the background.

Venkat Subramaniam has an excellent example of this [Subramaniam 2019]. Write the class Car in a separate file named Car.kt:

 class Car(val yearOfMake: Int, var color: String)

Then compile the code and take a look at the bytecode using the javap tool, by running these commands:

 $ kotlinc-jvm Car.kt
 $ javap -p Car.class

This will display the bytecode generated by the Kotlin Compiler for the Car class:

 public final class Car {
   private final int yearOfMake;
   private java.lang.String color;
   public final int getYearOfMake();
   public final java.lang.String getColor();
   public final void setColor(java.lang.String);
   public Car(int, java.lang.String);
 }

That concise single line of Kotlin code for the Car class resulted in the creation of two fields—the backing fields for properties, a constructor, two getters, and a setter.

Constructors

Like other OO languages, Kotlin supports explicit constructors that are called when objects are created.

Primary Constructors

A primary constructor is the main constructor that your class will support (representing how you want it to be instantiated most of the time). You define it by expanding the class definition:

 // class definition includes the primary constructor
 class Person constructor() { }
 
 // we can collapse this to define an explicit no-arg constructor
 class Person() {}

In the example above, the primary constructor is called when this class is instantiated.

Optionally, you can include parameters in the primary constructor, and use these to initialize parameters in the constructor body.

 // constructor with arguments
 // this uses the parameters to initialize properties (i.e. variables)
 class Person (first:String, last:String) {
   val firstName = first.take(1).uppercase() + first.drop(1).lowercase()
   val lastName = last.take(1).uppercase() + last.drop(1).lowercase()
 
   // adding a statement like this will prevent the code from compiling
   // println("${firstname} ${lastname}")  // will not compile
 }
 
 // this does not work! we do not have a no-arg constructor
 val person = Person() // error since no matching constructor
 
 // this works and demonstrates the properties
 val person = Person("JEFF", "AVERY")
 println("${person.firstName} ${person.lastName}") // Jeff Avery

Constructors are designed to be minimal:

  • Parameters can only be used to initialize properties. They go out of scope immediately after the constructor executes.
  • You cannot invoke any other code in your constructor (there are other ways to handle that, which we will discuss below).

Secondary Constructors

What if you need more than a single constructor?

You can define secondary constructors in your class. Secondary constructors must delegate to the primary constructor. Let’s rewrite this class to have a primary no-arg constructor, and a second constructor with parameters.

 // primary constructor
 class Person() {
   // initialize properties
   var firstName = "Paula"
   var lastName = "ABDUL"
   
   // secondary constructor
   // delegates to the no-arg constructor, which will be executed first
   constructor(first: String, last: String) : this() {
     // assign to the properties defined in the primary constructor
     firstName = first.take(1).uppercase() + first.drop(1).lowercase()
     lastName = last.take(1).uppercase() + last.drop(1).lowercase()
   }
 }
 
 val person1 = Person("JEFF", "AVERY")
 println("${person1.firstName} ${person1.lastName}")
 > Jeff Avery
 
 val person2 = Person()
 println("${person2.firstName} ${person2.lastName}")
 > Paula Abdul

Init Blocks

How do we handle the case where we want to execute code in the constructor? We often want to do more than just initialize properties.

Kotlin has a special method called init() that is used to manage initializtion code. You can have one or more of these init blocks in your code, which will be called in order after the primary constructor (they’re actually considered part of the primary constructor). The order of initialization is (1) primary constructor, (2) init blocks in listed order, and then finally (3) secondary constructor.

class InitOrderDemo(name: String) { 
   val first = "$name".also(::println) 

   init { 
     println("First init: ${first.length}")
   }

   val second = "$name".also(::println) 
   init { 
     println("Second init: ${second.length}")
     }
 } 

fun main() {
    InitOrderDemo("Jeff")
}

Jeff
First init: 4
Jeff
Second init: 4
Why does Kotlin split the constructor up like this? It’s a way to enforce that initialization MUST happen first, which results in cleaner, and safer code.

Class Methods

Similarly to other programming languages, functions defined inside of a class are called methods.

 class Person(var firstName: String, var lastName: String) {
   fun greet() {
     println("Hello! My name is $firstName")
   }
 }
 
 val person = Person ("Jeff", "Avery")
 println("${person.firstName} ${person.lastName}")

Operator Overloading

Kotlin allows you to provide custom implementations for the predefined set of operators. These operators have predefined symbolic representation (like + or *) and predefined precedence if you combine them.

Basically, you use the operator keyword to define a function, and provide a member function or an extension function with a specific name for the corresponding type. This type becomes the left-hand side type for binary operations and the argument type for the unary ones.

Here’s an example that extends a class named ClassName by overloading the + operator.

// function signature for overloading the '+' operator
// ClassName is an existing class
operator fun ClassName.plus(other: ClassName)

Here’s a couple of examples using a Point class: we’re like to be able to add and subtract points, or multiple them against a scalar value.

data class Point(val x: Double, val y: Double)
val p1 = Point(5.0, 10.0)
val p2 = Point(10.0, 12.0)

// -point
operator fun Point.unaryMinus() = Point(-x, -y)
println(-p1)
> Point(x=-5.0, y=-10.0)

// p1+p2
operator fun Point.plus(other: Point) = Point(this.x + other.x, this.y + other.y)
print(p1+p2)
> Point(x=15.0, y=22.0)

// p1*5
operator fun Point.times(scalar: Int) = Point(this.x * scalar, this.y * scalar)
operator fun Point.times(scalar: Double) = Point(this.x * scalar, this.y * scalar)

We can override any operators by using the keyword that corresponds the symbol we want to override.

Note that this is the reference object on which we are calling the appropriate method. Parameters are available as usual.

Description Expression Translated to
Unary prefix +a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
Increments, decrements a++ a.inc()
a– a.dec()
Arithmetic a+b a.plus(b)
a-b a.minus(b)
a*b a.times(b)
a/b a.div(b)
a%b a.rem(b)
a..b a.rangeTo(b)
In a in b b.contains(a)
Augmented assignment a+=b a.plusAssign(b)
a-=b a.minusAssign(b)
a*=b a.timesAssign(b)
a/=b a.divAssign(b)
a%b a.remAssign(b)
Equality a==b a?.equals(b) ?: (b === null)
a!=b !(a?.equals(b) ?: (b === null))
Comparison a>b a.compareTo(b) > 0
a<b a.compareTo(b) < 0
a>=b a.compareTo(b) >= 0
a<=b a.compareTo(b) <= 0

Infix Functions

Functions marked with the infix keyword can also be called using the infix notation (omitting the dot and the parentheses for the call). Infix functions must meet the following requirements:

For example, we can add a “shift left” function to the built-in Int class:

 infix fun Int.shl(x: Int): Int { ... }
 
 // calling the function using the infix notation
 1 shl 2
 
 // is the same as
 1.shl(2)

Extension Functions

Kotlin supports extension functions: the ability to add functions to existing classes, even when you don’t have access to the original class’s source code, or cannot modify the class for some reason. This is also a great alternative to inheritance when you cannot extend a class.

For an simple example, imagine that you want to determine if an integer is even. The “traditional” way to handle this is to write a function:

 fun isEven(n: Int): Boolean = n % 2 == 0
 // isEven(4) -> true
 // isEven(5) -> false

In Kotlin, the Int class already has a lot of built-in functionality. It would be a lot more consistent to add this as an extension function to that class.

 fun Int.isEven() = this % 2 == 0
 // 4.isEven() -> true
 // 5.isEven() -> false

You can use extensions with your own types and also types you do not control, like List, String, and other types from the Kotlin standard library.

Extension functions are defined in the same way as other functions, with one major difference: When you specify an extension function, you also specify the type the extension adds functionality to, known as the receiver type. In our earlier example, Int.isEven(), we need to include the class that the function extends, or Int.

Note that in the extension body, this refers to the instance of the type (or the receiver for this method invocation).

// this refers to this string instance
fun String.addEnthusiasm(enthusiasmLevel: Int = 1) = this + "!".repeat(enthusiasmLevel)
Defining an extension on a superclass

Extensions do not rely on inheritance, but they can be combined with inheritance to expand their scope. If you extend a superclass, all of it’s subclasses will inherit the extension method that you defined.

Define an extension on the Any class called print. Because it is defined on Any, it will be directly callable on all types.

// Any is the top-level class from which all classes derive i.e. the ultimate superclass
fun Any.print() {
  println(this)
}

"string".print()
> string
42.print()
> 42
Extension Properties

In addition to adding functionality to a type by specifying extension functions, you can also define extension properties.

For example, here is an extension property that counts a string’s vowels:

val String.numVowels
    get() = count { it.lowercase() in "aeiou" }

Special Classes

Data Classes

A data class is a special type of class, which primarily exists to hold data, and doesn’t have custom methods. Classes like this are more common than you expect – we often create trivial classes to just hold data, and Kotlin makes it very easy.

Why would you use a data class over a regular class? It generates a lot of useful methods for you:

  • hashCode()
  • equals() // compares fields
  • toString()
  • copy() // using fields
  • destructuring

Here’s an example of how useful this can be:

 data class Person(val name: String, var age: Int) 
 val mike = Person("Mike", 23) 
 
 // toString() displays all properties 
 print(mike.toString()) // Person(name=Mike, age=23) 
 
 // structural equality (==) compares properties
 print(mike == Person("Mike", 23)) // True 
 print(mike == Person("Mike", 21)) // False 
 
 // referential equality (===) compares object references
 print(mike === Person("Mike", 23)) // True 
 
 // hashCode based on primary constructor properties
 val hash = mike.hashCode()
 print(hash == Person("Mike", 23).hashCode()) // T 
 print(hash == Person("Mike", 21).hashCode()) // F 
 
 // destructuring based on properties
 val (name, age) = mike 
 print("$name $age") // Mike 23 
 
 // copy that returns a copy of the object 
 // with concrete properties changed
 val jake = mike.copy(name = "Jake") // copy 

Enum Classes

Enums in Kotlin are classes, so enum classes support type safety.

We can use them in expected ways. Enum num constants are separated with commas. We can also do interesting things with our enums, like use them in when clauses1.

 enum class Suits {
     HEARTS, SPADES, DIAMONDS, CLUBS
 }
 
 val suit = Suits.SPADES
 val color = when(suit) {
   Suits.HEARTS, Suits.DIAMONDS -> "red"
   Suits.SPADES, Suits.CLUBS -> "black"
 }
 println(color)
 // black

Each enum constant is an object, and can be instantiated.

 enum class Direction(val degrees: Double) {
     NORTH(0.0), SOUTH(180.0), WEST(270.0), EAST(90.0)
 }
 
 val direction = Direction.EAST
 print(direction.degrees)
 // 90.0

Class Hierarchies

All classes in Kotlin have a common superclass Any, that is the default superclass for a class with no supertypes declared:

 class Example // Implicitly inherits from Any

Any has three methods: equals(), hashCode() and toString(). Thus, they are defined for all Kotlin classes.

Inheritance

To derive a class from a supertype, we use the colon : operator. We also need to delegate to the base class constructor using ().

By default, classes and methods are closed to inheritance. If you want to extend a class or method, you need to explicitly mark it as open for inheritance.

 class Base
 class Derived : Base() // error!
 
 open class Base 
 class Derived : Base() // ok

Kotlin supports single-inheritance.

 open class Person(val name: String) { 
   open fun hello() = "Hello, I am $name" 
 }
 
 class PolishPerson(name: String) : Person(name) { 
   override fun hello() = "Dzien dobry, jestem $name" 
 } 

If the derived class has a primary constructor, the base class can (and must) be initialized, using the parameters of the primary constructor. If the derived class has no primary constructor, then each secondary constructor has to initialize the base type using the super keyword, or to delegate to another constructor which does that.

 class MyView : View {
     constructor(ctx: Context) : super(ctx)
     constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
 }

Abstract Classes

Classes can be declared abstract, which means that they cannot be instantiated, only used as a supertype. The abstract class can contain a mix of implemented methods (which will be inherited by subclasses) and abstract methods, which do not have an implementation.

 // useful way to represent a 2D point
 data class Point(val x:Int, val y:Int)
 
 abstract class Shape() {
   // we can have a single representation of position
   var x = 0
   var y = 0
   
   fun position(): Point {
     return Point(x, y)
   }
   // subtypes will have their own calculations for area
   abstract fun area():Int
 }
 
 class Rectangle (var width: Int, var height: Int): Shape() {
   constructor(x: Int, y: Int, width: Int, height: Int): this(width, height) {
     this.x = x
     this.y = y
   }
   // must be overridden since our base is abstract
   override fun area():Int {
     return width * height
   }
 }
 
 // this won't compile, since Shape() is abstract
 // val shape = Shape()
 
 // this of course is fine
 val rect = Rectangle(10, 20, 50, 10)
 println("Rectangle at (${rect.position().x},${rect.position().y}) with area ${rect.area()}")
 // => Rectangle at (10,20) with area 500

Interfaces

Interfaces in Kotlin are similar to abstract classes, in that they can contain a mix of abstract and implemented methods. What makes them different from abstract classes is that they cannot store state. They can have properties but these need to be abstract or to provide accessor implementations.

Visibility Modifiers

Classes, objects, interfaces, constructors, functions, properties and their setters can have visibility modifiers. Getters always have the same visibility as the property. Kotlin defaults to public access if not visibility modifier is provided.

The possible visibility modifiers are:

  • public: visible to any other code.
  • private: visible inside this class only (including all its members).
  • protected: visible to any derived class, otherwise private.
  • internal: visible within the same module, where a module is a set of Kotlin files compiled together.

Destructuring

Sometimes it is convenient to destructure an object into a number of variables, for example:

 val (name, age) = person

This syntax is called a destructuring declaration. A destructuring declaration creates multiple variables at once. You have declared two new variables: name and age, and can use them independently:

 println(name)
 println(age)

A destructuring declaration is compiled down to the following code:

 val name = person.component1()
 val age = person.component2()

component1(), component() are aliases to the named properties in this class, in the order they were declared (and, of course, there can be component3() and component4() and so on). You would never normally refer to them using these aliases.

Here’s an example from the Kotlin documentation on how to use this to return multiple values from a function:

 // data class with properties `result` and `status`
 data class Result(val result: Int, val status: Status)
 
 fun function(...): Result {
     // computations
     return Result(result, status)
 }
 
 // Destructure into result and status
 val (result, status) = function(...)
 
 // We can also choose to not assign fields
 // e.g. we could just return `result` and discard `status`
 val (result, _) = function(...)

Companion Objects

OO languages typically have some idea of static members: methods that are associated with a class instead of an instance of a class. Static methods can be useful when attempting to implement the singleton pattern, for instance.

Kotlin doesn’t support static members directly. To get something comparable in Kotlin, you need to declare a companion object as an inner class of an existing class. Any methods that are created as part of the companion object are considered to be static methods in the enclosing class.

Examples below are taken from: https://livevideo.manning.com/module/59_5_16/kotlin-for-android-and-java-developers

 class House(val numberOfRooms: Int, val price: Double) {
   companion object {
     val HOUSES_FOR_SALE = 10
     fun getNormalHouse() = House(6, 599_000.00)
     fun getLuxuryHouse() = House(42, 7_000_000.00)
   }
 }
 
 fun main() {
   val normalHouse = House.getNormalHouse()  // works 
   println(normalHouse.price)
   println(House.HOUSES_FOR_SALE)
 }
 
 // 599000.0
 // 10

Companion objects are actually objects at runtime, so they can implement interfaces for instance.

 interface HouseFactory {
   fun createHouse() : House
 }
 
 class House(val numberOfRooms: Int, val price: Double) {
   companion object : HouseFactory{
     val HOUSES_FOR_SALE = 10
     fun getNormalHouse() = House(6, 599_000.00)
     fun getLuxuryHouse() = House(42, 7_000_000.00)
     override fun createHouse() = getNormalHouse()
   }
 }

We can also use object types to implement singletons. All we need to do is use the object keyword.

 class Country(val name:String) {
     var area = 0.0
 }
 
 // there can be only one
 object CountryFactory {
     fun createCountry() = Country("Canada")
 }
 
 fun main() {
     val obj = CountryFactory.createCountry()
     println(obj.name)
 }

Resources

Andrew Bailey, David Greenhalgh & Josh Skeen. 2021. Kotlin Programming: The Big Nerd Ranch Guide. 2nd Edition. Pearson. ISBN 978-0136891055.

Bruce Eckel & Svetlana Isakova. 2020. Atomic Kotlin. Leanpub. https://leanpub.com/AtomicKotlin

Svetlana Isakova & Andrey Breslav. 2019. Kotlin for Java Developers. Coursera. https://www.coursera.org/learn/kotlin-for-java-developers

JetBrains. Kotlin Documentation & Tutorials. https://kotlinlang.org

JetBrains. Kotlin Koans. https://play.kotlinlang.org/koans/overview

Dave Leeds. Dave Leeds on Kotlin. https://typealias.com

Venkat Subramaniam. 2019. Programming Kotlin. Pragmatic Bookshelf. ISBN 978-1680506358.


  1. Example from [Sommerhoff 2020].
    
     ↩︎