8. Objects & Classes

8.1. Creating & Using Classes

8.1.1. A Basic Money Class

  1. In a new Kotlin or Kotlinscript file, create the following class definition:

    class Money(val euros: Int, val cents: Int)
    

    This one line does a lot. It specifies that the class Money has read-only integer properties named euros and cents, and that the class has a constructor that requires values for both of these properties to be supplied.

    Create a small program that tests this class by creating one or more instances of Money. Try printing out one of these instances, using println. You should see some unhelpful output, because a meaningful toString method hasn’t yet been defined in the class.

  2. Now add a meaningful toString method. As a first step, do the following:

    class Money(val euros: Int, val cents: Int) {
      override fun toString(): String = "Money"
    }
    

    When you’ve verified that this works, replace "Money" with something more useful. For a Money object with euros and cents values of 1 and 5, respectively, the method should return the string "€1.05".

    Hint: you can use String.format from the Java API here. The euro symbol can be produced using the Unicode escape sequence \u20ac.

8.1.2. Initialization Code

  1. The Money class currently has an implicit primary constructor that simply initialises euros and cents using the supplied values, but what if we want to do something else - e.g., check that the supplied values are valid? Such things can be done by writing an initializer block for the class. Any such blocks will be executed as part of primary constructor execution.

    Add the following code, inside the class definition but before the toString method:

    init {
      require(euros >= 0) { "euros cannot be negative" }
      require(cents >= 0) { "cents cannot be negative" }
    }
    

    The require function is from the Kotlin standard library. It takes a boolean expression as its first argument and a lambda expression as its final argument. If the boolean expression is false, an IllegalArgumentException will be triggered, with an error message generated by the lambda expression. In this case, we just use fixed strings as the error messages.

  2. Add code to the program to check that validation is working correctly. Something like this would be suitable for euros:

    try {
      Money(-1, 0)
    }
    catch (error: IllegalArgumentException) {
      println(error)
    }
    

    Catching the exception avoids us having to see the stack trace displayed. If you don’t mind this, omit the try and catch and just invoke the constructor.

8.1.3. Adding Other Properties

You can define properties outside of the primary constructor if need be.

  1. Suppose we would like Money objects to have a property that represents the amount of money expressed only using cents, not euros and cents. To introduce this property, add the following to the class definition, just after the init block:

    val asCents get() = 100*euros + cents
    

    This is a read-only property (due to the use of val instead of var). Its value is computed using a custom getter from the values of the euros and cents properties, which were themselves determined by the values supplied to the primary constructor. The Kotlin compiler is smart enough to figure out that no backing storage is needed for this property: i.e., it does not create a private field for asCents.

  2. Add code to the program that accesses asCents for one of your Money objects and prints its value.

8.1.4. Overloading Operators

Unlike in Java, operators such as +, -, etc, can be overloaded in Kotlin so that they have meaning for instances of your own classes. In the case of Money, for example, it would be handy if we could add together two amounts of money using the + operator.

  1. Add the following to the Money class definition:

    operator fun plus(other: Money): Money {
      val total = asCents + other.asCents
      return Money(total / 100, total % 100)
    }
    
  2. Modify your program so that it adds two Money objects and prints the result.

8.2. Companion Objects

Kotlin doesn’t have a static keyword. Most examples of what would be static methods in Java can be implemented as top-level functions in Kotlin. But sometimes it is necessary to write code that has access to the internals of class yet should not be associated with specific instances of the class.

For example, suppose you wanted to impose restrictions of some sort on object creation. In Java, this could be achieved by making the constructor private and then providing a static method that creates instances in a controlled way. You can do something similar in Kotlin by using companion objects.

  1. In a new Kotlin or Kotlinscript file, start with the following simple class definition:

    class Person(val name: String) {
      var isMarried = false
    }
    
  2. Override the toString method to generate a nice string representation of a Person, then add a program that creates and prints a couple of Person objects.

  3. Now suppose you wish to impose the restriction that every Person must have a unique name. To do this, you will need to store all previously used names and check against this collection of names whenever a Person object is created. The creation will need to be done by a special method, associated with the Person class rather than with an instance of the class. The collection of previously-used names will also need to be associated with Person rather than any specific instance.

    Start by making the constructor private. Change the beginning of the class definition so it looks like this:

    class Person private constructor(val name: String) {
    

    Your program won’t compile, since code outside the class is no longer allowed to invoke the constructor of Person.

  4. Now create a companion object with a create method, by adding the following to the class:

    companion object Factory {
      fun create(name: String): Person {
        return Person(name)
      }
    }
    

    Modify your program so that you now create Person objects like this:

    val nick = Person.create("Nick", true)
    

    The program should now compile and run just as it did before you make the constructor private.

  5. Finally, modify the companion object so that it imposes the desired restriction on Person names. You’ll need to give the companion object a private property that stores previously seen names:

    private val names = mutableSetOf<String>()
    

    Then you’ll need to add code to the companion object’s create method so that it checks against this set of names, generates an exception if the name is already in use, and otherwise updates the set of names:

    require(name !in names) { "name already in use" }
    names.add(name)
    
  6. To verify that the companion object is doing its job properly, modify your program so that it attempts to create a new Person object with the same name as one of the existing objects. You should find that this new bit of code generates an IllegalArgumentException when the program is run.