Next we begin our exploration of relationships between Kotlin classes.
So far the class
es that we’ve created have stood alone—or at least we thought.
In this lesson we’ll see that all Kotlin class
es are related to each other, and how to utilize those connections to reflect real-world relationships and improve our code.
But let’s start with some debugging practice!
Create a public class
named Restaurant
.
Restaurant
should store two pieces of data: a name (as a String?
) using the property name
and a capacity (as an Int
) using the property capacity
.
Provide a constructor that allows both fields to be set, with the name first.
Design your class so that it provides both a setter and a getter for the capacity but only a getter for the name.
Finally, reject negative and zero capacity values and null
names.
Let’s begin this lesson with a puzzle:
If we compile and run this code, we see something printed.
Which is… weird!
Right?
The snippet above uses dot notation to call a method on chuchu
called toString
.
But where is that instance method defined?
Do you see it?
I don’t!
So why does this code work?
On some level, this lesson is about figuring that out.
Kotlin allows us to establish relationships between classes. Specifically, Kotlin allows one class to inherit state and behavior by extending another class. Let’s look at an example of this:
We use the :
notation to create a relationship between two classes.
In the example above, we say that Student
extends Person
.
Note also that the parent must be marked as open
.
This allows it to be extended.
If we omit this keyword, attempts to extend the class will fail:
This relationship is one way. The terminology that we use here is helpful. We refer to the class that is extended as the parent and the class that extends as the child:
This helps us remember that we cannot create circular class relationships. This won’t compile:
We can also establish multiple levels of inheritance. When we do, we use similar family-based terminology:
When we extend a class, we need to make sure that our parent class is set up properly when instances of our class are created.
For example, consider the hierarchy below.
Whenever an instance of Student
is created, we are also creating an instance of Person
.
So we need to make sure that the Person
constructor gets called.
Kotlin forces us to do this correctly.
Let’s see how:
private
private
As a final observation, note that private
still works the way that we expect.
A class that extends another does not gain access to its private
variables:
Any
Any
However, none of this really resolves our puzzle. We still don’t know why this works:
Pet
doesn’t extend anything.
So where is toString
coming from?
To fill in the missing piece of the puzzle, we need to meet the class
that sits at the top of Kotlin’s class hierarchy, Any
:
Create a class called Student
that inherits from a class called Person
.
(Do not create Person
. It is already available.)
Define a single Student
constructor that takes a String?
value (name) and an Int
value
(university ID number), in that order.
You should call the Person
constructor and pass the String
argument.
(You don't need to do anything else with it.)
Provide a publicly-readable but not writable property ID
storing the university ID number.
Reject negative ID numbers using require
.
So it’s nice and all that every class will inherit a toString
method from Any
.
But this method really isn’t very useful!
For example, given that my Pet
has a String
name
, I might want to display that instead.
Can we do this?
Yes!
Let’s look at how:
We’ll get into this more tomorrow and review exactly how Kotlin locates various method and field names when it compiles your code.
Create a class
named ScoreTracker
that we'll use to keep track of the scores of two players who are
playing a game.
Players playing this game always take turns, and Player 1 always plays first.
Both players scores start at zero.
Each ScoreTracker
should provide a method score
that accepts a number of points and does not return a value.
If it is Player 1's turn, the points (which might be negative) get added to their score.
Otherwise they get added to Player 2's score.
You should also provide a method currentlyAhead
that returns 1
if Player 1 is ahead, 2
if Player 2 is ahead,
and 0
if it's a tie.
You should not expose any of your state publicly, and ScoreTracker
does not need to provide a public constructor.
When your class is complete, here is how it should work:
Declare and implement a class
named RatingTracker
, which will store and return the average of a set
of ratings.
RatingTracker
should not provide a constructor.
It should provide one public
method: addRating
.
addRating
adds a new rating to the average, accepting a single Int
value.
You should require
that this value is between 0 and 5, inclusive.
You should create a read-only averageRating
property and override the getter appropriately so that it returns
the average of all the ratings seen so far as a Double
.
You should require
that at least one rating has been added before calling the averageRating
getter.
Note that in this case Kotlin will not create a so-called backing field for the property, since the value it
returns is created entirely from other data!
As a reminder, here's the syntax for creating that field:
When your class is completed, here's how it should work:
You should not need to use a loop, array, or list to solve this problem. You may need to cast your values carefully when computing the average.
If you’re like me, you use a search engine constantly. But have you ever stopped to consider where the results come from? The Internet is huge! Without search, most of us would never find our way around. And so what ends up on the first page of results matters. A lot.
Professor and MacArthur Award Winne Safiya Noble has examined how biases infiltrate Google and other search engine results in her seminal book “Algorithms of Oppression”. Listen to her describe some of her work and its broader implications:
Need more practice? Head over to the practice page.