Exercises in Kotlin: Part 5 - Classes

Exercises in Kotlin: Part 5 - Classes

Dhananjay Nene

After the last post Exercises in Kotlin: Part 4 - Control flows and return, we shall be exploring classes and objects in the exercises in this post. While retaining many similarities to Java classes, there are actually significant differences in syntax as well and these help classes become a little easier to work with in Kotlin. And save a lot of unnecessary boilerplate code.

Minimalist Syntax

Probably the minimalistic (and for all practical purposes useless) declaration of a class in Kotlin will be

class Foo

A typical class

Thats it. No constructor, no fields, no body. However as is obvious this is not particularly useful. Let us look at a more useful one.

class Account (val number: String, var balance: BigDecimal) {
    fun deposit(amount: BigDecimal) {
        balance += amount
    }
    fun withdraw(amount: BigDecimal) {
        if (balance < amount) {
            throw Exception("Insufficient funds in the account")
        }
        balance -= amount
    }
}

Here Account is the class name. It has two properties number which is a String and balance which is a BigDecimal. Another aspect is number is a read-only property ie. you can't change the value within the body of the class (say in a method) after one is provided at the construction time. As mentioned in an earlier post read-only properties (aka final in Java terms) are identified by the keyword val, while read-write properties are identified by var. Notice the succinct constructor. The properties of the class that are required by the primary constructor are specified immediately after the class name within parenthesis. You can have variables declared in the constructor that are neither vals nor vars. However they can be used only during construction time and do not become properties of the class (more about that later). Thus unlike Java you don't need to write constructors just in order copy values from the constructor parameter to a member field. Also unlike Java you do not need to write getter/ setter methods in most cases (though you could if you had a specific reason to write custom getter/setters).

Client code for the account class could perhaps use it as follows. Do note that the object is instantiated using the ClassName(parameter_list) construct. It is essentially similar to Java except that there is no new keyword preceding it.

// Instantiate object
val acc1 = Account("123", BigDecimal("456.70"))

// get value of a property and print it
println(acc1.number)

// invoke a method
acc1.deposit(BigDecimal("234.56"))

// change the value of a property
acc1.balance = BigDecimal("567.89")

Member Visibility

Now balance needs to be a var since it changes. But some might be disappointed at how it can be modified from outside the class. One solution would be to make it a private var as follows

class Account (val number: String, private var balance: BigDecimal) {
    fun deposit(amount: BigDecimal) {
        balance += amount
    }
    fun withdraw(amount: BigDecimal) {
        if (balance < amount) {
            throw Exception("Insufficient funds in the account")
        }
        balance -= amount
    }
}

fun somewhereElse() {
    val acc1 = Account("123", BigDecimal("456.70"))

    // The following wouldn't compile
    acc1.balance = BigDecimal("567.89")

    // But neither would the following line
    println(acc1.balance)
}

What if you wanted it to be read-only from outside the class but read-write within the class? You could choose to use the following approach of passing in a parameter to the constructor, using it to declare another property within the body of the class and then declaring that variable to have a private setter. Note that in this case initBalance is simply a parameter passed to the constructor (since it is not declared as either a val or a var), and can be accessed in a read only fashion only during the construction stage. (Thus other methods cannot use this parameter)

class Account (val number: String, initBalance: BigDecimal) {
    var balance = initBalance
        private set

    fun deposit(amount: BigDecimal) {
        balance += amount
    }

    fun withdraw(amount: BigDecimal) {
        if (balance < amount) {
            throw Exception("Insufficient funds in the account")
        }
        balance -= amount
    }
}

Multiple constructors

Now let us assume that we wish to allow the constructor to use a String based value for balance though the balance property on the account should continue to be a BigDecimal. You could modify the class as follows.

class Account (val number: String, initBalance: String) {
    var balance = BigDecimal(initBalance)
        private set

    fun deposit(amount: BigDecimal) {
        balance += amount
    }
    fun withdraw(amount: BigDecimal) {
        if (balance < amount) {
            throw Exception("Insufficient funds in the account")
        }
        balance -= amount
    }
}

Here balance is being initialised by constructing a BigDecimal using the String value. But we know if the value passed in is not a number, a NumberFormatException will be thrown. But we can't see that in the constructor declaration anywhere. Huh?

I won't dwell long on that, but note that Kotlin (unlike Java) does NOT use checked exceptions. Any method could throw any exception at any point in time and it does not have to be documented as a part of the method signature. A fuller discussion of this is beyond the scope of this post, but it should be stated that the conventional wisdom tends towards preferring unchecked exceptions and kotlin supports that style of programming.

