Published on [Permalink]
Reading time: 14 minutes
Posted in:

Introduction to Kotlin Objects

Introduction to Kotlin Objects

Note this blog post contains my notes from the Introduction to Kotlin Objects chapter in the book Atomic Kotlin. Consider buying a copy of the book for a more detailed reference.

Objects hold data and perform actions.

They store data using properties (vals and vars) and perform actions using functions.

Creating Classes

Classes are called user-defined types

A class member is a property or function of a class

An object is an instance of a class

Calling a member function:

fun main() {
	val r = IntRange(0, 10)
	println(r.sum())
}


//Output
//55

Conversions between numerical types are explicit:

Use the class keyword to create a new type:

class Giraffe
class Bear
class Hippo


fun main() {
	val g1 = Giraffe()
	val g2 = Giraffe()
	val b = Beaer()
	val h = Hippo()
}

Class names begin with a letter. Convention is to begin with an uppercase letter. Names can include numbers and _s

The printed output of an object contains Class@MemoryAddress

In the example above the entire class definition is a single line with no body.

Example of a member function:

class Dog {
	fun bark() = "yip!"
}


fun main() {
	val dog = Dog()
	val s = dog.bark()
	println(s)
}

Member functions can access other members of that particular object.

When calling a member function, Kotlin keeps track of the object by passing a reference to that object. The object reference is available inside the member function by using the keyword this:

class Hampster {
	fun speak() = "Squeak!"
	fun exercise() =
		this.speak() + speak() + " Running on wheel"
}

Member functions do not have to explicitly qualify other member elements like above.

Outside of a class you must qualify a member function call by the name of that object.

Properties

A property is a var or val that’s part of a class

Each object gets its own storage for properties.

class Cup {
	var percentFull = 0
}


fun main() {
	val c1 = Cup()
	c1.percentFull = 50
	println(c1.percentFull)
}

You must refer to the property in main by using dot notation

A member function can refer to a property within its object without dot notation (aka using this)

class Cup2 {
	val percentFull = 0
	val max = 100
	fun add(increase: Int): Int {
		percentFull += increase
		if (percentFull > max)
			percentFull = max
		return percentFull
	}
}


fun main() {
	val cup = Cup2()
	cup.add(50)
	println(cup.percentFull)
	cup.add(70)
	println(cup.percentFull)
}

Vars within a constant reference can still change like so:

class House {
	var sofa: String = ""
}


fun main() {
	val house = House()
	house.sofa = "Simple sleeper sofa: $89.00"
	println(house.sofa)
	house.sofa = "New leather sofa: $3,099.00"
	println(house.sofa)
}

An immutable object contains no mutable state (i.e. all properties are vals).

Constructors

You init a new object by passing information to a constructor

A constructor is like a special member function that inits a new object.

The simplest constructor is a single line class definition

class Wombat

You can pass information to a constructor using a parameter list:

class Alien(name: String) {
	val greeting = "Poor $name"
}


fun main() {
	val alien = Alien("Humza")
	println(alien.greeting)
}

The arguments to the constructor are not accessible outside the class body.

If you want the constructor parameter to be accessible outside the class body define it as a var or val in the parameter list.

class MutableNameAlien(var name: String)
class FixedNameAlien(val name: String)


fun main() {
	val alien1 = MutableAlien("Humza")
	val alien2= FixedNameAlien("Daniyal")
}

The above class definitions have no explicit class bodies, they are implied. When parameters are specified as a var or val they become properties and are thus accessible outside the class body.

You can have multiple constructor parameters:

class AlienSpecies(
	val name: String,
	val eyes: Int,
	val hands: Int,
	val legs: Int
) {
	fun describe =
		"$name with $eyes eyes, " +
			"$hands hands, and $legs legs"
}


fun main() {
	val kevin = AlienSpecies("Zigerion", 2, 2, 2)
	println(kevin.describe())
}


//Output
//Zigerion with 2 eyes, 2 hands, and 2 legs

Future notes will describe more complex constructors

If the object is used where a String is expected then the object’s toString method is called.

Objects come with a default toString function.

class Scientist(val name: String) {
	override fun toString() = "Scientist('$name')"
}


fun main() {
	val tim = Scientist("Timothy")
	println(tim)
}


//Output
//Scientist('Timothy')

Constraining Visibility

Access modifiers include

An access modifier only controls access for that particular definition. Modifiers can appear before a class, function, or property.

Public is the default and will impact client programmers directly.

A private definition is hidden and only accessible from other members of the same class.

Private classes, top-level functions, and top-level properties are only accessible inside that file.

private var index = 0
private class Animal(val name: String)
private fun recordAnimal(animal: Animal) {
	println("Animal #$index: ${animal.name}")
	index++
}


