CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Classes & Objects

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.

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() { var firstName = "Vanilla" var lastName = "Ice" } fun main() { val p = Person() // we can access properties directly // this calls an implicit get() method; default returns the value println("${p.firstName} ${p.lastName} ${p.lastName} Baby") }

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 } } fun main() { // 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") }

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 } fun main() { // 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() } } fun main() { val person1 = Person() // primary constructor using default property values println("${person1.firstName} ${person1.lastName}") val person2 = Person("JEFF", "AVERY") // secondary constructor println("${person2.firstName} ${person2.lastName}") }

Init Blocks

How do we 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 initialization 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") }
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") } } fun main() { 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.

data class Point(val x: Double, val y: Double) // -point operator fun Point.unaryMinus() = Point(-x, -y) // p1+p2 operator fun Point.plus(other: Point) = Point(this.x + other.x, this.y + other.y) // 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) fun main() { val p1 = Point(5.0, 10.0) val p2 = Point(10.0, 12.0) println("p1=${p1}") println("p2=${p2}\n") println("-p1=${-p1}") println("p1+p2=${p1+p2}") print("p2*5=${p2*5}") }

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 { return (this shl x) } fun main() { // calling the function using the infix notation // shl 1 multiples an int by 2 println(212 shl 1) // is the same as println(212.shl(1)) }

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 fun main() { println(isEven(4)) println(isEven(5)) }

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 fun main() { println(4.isEven()) println(5.isEven()) }

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).

fun String.addEnthusiasm(enthusiasmLevel: Int = 1) = this + "!".repeat(enthusiasmLevel) fun main() { val s1 = "I'm so excited" val s2 = s1.addEnthusiasm(5) println(s2) }

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) } fun main() { "string".print() 42.print() }

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" } fun main() { println("abcd".numVowels) }

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) fun main() { val mike = Person("Mike", 23) // toString() displays all properties println(mike.toString()) // structural equality (==) compares properties println(mike == Person("Mike", 23)) // True println(mike == Person("Mike", 21)) // False // referential equality (===) compares object references println(mike === Person("Mike", 23)) // True // hashCode based on primary constructor properties println(mike.hashCode() == Person("Mike", 23).hashCode()) // True println(mike.hashCode() == Person("Mike", 21).hashCode()) // False // destructuring based on properties val (name, age) = mike println("$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 clauses (Example from [Sommerhoff 2020]).

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

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) } fun main() { val direction = Direction.EAST print(direction.degrees) }

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" } fun main() { val p1 = Person("Jerry") val p2 = PolishPerson("Beth") println(p1.hello()) println(p2.hello()) }

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 } } fun main() { // 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. This syntax is called a destructuring declaration. A destructuring declaration creates multiple variables at once. In the example below, you declared two new variables: name and age, and can use them independently:

data class Person(val name: String, val age: Int) fun main() { val p = Person("Janine", 38) val (name, age) = p // destructuring 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) }

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) }