Object-Oriented Kotlin
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 class name (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 declared in a class, but outside 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 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 are 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 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 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}")
}
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)
}
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()
andtoString()
. Thus, they are defined for all Kotlin classes.
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.
Data Classes
A data class is a special type of class, which primarily exists to hold data, and does not have custom methods. Classes like this are more common than you expect – we often create trivial classes to just hold data, and Kotlin makes them simple to create.
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)) // False
// 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, e.g. 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)
}
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 no 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.
Operator Overloading
Kotlin allows you to provide custom implementations for the predefined set of operators. These operators have predefined symbolic representation (like +
or *
) and 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 to 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:
- They must be member functions or extension functions.
- They must have a single parameter.
- The parameter must not accept variable number of arguments and must have no default value.
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 a 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 its 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)
}
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()
, component2()
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.
The 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)
}
``