Lesson 3 - RollingDie in Kotlin - Constructors and random numbers
In the previous lesson, First object-oriented app in Kotlin - Hello object world, we programmed our first object-oriented application in Kotlin. We are already able to create new classes, insert properties, methods with parameters and return values into them. Today, we're going to start working on an arena in which two warriors will battle against each other. The battle will be turn-based and one warrior will deduct health points from the other one based on his attack and on the other warrior's defense. Essentially, we'll be simulating a board game, so we'll add a "die" to our game so as to add a bit of randomness to the game. We're also going to learn to define custom constructors.
We'll start by creating a new project and naming it ArenaFight
.
We'll also add all necessary components such as a package
and
Main.kt
containing our main()
method. We'll add a new
class
to our project and name it RollingDie
. Let's
think about the properties we'll have to add to our die in order for it to
function according to our current needs. We should be able to choose the number
of sides. Typically 6 or 10 sides as is standard in this type of game. Our die
will also need to generate random numbers. We can do so using range
and the shuffled()
method. Our class will now have one property
only:
sidesCount
of theInt
type
Last time, for simplicity's sake, we set all the properties of our class as
publicly accessible; however, in most cases, we don't want our properties to be
modified externally. In this case, we will use the private
modifier. A property is from then on only accessible from the inside of the
class and from the outside Kotlin treats it like it doesn't exist. When we
design a class we usually set everything to private
and then we
make public
only those class members we really need to expose. In
Kotlin, when the modifier is public
, we usually just omit it, since
public
is the default one. Our class should now look something like
this:
class RollingDie { /** Number of sides that the die has */ private val sidesCount: Int }
Our code will now become underlined in red and it won't
be able to compile it. This is because Kotlin doesn't like we declared
a new property without any value. We'll solve this by adding a constructor and
initialize the sidesCount
property properly.
Сonstructors
We would now only be able to set the value of the public
properties from outside of the class since the private
properties
are not visible from the outside. We've already discussed constructors a bit. A
constructor is a method that is called while an object instance is being
created. We use it to set the internal state of the object and performs
any necessary initializations. We'd create a die in the Main.kt
now
like this:
fun main(args: Array<String>) { val rollingDie = RollingDie() // at this moment the constructor is called }
The RollingDie()
method is the constructor. Since our class
doesn't have a constructor, Kotlin automatically generated an empty method.
However, we will now add a constructor to the RollingDie
class. We
declare the body of the constructor using the init
keyword. In the
constructor, we'll set a number of sides to a fixed value. The constructor will
look like this:
init {
sidesCount = 6
}
We'll be able to compile our application now.
If we create the die now, it'll have the sidesCount
property set
to 6
. We'll print the number of sides to the console, so as to
visually confirm that the value is there. To be able to read the value from
outside, we have to set the access modifier of the sidesCount
property to public
, i.e. remove the access modifier. We've just
broken the encapsulation rule, you say? Don't worry. The property can't be
changed (it's immutable), thanks to the val
keyword. By making it
public we basically achieved the variable to be read-only (the property is
visible, we're able to read it, however, we can't change its value). Of course,
this principle works only if we initialize the property by ourselves in the
constructor first. Kotlin offers some more constructs for these purposes, but
let's not bother with them now. The property declaration will now look like
this:
val sidesCount: Int
Let's move on to Main.kt
so we could create our first die and
print the number of its sides to the console:
fun main(args: Array<String>) { val rollingDie = RollingDie() // at this moment the constructor is called println(rollingDie.sidesCount) }
The output:
6
Now we have visual confirmation that the constructor had been called. However, we'd like to be able to specify how many sides our die will have. Let's add a parameter to our constructor. In Kotlin, we specify constructor parameters in the parentheses after the class name, similarly to parameters of a function or method:
class RollingDie(aSidesCount: Int) {
val sidesCount: Int
init {
sidesCount = aSidesCount
}
}
Notice that we prefixed the parameter name with a
, otherwise it
would have the same name as the property and cause an error. Let's go back to
Main.kt
and add a value for this parameter in the constructor:
fun main(args: Array<String>) { val rollingDie = RollingDie(10) // at this moment the constructor is called println(rollingDie.sidesCount) }
The output:
10
Everything works as expected. Kotlin will not generate an empty, aka parameterless, constructor now, so we can't create a die without passing the parameter(s) it needs. We can make it possible by adding another constructor, the parameterless one this time. We'll call our parametric constructor in it and pass the number 6 to it since the user probably expects this value as default:
constructor(): this(6)
We use the this
keyword to call other
constructors, we'll explain it more later.
Let's create 2 dice instances now, one using a parameter and one without it
(in Main.kt
):
fun main(args: Array<String>) { val sixSided = RollingDie() val tenSided = RollingDie(10) println(sixSided.sidesCount) println(tenSided.sidesCount) }
The output:
6 10
Kotlin doesn't mind us having two constructors as long as their parameters are different. We say that the constructor is overloaded. This works for all methods with same names and different parameters, not just constructors. IntelliJ offers overloaded versions of constructors or methods when we're typing them. We can scroll through the method variants with the arrow keys. We can see our two constructor overloads on the list:
The same behavior could be achieved using
default arguments
, but we'll talk about that later.
Many methods in Kotlin libraries have several overloads. Take a look at the
trim()
method on the String
class. It's good to look
through method overloads, so you don't end up programming something what has
already been done for you.
Now, let's go over how to name constructor parameters better than
aSidesCount
in our case. If we named them the same as the
properties they initialize, we would cause an error:
class RollingDie(sidesCount: Init) {
init {
sidesCount = sidesCount
}
}
Kotlin can't tell the two apart. In this case, the parameter would be
assigned to the parameter again. We can refer to the current instance inside a
class because it's stored in the this
variable. We would use
this
e.g. if our die had a
giveToPlayer(player: Player)
method, calling
player.pickUpDie(this)
inside. We'd essentially be passing itself,
the concrete die instance with which we're working, to the other player by means
of the this
keyword. We won't deal with any of the "player" stuff
for now, but we'll set things up to reference the current instance while setting
the property:
class RollingDie(sidesCount: Init) { init { this.sidesCount = sidesCount } }
Using this
, we specified that the left variable,
sidesCount
, belongs to the instance. The right one is treated as a
parameter by Kotlin. Now we have two constructors that allow us to create
different dice.
Since it might be a bit lengthy to write a class with multiple properties and
then set these properties using constructor parameters, Kotlin offers a short
constructor syntax. We can declare a class property through a constructor
parameter by adding the val
or var
keyword before it.
Kotlin then knows that we want to create a class property of the same name as
the constructor parameter. In our case, the code would look like this:
class RollingDie(val sidesCount: Int) { // We declared our property in the constructor parameter constructor() : this(6) }
Random numbers
Next, we'll declare a roll()
method in the
RollingDie
class, which will return a random number from
1
to the number of sides. The method will be public
,
meaning that it can be called from outside the class, and will have no
parameters. The return value will be of the Int
type. We can obtain
a random number by generating a range of elements from 1
to the
value of the sidesCount
property, shuffling it and getting the
first element.
We can get a shuffled range (or a shuffled array) using the
shuffled()
method. We'll create the range the same way as we're
used to from for
loops:
fun roll(): Int { return (1..sidesCount).shuffled().first() }
Overriding the toString() method
Our RollingDie
class is almost ready, let's add one more method
before we wrap up this class. The toString()
method that we've
briefly gone over in previous lessons is contained in every
object, including our die. The method is designed to return a textual
representation of the instance. Which is handy in all cases where we
need to print the instance or work with it as with text. Even numerical
datatypes have this method.
We already know that Kotlin performs implicit conversions. When we want to
print an object to the console, Kotlin calls the toString()
method
and prints its return value. When creating a class, we should consider whether
toString()
will come in handy. We should never make our own method
such as print()
when there is already a way to handle string
conversions in Kotlin. It makes no sense to use it in our die, but when
implementing warriors it could return their names. Still, for education's sake,
we'll override toString()
in our RollingDie
class
anyway. It'll say that it's a die and print how many sides it has. First, let's
try to print a RollingDie
instance directly to the console:
fun main(args: Array<String>) { val sixSided = RollingDie() val tenSided = RollingDie(10) println(sixSided) }
Only the path to our class is printed to the console, which is
social.ict.RollingDie
and it's followed by the instance's hash.
We can't just declare a custom toString()
method since it's
already there (we'll find out why in upcoming tutorials). We must
override it. We won't go too far in detail for now, but I want
you to know what the proper usage of toString()
is. We use the
override
keyword to mark overriden methods:
In Kotlin, there is also a so-called data class
which can generate toString()
and another 2 methods. We'll talk
about them in the next lessons.
override fun toString(): String { return "A rolling die with $sidesCount sides" }
Let's print out the die instance to the console again.
The output:
Rolling die with 6 sides
Let's test our rolling dice now. We'll roll them in loops and see if they work as expected:
fun main(args: Array<String>) { // create instances val sixSided = RollingDie() val tenSided = RollingDie(10) // Rolls the 6-sided die println(sixSided) for (i in 0..9) { print(sixSided.roll().toString() + " ") } // Rolls the 10-sided die println("\n\n" + tenSided) for (i in 0..9) { print(tenSided.roll().toString() + " ") } }
The output should look something like this:
Rolling die with 6 sides 3 6 6 1 6 3 6 2 6 3 Rolling die with 10 sides 5 9 9 2 10 4 9 3 10 5
We've created a nice, customizable class that represents a rolling die. It will come in handy when we get to the arena part of the application, but you can use it whenever you'd like. Hopefully, you now understand how OOP allows you to reuse components. In the next lesson, Reference data types in Kotlin, we'll talk about the differences between reference data types (objects) and value data types (e.g. int).