fun recordAnimals() {
	recordAnimal(Animal("Tiger"))
	recordAnimal(Animal("Antelope"))
}


fun recordAnimalsCount() {
	println("$index animals are here!")
}
// In a separate file main can only do the following
fun main() {
	recordAnimals()
	recordAnimalsCount()
}

Index, Animal, and recordAnimal are only accessible inside this file

privacy is most commonly used for members of a class

An internal definition is only accessible inside the same module.

A library is often a single module consisting of multiple packages. Internal elements are typically accessible within the library but are not accessible by consumers of that library.

Packages

The import keyword reuses code from other files. You can use it to import a class, property, or function:

import packagename.ClassName
import packakgename.propertyName
import packageName.functionName

A package is an associated collection of code

import kotlin.math.PI
import kotlin.math.cos


fun main() {
	println(PI)
	println(cos(PI))
	println(cos(2 * PI))
}

When using multiple libraries with the same class name use the as keyword:

import kotlin.math.PI as circleRatio
import kotlin.math.cos as cosine


fun main() {
	println(circleRatio)
	println(cosine(circleRatio))
	println(cosine(2 * circleRatio))
}

You can alternatively fully qualify an import in the body of your code:

fun main() {
	println(kotlin.math.PI)
}

To import everything from a package use *

import kotlin.math.*


fun main() {
	println(E)
	println(E.roundToInt())
	println(E.toInt())
}

roundToInt rounds a number to the nearest Int. toInt truncates numbers.

To create a package:

The package statement must be the first non-comment statement in the file

package pythagorean
import kotlin.math.sqrt


class RightTriangle(
	val a: Double,
	val b: Double
) {
	fun hypotenuse() = sqrt(a * a + b * b)
	fun area() = a * b / 2
}

You can name the source file anything you want unlike Java.

Convention is to name the package identical to the directory name where the package files are located. Then you can import the package like so:

import pythagorean.RightTriangle


fun main() {
	val rt = RightTriangle(3.0, 4.0)
	println(rt.hypotenuse())
	println(rt.area())
}

Testing

Testing is important for rapid app development

Atomic Kotlin uses a minimal testing approach

Junit, Kotest, and Spek are full testing frameworks

Basics of Atomic Kotlin testing:

import atomictest.*


fun main() {
	val v1 = 11
	val v2 = "Ontology
	
	//eq means equals
	v1 eq 11
	v2 eq "Ontology"
	
	//neq means not equal
	v2 neq "Astrology"
}

eq and neq use infix notation

The basic syntax is expression eq expected

If expected is a String then expression is converted to a String and then compared. Otherwise expression and expected are compared directly.

The result of the expression appears on the console even when tests succeed.

If the comparison fails than an error shows when the program runs.

Using this system you can verify your program by running it.

You can use the trace object to capture output for later comparison.

import atomictest.*


fun main() {
	trace("line 1")
	trace(47)
	trace("line 2")
	trace eq """
		line 1
		47
		line 2
	"""
}

Exceptions

An exceptional condition prevents continuation of the current scope.

In an exceptional condition there is not enough information to continue in the current scope.

Throwing an exception relegates the problem to an external context.

In Kotlin an Exception is an object thrown from the site of the error.

Example:

package exceptions


fun erroneousCode() {
	val i = "1$".toInt()
}


fun main() {
	erroneousCode()
}

Above the exception is thrown to main and is shown as an Error to the user.

An exception that isn’t caught by the program aborts and shows a stack trace.

We can use the capture function from AtomicTest to display exceptions.

import atomictest.*


fun main() {
	capture {
		"1$".toInt()
	} eq "NumberFormatException: " + """For Input String "1$""""
}

Alternatively to throwing an exception we could return null

import atomictest.eq


fun main() {
	"1$".toIntOrNull() eq null
}

Another example before returning null:

package firstversion
import atomictest.*


fun averageIncome(income: Int, months: Int): Int {
	return income / months
}


fun main() {
	averageIncome(3300, 3) eq 1100
	capture {
		averageIncome(5000, 0) eq "ArithmeticException: / by zero"
	}
}

Refactor of above to provide more information about the source of the problem:

package withNull
import atomictesteq


fun averageIncome(income: Int, months: Int) =
	if (months == 0)
		null
	else
		income / months


fun main() {
	averageIncome(3300, 3) eq 1100
	averageIncome(5000, 0) eq null
}

If a function can return null, Kotlin requires that you check the result before using it. Even if you only want to display the output to the user.

Another refactor with a more specific exception:

package properexception
import atomictest.*


