Problem overview
A ticket machine gives every new ticket the next number: the first ticket is 1, the next is 2, and so on. The counter belongs to the class Ticket, not to any one ticket, so there is one copy shared by all tickets. Each ticket also keeps its own number, copied from the counter at the moment the ticket is created.
You write the class Ticket:
Ticket(name)adds 1 to the shared counter and stores that value as the ticket'snumber, next to itsname.Ticket.issued()returns how many tickets were created since the last reset.Ticket.resetCounter()sets the counter back to 0, so the next ticket is number 1 again.
Create tickets for Ann, Bob and Cy after a reset, and they get 1, 2 and 3, and issued() returns 3. If no ticket is created, issued() returns 0.
Approach: shared state versus per-object state
Two kinds of data are at work, and the whole lesson is about telling them apart.
- Per-object data is different for every ticket: the name and the number. Each object has its own copy.
- Per-class data is the counter. There is exactly one, and every ticket sees the same one.
Kotlin has no static keyword. Whatever you put in a class's companion object exists once for the whole class, and you reach it through the class name: Ticket.issued(), with no ticket needed. Inside the class, the companion's members can be used without the Ticket. prefix.
The important trap is the number. The counter keeps moving as more tickets are made, so a ticket must copy the counter's value when it is created. If a ticket only looked at the counter later, every ticket would show the last number.
Step by step
- The counter. Inside the companion object, declare
var count = 0. It is avarbecause it changes. - The ticket's own number. In the class body, declare
val number: Intwith no value. Avalwithout an initial value must be assigned exactly once before the object is finished, which is what aninitblock can do. After that it never changes, as a ticket number shouldn't. - Count the new ticket. In
init, runcount++. Every ticket counts once, whatever its name. - Keep a copy. Then write
number = count. Right after a reset, the first ticket finds the counter at 1 here, so numbering starts at 1. - issued returns
count. - resetCounter assigns 0 to
count.
The order inside init matters: increment first, then copy. Copying first would give the first ticket number 0.
The solution in Kotlin
class Ticket(val name: String) {
companion object {
var count = 0
fun issued(): Int {
return count
}
fun resetCounter() {
count = 0
}
}
val number: Int
init {
count++
number = count
}
}
Some Kotlin details here:
val namein the header makesnamea property of each ticket, set from the constructor argument.- The
initblock runs once for each object, when it is created, after the properties above it are set up. That is why it can change the sharedcountand set this ticket'snumber. - The members of the companion object are called on the class:
Ticket.issued()andTicket.resetCounter(). A ticket object has noissuedmethod of its own.
Using the class
fun main() {
Ticket.resetCounter()
val t1 = Ticket("Ann")
val t2 = Ticket("Bob")
val t3 = Ticket("Cy")
println("${t1.name} #${t1.number}")
println("${t2.name} #${t2.number}")
println("${t3.name} #${t3.number}")
println(Ticket.issued())
Ticket.resetCounter()
println(Ticket.issued())
val t4 = Ticket("Mia")
println("${t4.name} #${t4.number} ${Ticket.issued()}")
}
It prints:
Ann #1
Bob #2
Cy #3
3
0
Mia #1 1
All three tickets are printed after they were all created, and each still shows its own number. The earlier tickets keep 1, 2 and 3 even after resetCounter(): a reset changes the counter only, not tickets that exist already.
What lives where
It helps to picture the memory. The class has one box, count. Each ticket has two boxes, name and number. Creating a ticket does three things in order:
- the new object gets its empty boxes and its
namefrom the constructor argument; - the
initblock adds 1 to the one shared box; - the block copies the shared box's value into the ticket's own
numberbox.
After step 3 the ticket no longer depends on the counter. That is why tickets created earlier keep their numbers while the counter moves on, and why printing all the tickets at the end, after the last was created, still shows 1, 2, 3 and not 3, 3, 3.
Two tickets can have the same name, as in two tickets for Mia: the number comes from the counter, never from the name, so they get different numbers.
Testing it
- No tickets after a reset:
issued()returns 0. - One ticket after a reset: its number is 1 and
issued()returns 1. - Two batches with a reset between them: the second batch starts at 1 again.
- Many tickets, such as 1000: the last number equals
issued().
Common mistakes
- Making the number a function of the counter. A property like
fun number() = countwould return the current counter, so every ticket would report the last value. - Putting the counter in the class body instead of the companion. A plain
var count = 0there belongs to each ticket, so every ticket starts its own count at 0 and gets number 1. - Copying before incrementing.
number = countthencount++numbers the first ticket 0, andissued()stays one ahead of the numbers. - Forgetting the reset. The counter lives as long as the program, so a second batch of tickets continues from where the first one stopped unless
resetCounter()runs first. - Reassigning
number. It is aval, so the compiler refuses a second assignment.
