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
- public
- private
- protected
- internal
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.