fun averageIncome(income: Int, months: Int) =
	if (months == 0)
		throw IllegalArgumentException("Months can't be zero")
	else
		income / months


fun main() {
	averageIncome(3300, 3) eq 1100
	capture {
		averageIncome(5000, 5) eq "IllegalArgumentException: Months can't be zero"
	}
}

Lists

A list is a container

Containers are also called collections

We can init a list by providing literal values

import atomictest.eq


fun main() {
	val ints = listOf(1, 2, 3, 4, 5)
	ints eq "[1, 2, 3, 4, 5]"
	var result  ""
	for (i in ints) {
		result += "$i "
	}
	result eq "1 2 3 4 5"
	ints[4] eq 5
}

A list uses square brackets when displaying itself

For loops work well with lists

Use [] to index into a list starting at position 0

Out of bounds slicing results in an exception

import atomictest.*


fun main() {
	val ints = listOf(1, 2, 3)
	capture {
		ints[3]
	} contains listOf("ArrayIndexOutOfBoundsException")
}

A list can hold different types

import atomictest.eq


fun main() {
	val doubles = listOf(1.0, 2.0, 3.0)
	doubles.sum() eq 6.0
	
	val strings = listOf("Twas", "Brillig", "And", "Slithy", "Toves")
	strings eq listOf("Twas", "Brillig", "And", "Slithy", "Toves")
	strings.sorted() eq listOf("And", "Brillig", "Slithy", "Toves", "Twas")
	strings.reversed() eq listOf("Toves", "Slithy", "And", "Brillig", "Twas")
	strings.first() eq "Twas"
	strings.takeLast(2) eq listOf("Slithy", "Toves")
}

List functions ending in ed (e.g. sorted) produce a new List and leaves the original alone.

To explicitly tell Kotlin what type is in a List:

import atomictest.eq


fun main() {
	val numbers = listOf(1, 2, 3)
	val strings = listOf("one", "two", "three")
	//Explicit annotation
	val numbers2: List<Int> = listOf(1, 2, 3)
	val strings2: List<String> = listOf("one", "two", "three")
	numbers eq numbers2
	strings eq strings2
}

This uses parameterized types.

<> denote a type parameter

Return values can also have type parameters:

package lists
import atomictest.eq


fun inferred(p: Char, q: Char) = listOf(p, q)
fun explicit(p: Char, q: Char): List<Char> = listOf(p, q)


fun main() {
	inferred('a', 'b') eq listOf('a', 'b')
	explicit('a', 'b') eq "['a', 'b']"
}

By default listOf() produces a read-only List.

If your building a list gradually use mutableListOf() to get a type MutableList

import atomictest.eq


fun main() {
	val list = mutableListOf<Int>()
	list.add(1)
	list.addAll(listOf(2, 3))
	list += 4
	list += listOf(5, 6)
	
	list eq listOf(1, 2, 3, 4, 5, 6)
}

The above example shows how you can add elements to a MutableList

You can treat a MutableList as a List, but you cannot treat a List as a MutableList

package lists
import atomictest.eq


fun makeList(): List<Int> = mutableListOf(1, 2, 3)


fun main() {
	val list = makeList()
	list eq listOf(1, 2, 3)
}

The list above lacks mutating functions even though it was created as a mutableList

The original object is still mutable though it is viewed through the lens of a List.

Below is an example of aliasing:

import atomictest.eq


fun main() {
	val first = mutableListOf(1)
	val second: List<Int> = first
	
	second eq listOf(1)
	first.add(2)
	second eq listOf(1, 2)
}

+= gives the illusion that an aimmutable list is a MutableList. This only occurs if the List is assigned to a var.

Variable argument Lists

The vararg keyword produces a flexibly sized argument list.

package varargs


fun v(s: String, vararg d: Double) {}


fun main() {
	v("abc", 1.0, 2.0)
	v("def", 1.0, 2.0, 3.0, 4.0)
}

Only one function parameter can be vararg. Convention is to be the last argument, but it can be any.

varargs are accessed through the parameter name which is an Array.

package varargs
import atomictest.eq


fun sum(vararg numbers: Int): Int {
	var total = 0
	for (n in numbers) {
		total += n
	}
	return total
}


fun main() {
	sum(13, 27, 44) eq 84
}

List is a regular library class while Array has special low-level support.

Use List when you need a simple sequence. Use array when a third-party API requires an Array or using varargs.

package varargs
import atomictest.eq


fun evaluate(vararg ints: Int) =
	"Size ${ints.size}\n" + "Sum ${ints.sum()}\n" + "Average ${ints.average()}"


fun main() {
	evaluate(10, -3, 8, 1, 9) eq """
		Size: 5
		Sum: 25
		Average: 5
	"""
}	