Now let us assume we wish to have both types of constructors, one taking the initial balance as a String and another as a BigDecimal. Each class can have one primary and zero or more secondary constructors, each one of them eventually calling the primary constructor via the keyword this. Note the parameter lists to secondary constructors cannot declare properties using val or var since these are anyways in turn declared by the primary constructor.

class Account (val number: String, initBalance: BigDecimal) {
    constructor(number: String, initBalance: String) :
        this(number, BigDecimal(initBalance))

    var balance = initBalance
        private set

    fun deposit(amount: BigDecimal) {
        balance += amount
    }
    fun withdraw(amount: BigDecimal) {
        if (balance < amount) {
            throw Exception("Insufficient funds in the account")
        }
        balance -= amount
    }
}

So far our constructors have been only initialising properties. But what if you wanted them to do more. I have shown the way to do so by adding additional println() statements during both the primary and secondary construction phases as below.

class Account (val number: String, initBalance: BigDecimal) {
    init {
        println("Into primary constructor")
    }
    constructor(number: String, initBalance: String) :
        this(number, BigDecimal(initBalance)) {
        println("Into secondary constructor")
    }

    var balance = initBalance
        private set

    fun deposit(amount: BigDecimal) {
        balance += amount
    }
    fun withdraw(amount: BigDecimal) {
        if (balance < amount) {
            throw Exception("Insufficient funds in the account")
        }
        balance -= amount
    }
}

As can be seen the non property initialisation of primary constructor can be done in the init{...} block. Similarly secondary constructors can also include a block at the end which lists out the necessary statements to be performed.

Data Classes

Very often we need basic classes which are primarily place holders for a collection of different values. Though java does not have an equivalent, in C, you would have called it a struct and in scala you would call it a case class. In Kotlin we have data class. eg.

data class Point(val x: Double, val y: Double)

These classes are tailored for using them as collection of other properties.

  • A data class automatically generates a sensible equals()/hashcode() functions for the class based on the properties declared in the primary constructor.
  • A sensible toString() is automatically provided
  • Another helper method copy() to selectively override some property values when creating a copy is also automatically provided.
  • You can also use a data class similar to a record of values easily since it also generates component1(), component2(), ... componentN() functions which can treat the class as a record of sequence of properties (in the same order as declared in the primary constructor).

Lets take a usage at a sample usage of the class above

val p1 = Point(1.1,2.2)
println(p1)
println(p1.x)
println(p1.component1())

val p2 = p1.copy(x=3.3)
println(p2)

val (x, y) = p1
println("" + x + ":" + y)

The output for the above will be as follows

Point(x=1.1, y=2.2)
1.1
1.1
Point(x=3.3, y=2.2)
1.1:2.2

Note, how a second copy (p2) was easily created from the first while allowing the ability to override a few values. Also how the point object can be destructured into its component values, in this case vals x and y. If the Point class had 3 properties, we would have had to provide 3 variables on the left hand side of the assignment operator for it to succeed

Enum classes

Let us go back to the Account class example. Let us assume we wanted to add an account type member. Account type can either be Savings or Current (aka Checking). In order to allow the capability we will declare an enum class

enum class AccountType {
    Savings, Current
}

Now we can go ahead and modify the Account class as follows

class Account (val number: String, initBalance: BigDecimal, val type: AccountType){ ... }

fun somewhereElse() {
    val acc1 = Account("123", BigDecimal("456.7"),AccountType.Savings)
    println(acc1.type)
}

Now let us take a look at some of the sample usages of the enum class

fun somewhereElse() {
    // Iterate through all the enums
    for( accType in AccountType.values()) {
        println(accType)
    }

    // Every enum instance has two properties: name and ordinal
    println(AccountType.Savings.name)
    println(AccountType.Current.ordinal)

    // You can recreate an enum instance based on a String
    println(AccountType.valueOf("Savings"))
}

This program will result in the following output

Savings
Current
Savings
1
Savings

Thus as you can see, You could iterate through all the enum types by getting an array of all its possible types using Every enum instance has two properties, a name which is a string and an ordinal which allows the enum values to be used a set of ordered values and allows them to be comparable. The name of course is the name as was provided in the declaration and ordinal is the sequence in the declaration of values. * You can use the string value of the enum to obtain an enum instance using valueOf()

Finally you can provide additional properties to each of the enum instances as follows

enum class Color(r: Int, g: Int, b: Int) {
    Red(255,0,0),
    Green(0,255,0),
    Blue(0,0,255)
}

We have looked at the basics of classes in this post. We will look at inheritance next.