8. Objects & Classes¶
8.1. Creating & Using Classes¶
8.1.1. A Basic Money Class¶
-
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 namedeuros
andcents
, 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, usingprintln
. You should see some unhelpful output, because a meaningfultoString
method hasn’t yet been defined in the class. -
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 aMoney
object witheuros
andcents
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¶
-
The
Money
class currently has an implicit primary constructor that simply initialiseseuros
andcents
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, anIllegalArgumentException
will be triggered, with an error message generated by the lambda expression. In this case, we just use fixed strings as the error messages. -
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
andcatch
and just invoke the constructor.
8.1.3. Adding Other Properties¶
You can define properties outside of the primary constructor if need be.
-
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 theinit
block:val asCents get() = 100*euros + cents
This is a read-only property (due to the use of
val
instead ofvar
). Its value is computed using a custom getter from the values of theeuros
andcents
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 forasCents
. -
Add code to the program that accesses
asCents
for one of yourMoney
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.
-
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) }
-
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.
-
In a new Kotlin or Kotlinscript file, start with the following simple class definition:
class Person(val name: String) { var isMarried = false }
-
Override the
toString
method to generate a nice string representation of aPerson
, then add a program that creates and prints a couple ofPerson
objects. -
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 aPerson
object is created. The creation will need to be done by a special method, associated with thePerson
class rather than with an instance of the class. The collection of previously-used names will also need to be associated withPerson
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
. -
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.
-
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)
-
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 anIllegalArgumentException
when the program is run.