You can pass an Array wherever a vararg is accepted. Use the function arrayOf()

An Array is always mutable

To convert an Array into a sequence of arguments use the spread operator.

import varargs.sum
import atomictest.eq


fun main() {
	val array = intArrayOf(4, 5)
	sum(1, 2, 3, *array, 6) eq 21
	
	val list = listOf(9, 10, 11)
	sum(*list.toIntArray()) eq 30
}

If you pass an Array of primitive types like above you must specifically type the array creation function. Otherwise type-innference will not work.

The spread operator only works with arrays.

When invoking a program on the command line, you can pass it a vararg. To capture command line args you must provide a specific parameter to main.

fun main(args: Array<String>) {
	for (a in args) {
		println(a)
	}
}

Convention is to call this argument args.

To run a command line program using kotlinc

kotlinc MainArgs.kt hamster 42, 3.14
#Output
#hamster
#42
#3.14

args.size tells you the number of arguments passed to the command line function.

Sets

Sets are collections that only contain distinct values.

You can use in or contains() to test for membership.

import atomictest.eq


fun main() {
	val intSet = setOf(1, 1, 2, 2, 3, 4, 5)
	
	//No duplicates
	intSet eq setOf(1, 2, 3, 4, 5)
	//Element order is unimportant
	setOf(1, 2) eq setOf(2, 1)
	//set membership
	(3 in intSet) eq true
	(99 in intSet) eq false
	intSet.contains(5) eq true
	//Does this set contain another set
	intSet.containsAll(setOf(1, 2, 3)) eq true
	//Set union
	intSet.union(3, 4, 5, 6) eq setOf(1, 2, 3, 4, 5, 6)
	//Set difference
	intSet subtract setOf(0, 1, 2) eq setOf(3, 4, 5)
	intSet - setOf(0, 1, 2) eq setOf(3, 4, 5)
	//set intersection
	intSet intersect setOf(0, 1, 7, 8) eq setOf(1)	
}

You can convert a List to a Set

You can call distinct on a List to return a distinct List

Calling toSet on a String converts into a set of unique characters

You can have immutable Set and MutableSet

Maps

Maps link keys to values similar to a dictionary

Use the to keyword to separate each key from its value

import atomictest.eq


fun main() {
	val constants = mapOf(
		"Pi" to 3.14,
		"e" to 2.71,
		"phi" to 1.618
	)
	
	constants eq "{Pi=3.14, e=2.71, phi=1.618}"
	//Look up a value
	constants["e"] eq 2.71
	constants.keys eq setOf("Pi", "e", "phi")
	constans.values eq "[3.14, 2.71, 1.618]"
	
	var s = ""
	//Iterate through key-value pairs
	for (entry in constants) {
		s += "${entry.key=${entry.alues}, "
	}
	s eq "Pi=3.14, e=2,71, phi=1.618"
	
	s = ""
	//Unpack during iteration
	for ((key, value) in constants)
		s += "$key=$value, "
	s eq "Pi=3.14, 2.71, phi=1.618 "
}

You can have a MutableMap using mutableMap()

import atomictest.eq


fun main() {
	val m = mutableMat(5 to "five", 6 to "six")
	m[5] eq "five
	m[5] = "5ive"
	m[5] eq "5ive"
	
	m += 4 to "four"
	
	m eq mapOf(5 to "5ive", 6 to "six", 4 to "four")
}

Map and MutableMap preserve order of insertion. This is not true of other maps.

A map returns null if it doesn’t contain an entry

Use getValue() if you want an error instead of Null. Then catch NoSuchElementException.

Use getOrDefault() to return a default value of your choice.

You can use class instances as values to a Map, but using them as keys is a little trickier.

Maps are like little databases

Property Accessors

Basic read and write of a class property

package property accessors
import atomictest.eq


class Data(var i: Int)


fun main() {
	val data = Data(10
	data.i eq 10
	data.i = 20
	data.i eq 20
}

Kotlin actually calls functions to read / write properties

To customize behavior use a getter or setter

package propertyaccessors
import atomictest.eq


class Default {
	var i: Int = 0
		get() {
			trace("get()")
			return field
		}
		set(value) {
			trace("set($value)")
			field = value
		}
}


fun main() {
	val d = Default()
	d.i = 1
	trace(d.i)
	trace eq """
		set(1)
		get()
		1
	"""
}

You do not have to have both a custom getter and setter. You can have only one of the two.

If you define a property as private both accessors become private.

You can make the setter private and the getter public. Then you can read the property outside the class, but only set the value inside the class.

You can have properties that are computed. These properties have only a get() which is computed.

If you make a computed property a val it has no setter and is purely computed.