1. Before you begin
Prerequisites
- Familiarity with using the Kotlin Playground for editing Kotlin programs.
- Basic concepts of programming in Kotlin as taught in Unit 1 of this course. In particular, the
main()
program, functions with arguments that return values, variables, data types and operations, as well asif/else
statements. - Able to define a class in Kotlin, create an object instance from it, and access its properties and methods.
What you'll learn
- Create a Kotlin program that uses inheritance to implement a hierarchy of classes.
- Extend a class, override its existing functionality, and add new functionality.
- Choose the correct visibility modifier for variables.
What you'll build
- A Kotlin program with different types of dwellings that are implemented as a class hierarchy.
What you need
- A computer with an internet connection to access the Kotlin Playground
2. What is a class hierarchy?
It is natural for humans to classify items that have similar properties and behavior into groups and to even form some type of hierarchy among them. For example, you can have a broad category like vegetables, and within that you can have a more specific type like legumes. Within legumes, you can have even more specific types like peas, beans, lentils, chickpeas, and soybeans for example.
This can be represented as a hierarchy because legumes contain or inherit all the properties of vegetables (e.g. they are plants and edible). Similarly, peas, beans, and lentils all have the properties of legumes plus their own unique properties.
Let's look at how you would represent this relationship in programming terms. If you make Vegetable
a class in Kotlin, you can create Legume
as a child or subclass of the Vegetable
class. That means all the properties and methods of the Vegetable
class are inherited by (meaning also available in) the Legume
class.
You can represent this in a class hierarchy diagram as shown below. You can refer to Vegetable
as the parent or superclass of the Legume
class.
You could continue and expand the class hierarchy by creating subclasses of Legume
such as Lentil
and Chickpea
. This makes Legume
both a child or subclass of Vegetable
as well as a parent or superclass of Lentil
and Chickpea
. Vegetable
is the root or top-level(or base) class of this hierarchy.
Inheritance in Android Classes
While you can write Kotlin code without using classes, and you did in previous codelabs, many parts of Android are provided to you in the form of classes, including activities, views, and view groups. Understanding class hierarchies is therefore fundamental to Android app development and allows you to take advantage of features provided by the Android framework.
For example, there is a View
class in Android that represents a rectangular area on the screen and is responsible for drawing and event handling. The TextView
class is a subclass of the View
class, which means that TextView
inherits all the properties and functionality from the View
class, plus adds specific logic for displaying text to the user.
Taking it a step further, the EditText
and Button
classes are children of the TextView
class. They inherit all the properties and methods of the TextView
and View
classes, plus add their own specific logic. For example, EditText
adds its own functionality of being able to edit text on the screen.
Instead of having to copy and paste all the logic from the View
and TextView
classes into the EditText
class, the EditText
can just subclass the TextView
class, which in turn subclasses the View
class. Then the code in the EditText
class can focus specifically on what makes this UI component different from other views.
On the top of a documentation page for an Android class on the developer.android.com website, you can see the class hierarchy diagram. If you see kotlin.Any
at the top of hierarchy, it's because in Kotlin, all classes have a common superclass Any. Learn more here.
As you can see, learning to leverage inheritance among classes can make your code easier to write, reuse, read, and test.
3. Create a base class
Class hierarchy of dwellings
In this codelab, you are going to build a Kotlin program that demonstrates how class hierarchies work, using dwellings (shelters in which people live) with floor space, stories, and residents as an example.
Below is a diagram of the class hierarchy you are going to build. At the root, you have a Dwelling
that specifies properties and functionality that is true for all dwellings, similar to a blueprint. You then have classes for a square cabin (SquareCabin
), round hut (RoundHut
), and a round tower (RoundTower
) which is a RoundHut
with multiple floors.
The classes that you will implement:
Dwelling
: a base class representing a non-specific shelter that holds information that is common to all dwellings.SquareCabin
: a square cabin made of wood with a square floor area.RoundHut
: a round hut that is made of straw with a circular floor area, and the parent ofRoundTower
.RoundTower
: a round tower made of stone with a circular floor area and multiple stories.
Create an abstract Dwelling class
Any class can be the base class of a class hierarchy or a parent of other classes.
An "abstract" class is a class that cannot be instantiated because it is not fully implemented. You can think of it as a sketch. A sketch incorporates the ideas and plans for something, but not usually enough information to build it. You use a sketch (abstract class) to create a blueprint (class) from which you build the actual object instance.
A common benefit of creating a superclass is to contain properties and functions that are common to all its subclasses. If the values of properties and implementations of functions are not known, make the class abstract. For example, Vegetables
have many properties common to all vegetables, but you can't create an instance of a non-specific vegetable, because you don't know, for example, its shape or color. So Vegetable
is an abstract class that leaves it up to the subclasses to determine specific details about each vegetable.
The declaration of an abstract class starts with the abstract
keyword.
Dwelling
is going to be an abstract class like Vegetable
. It is going to contain properties and functions that are common to many types of dwellings, but the exact values of properties and details of implementation of functions are not known.
- Go to the Kotlin Playground at https://developer.android.com/training/kotlinplayground.
- In the editor, delete
println("Hello, world!")
inside themain()
function. - Then add this code, below the
main()
function to create anabstract
class calledDwelling
.
abstract class Dwelling(){
}
Add a property for building material
In this Dwelling
class, you define things that are true for all dwellings, even if they may be different for different dwellings. All dwellings are made of some building material.
- Inside
Dwelling
, create abuildingMaterial
variable of typeString
to represent the building material. Since the building material won't change, useval
to make it an immutable variable.
val buildingMaterial: String
- Run your program and you get this error.
Property must be initialized or be abstract
The buildingMaterial
property does not have a value. In fact, you CAN'T give it a value, because a non-specific building isn't made of anything specific. So, as the error message indicates, you can prefix the declaration of buildingMaterial
with the abstract
keyword, to indicate that it is not going to be defined here.
- Add the
abstract
keyword onto the variable definition.
abstract val buildingMaterial: String
- Run your code, and while it does not do anything, it now compiles without errors.
- Make an instance of
Dwelling
in themain()
function and run your code.
val dwelling = Dwelling()
- You'll get an error because you cannot create an instance of the abstract
Dwelling
class.
Cannot create an instance of an abstract class
- Delete this incorrect code.
Your finished code so far:
abstract class Dwelling(){
abstract val buildingMaterial: String
}
Add a property for capacity
Another property of a dwelling is the capacity, that is, how many people can live in it.
All dwellings have a capacity that doesn't change. However, the capacity cannot be set within the Dwelling
superclass. It should be defined in subclasses for specific types of dwellings.
- In
Dwelling
, add anabstract
integerval
calledcapacity
.
abstract val capacity: Int
Add a private property for number of residents
All dwellings will have a number of residents
who reside in the dwelling (which may be less than or equal to the capacity
), so define the residents
property in the Dwelling
superclass for all subclasses to inherit and use.
- You can make
residents
a parameter that is passed into the constructor of theDwelling
class. Theresidents
property is avar
, because the number of residents can change after the instance has been created.
abstract class Dwelling(private var residents: Int) {
Notice that the residents
property is marked with the private
keyword. Private is a visibility modifier in Kotlin meaning that the residents
property is only visible to (and can be used inside) this class. It cannot be accessed from elsewhere in your program. You can mark properties or methods with the private keyword. Otherwise when no visibility modifier is specified, the properties and methods are public
by default and accessible from other parts of your program. Since the number of people who live in a dwelling is usually private information (compared to information about the building material or the capacity of the building), this is a reasonable decision.
With both the capacity
of the dwelling and the number of current residents
defined, you can create a function hasRoom()
to determine whether there is room for another resident in the dwelling. You can define and implement the hasRoom()
function in the Dwelling
class because the formula for calculating whether there is room is the same for all dwellings. There is room in a Dwelling
if the number of residents
is less than the capacity
, and the function should return true
or false
based on this comparison.
- Add the
hasRoom()
function to theDwelling
class.
fun hasRoom(): Boolean {
return residents < capacity
}
- You can run this code and there should be no errors. It doesn't do anything visible yet.
Your completed code should look like this:
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
4. Create subclasses
Create a SquareCabin subclass
- Below the
Dwelling
class, create a class calledSquareCabin
.
class SquareCabin
- Next, you need to indicate that
SquareCabin
is related toDwelling
. In your code, you want to indicate thatSquareCabin
extends fromDwelling
(or is a subclass toDwelling)
becauseSquareCabin
will provide an implementation for the abstract parts ofDwelling
.
Indicate this inheritance relationship by adding a colon (:
) after the SquareCabin
class name, followed by a call to initialize the parent Dwelling
class. Don't forget to add parentheses after the Dwelling
class name.
class SquareCabin : Dwelling()
- When extending from a superclass, you must pass in the required parameters expected by the superclass.
Dwelling
requires the number ofresidents
as input. You could pass in a fixed number of residents like3
.
class SquareCabin : Dwelling(3)
However, you want your program to be more flexible and allow for a variable number of residents for SquareCabins
. Hence make residents
a parameter in the SquareCabin
class definition. Do not declare residents
as val,
because you are reusing a property already declared in the parent class Dwelling
.
class SquareCabin(residents: Int) : Dwelling(residents)
- Run your code.
- This will cause errors. Take a look:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling
When you declare abstract functions and variables, it is like a promise that you will give them values and implementations later. For a variable, it means that any subclass of that abstract class needs to give it a value. For a function, it means that any subclass needs to implement the function body.
In the Dwelling
class, you defined an abstract
variable buildingMaterial
. SquareCabin
is a subclass of Dwelling
, so it must provide a value for buildingMaterial
. Use the override
keyword to indicate that this property was defined in a parent class and is about to be overridden in this class.
- Inside the
SquareCabin
class,override
thebuildingMaterial
property and assign it the value"Wood"
. - Do the same for the
capacity
, saying 6 residents can live in aSquareCabin
.
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
Your finished code should look like this.
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
To test your code, create an instance of SquareCabin
in your program.
Use SquareCabin
- Insert an empty
main()
function before theDwelling
andSquareCabin
class definitions.
fun main() {
}
abstract class Dwelling(private var residents: Int) {
...
}
class SquareCabin(residents: Int) : Dwelling(residents) {
...
}
- Within the
main()
function, create an instance ofSquareCabin
calledsquareCabin
with 6 residents. Add print statements for the building material, the capacity, and thehasRoom()
function.
fun main() {
val squareCabin = SquareCabin(6)
println("\nSquare Cabin\n============")
println("Capacity: ${squareCabin.capacity}")
println("Material: ${squareCabin.buildingMaterial}")
println("Has room? ${squareCabin.hasRoom()}")
}
Notice that the hasRoom()
function was not defined in the SquareCabin
class, but it was defined in the Dwelling
class. Since SquareCabin
is a subclass to Dwelling
class, the hasRoom()
function was inherited for free. The hasRoom()
function can now be called on all instances of SquareCabin
, as seen in the code snippet as squareCabin.hasRoom()
.
- Run your code, and it should print the following:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false
You created squareCabin
with 6
residents, which is equal to the capacity
, so hasRoom()
returns false
. You could experiment with initializing SquareCabin
with a smaller number of residents
, and when you run your program again, hasRoom()
should return true
.
Use with to simplify your code
In the println()
statements, every time you reference a property or function of squareCabin
, notice how you have to repeat squareCabin.
This becomes repetitive and can be a source of errors when you copy and paste print statements.
When you are working with a specific instance of a class and need to access multiple properties and functions of that instance, you can say "do all the following operations with this instance object" using a with
statement. Start with the keyword with
, followed by the instance name in parentheses, followed by curly braces which contain the operations you want to perform.
with (instanceName) {
// all operations to do with instanceName
}
- In the
main()
function, change your print statements to usewith
. - Delete
squareCabin.
in the print statements.
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
- Run your code again to make sure it runs without errors and shows the same output.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false
This is your completed code:
fun main() {
val squareCabin = SquareCabin(6)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
Create a RoundHut subclass
- In the same way as the
SquareCabin
, add another subclass,RoundHut
, toDwelling
. - Override
buildingMaterial
and give it a value of"Straw"
. - Override
capacity
and set it to 4.
class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- In
main()
, create an instance ofRoundHut
with 3 residents.
val roundHut = RoundHut(3)
- Add the code below to print information about
roundHut
.
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
- Run your code and your output for the whole program should be:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true
You now have a class hierarchy that looks like this, with Dwelling
as the root class and SquareCabin
and RoundHut
as subclasses of Dwelling
.
Create a RoundTower subclass
The final class in this class hierarchy is a round tower. You can think of a round tower as a round hut made of stone, with multiple stories. So, you can make RoundTower
a subclass of RoundHut
.
- Create a
RoundTower
class that is a subclass ofRoundHut
. Add theresidents
parameter to the constructor ofRoundTower
, and then pass that parameter to the constructor of theRoundHut
superclass. - Override the
buildingMaterial
to be"Stone"
. - Set the
capacity
to4
.
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- Run this code and you get an error.
This type is final, so it cannot be inherited from
This error means that the RoundHut
class cannot be subclassed (or inherited from). By default, in Kotlin, classes are final and cannot be subclassed. You are only allowed to inherit from abstract
classes or classes that are marked with the open
keyword. Hence you need to mark the RoundHut
class with the open
keyword to allow it to be inherited from.
- Add the
open
keyword at the start of theRoundHut
declaration.
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- In
main()
, create an instance ofroundTower
and print information about it.
val roundTower = RoundTower(4)
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
Here is the complete code.
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(3)
val roundTower = RoundTower(4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- Run your code. It should now work without errors and produce the following output.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true Round Tower ========== Material: Stone Capacity: 4 Has room? false
Add multiple floors to RoundTower
RoundHut
, by implication, is a single-story building. Towers usually have multiple stories (floors).
Thinking of the capacity, the more floors a tower has, the more capacity it should have.
You can modify RoundTower
to have multiple floors, and adjust its capacity based on the number of floors.
- Update the
RoundTower
constructor to take an additional integer parameterval floors
for the number of floors. Put it afterresidents
. Notice that you don't need to pass this to the parentRoundHut
constructor becausefloors
is defined here inRoundTower
andRoundHut
has nofloors
.
class RoundTower(
residents: Int,
val floors: Int) : RoundHut(residents) {
...
}
- Run your code. There is an error when creating
roundTower
in themain()
method, because you are not supplying a number for thefloors
argument. You could add the missing argument.
Alternatively, in the class definition of RoundTower
, you can add a default value for floors
as shown below. Then, when no value for floors
is passed into the constructor, the default value can be used to create the object instance.
- In your code, add
= 2
after the declaration offloors
to assign it a default value of 2.
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
...
}
- Run your code. It should compile because
RoundTower(4)
now creates aRoundTower
object instance with the default value of 2 floors. - In the
RoundTower
class, update thecapacity
to multiply it by the number of floors.
override val capacity = 4 * floors
- Run your code and notice that the
RoundTower
capacity is now 8 for 2 floors.
Here is your finished code.
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(3)
val roundTower = RoundTower(4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
}
5. Modify classes in the hierarchy
Calculate the floor area
In this exercise, you will learn how you can declare an abstract function in an abstract class and then implement its functionality in the subclasses.
All dwellings have floor area, however, depending on the shape of the dwelling, it's calculated differently.
Define floorArea() in Dwelling class
- First add an
abstract
floorArea()
function to theDwelling
class. Return aDouble
. Double is a data type, likeString
andInt
; it is used for floating point numbers, that is, numbers that have a decimal point followed by a fractional part, such as 5.8793.)
abstract fun floorArea(): Double
All abstract methods defined in an abstract class must be implemented in any of its subclasses. Before you can run your code, you need to implement floorArea()
in the subclasses.
Implement floorArea() for SquareCabin
Like with buildingMaterial
and capacity
, since you are implementing an abstract
function that's defined in the parent class, you need to use the override
keyword.
- In the
SquareCabin
class, start with the keywordoverride
followed by the actual implementation of thefloorArea()
function as shown below.
override fun floorArea(): Double {
}
- Return the calculated floor area. The area of a rectangle or square is the length of its side multiplied by the length of its other side. The body of the function will
return length * length
.
override fun floorArea(): Double {
return length * length
}
The length is not a variable in the class, and it is different for every instance, so you can add it as a constructor parameter for the SquareCabin
class.
- Change the class definition of
SquareCabin
to add alength
parameter of typeDouble
. Declare the property as aval
because the length of a building doesn't change.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
Dwelling
and therefore all its subclasses have residents
as a constructor argument. Because it's the first argument in the Dwelling
constructor, It is a best practice to also make it the first argument in all subclass constructors and put the arguments in the same order in all the class definitions. Hence insert the new length
parameter after the residents
parameter.
- In
main()
update the creation of thesquareCabin
instance. Pass in50.0
to theSquareCabin
constructor as thelength
.
val squareCabin = SquareCabin(6, 50.0)
- Inside the
with
statement forsquareCabin
, add a print statement for the floor area.
println("Floor area: ${floorArea()}")
Your code won't run, because you also have to implement floorArea()
in RoundHut
.
Implement floorArea() for RoundHut
In the same way, implement the floor area for RoundHut
. RoundHut
is also a direct subclass of Dwelling
, so you need to use the override
keyword.
The floor area of a circular dwelling is PI * radius * radius.
PI
is a mathematical value. It is defined in a math library. A library is a predefined collection of functions and values defined outside a program that a program can use. In order to use a library function or value, you need to tell the compiler that you are going to use it. You do this by importing the function or value into your program. To use PI
in your program, you need to import kotlin.math.PI
.
- Import
PI
from the Kotlin math library. Put this at the top of the file, beforemain()
.
import kotlin.math.PI
- Implement the
floorArea()
function forRoundHut
.
override fun floorArea(): Double {
return PI * radius * radius
}
Warning: If you do not import kotlin.math.PI, you will get an error, so import this library before using it. Alternatively, you could write out the fully qualified version of PI, as in kotlin.math.PI * radius * radius, and then the import statement is not needed.
- Update the
RoundHut
constructor to pass in theradius
.
open class RoundHut(
residents: Int,
val radius: Double) : Dwelling(residents) {
- In
main()
, update the initialization ofroundHut
by passing in aradius
of10.0
to theRoundHut
constructor.
val roundHut = RoundHut(3, 10.0)
- Add a print statement inside the
with
statement forroundHut
.
println("Floor area: ${floorArea()}")
Implement floorArea() for RoundTower
Your code doesn't run yet, and fails with this error:
Error: No value passed for parameter 'radius'
In RoundTower
, in order for your program to compile, you don't need to implement floorArea()
as it gets inherited from RoundHut
, but you need to update the RoundTower
class definition to also have the same radius
argument as its parent RoundHut
.
- Change the constructor of RoundTower to also take the
radius
. Put theradius
afterresidents
and beforefloors
. It is recommended that variables with default values are listed at the end. Remember to passradius
to the parent class constructor.
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
- Update the initialization of
roundTower
inmain()
.
val roundTower = RoundTower(4, 15.5)
- And add a print statement that calls
floorArea()
.
println("Floor area: ${floorArea()}")
- You can now run your code!
- Notice that the calculation for the
RoundTower
is not correct, because it is inherited fromRoundHut
and does not take into account the number offloors
. - In
RoundTower
,override floorArea()
so you can give it a different implementation that multiplies the area with the number of floors. Notice how you can define a function in an abstract class (Dwelling
), implement it in a subclass (RoundHut
) and then override it again in a subclass of the subclass (RoundTower
). It's the best of both worlds - you inherit the functionality you want, and can override the functionality you don't want.
override fun floorArea(): Double {
return PI * radius * radius * floors
}
This code works, but there's a way to avoid repeating code that is already in the RoundHut
parent class. You can call the floorArea()
function from the parent RoundHut
class, which returns PI * radius * radius
. Then multiply that result by the number of floors
.
- In
RoundTower
, updatefloorArea()
to use the superclass implementation offloorArea()
. Use thesuper
keyword to call the function that is defined in the parent.
override fun floorArea(): Double {
return super.floorArea() * floors
}
- Run your code again and
RoundTower
outputs the correct floor space for multiple floors.
Here is your finished code:
import kotlin.math.PI
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(3, 10.0)
val roundTower = RoundTower(4, 15.5)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
abstract fun floorArea(): Double
}
class SquareCabin(residents: Int,
val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
override fun floorArea(): Double {
return length * length
}
}
open class RoundHut(residents: Int,
val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
override fun floorArea(): Double {
return PI * radius * radius
}
}
class RoundTower(residents: Int, radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
override fun floorArea(): Double {
return super.floorArea() * floors
}
}
The output should be:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Floor area: 2500.0 Round Hut ========= Material: Straw Capacity: 4 Has room? true Floor area: 314.1592653589793 Round Tower ========== Material: Stone Capacity: 8 Has room? true Floor area: 1509.5352700498956
Allow a new resident to get a room
Add the ability for a new resident to get a room with a getRoom()
function that increases the number of residents by one. Since this logic is the same for all dwellings, you can implement the function in Dwelling
, and this makes it available to all subclasses and their children. Neat!
Notes:
- Use an
if
statement that only adds a resident if there is capacity left. - Print a message for the outcome.
- You can use
residents++
as a shorthand forresidents = residents + 1
to add 1 to theresidents
variable.
- Implement the
getRoom()
function in theDwelling
class.
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
- Add some print statements to the
with
statement block forroundHut
to observe what happens withgetRoom()
andhasRoom()
used together.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()
Output for these print statements:
Has room? true You got a room! Has room? false Sorry, at capacity and no rooms left.
See Solution code for details.
Fit a carpet into a round dwelling
Let's say you need to know what length of one side of the carpet to get for your RoundHut
or RoundTower
. Put the function into RoundHut
to make it available to all round dwellings.
- First import the
sqrt()
function from thekotlin.math
library.
import kotlin.math.sqrt
- Implement the
calculateMaxCarpetLength()
function in theRoundHut
class. The formula to calculate the length of the square carpet that can be fit in a circle issqrt(2) * radius
. This is explained in the above diagram.
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
Pass in a Double
value, 2.0
to the math function sqrt(2.0)
, because the return type of the function is a Double
not Integer
.
- The
calculateMaxCarpetLength()
method can now be called onRoundHut
andRoundTower
instances. Add print statements toroundHut
androundTower
in themain()
function.
println("Carpet Length: ${calculateMaxCarpetLength()}")
See Solution code for details.
Congratulations! You have created a complete class hierarchy with properties and functions, learning everything you need to create more useful classes!
6. Solution code
This is the complete solution code for this codelab, including comments.
/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/
import kotlin.math.PI
import kotlin.math.sqrt
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(3, 10.0)
val roundTower = RoundTower(4, 15.5)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Floor area: ${floorArea()}")
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()
println("Carpet size: ${calculateMaxCarpetLength()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Floor area: ${floorArea()}")
println("Carpet Length: ${calculateMaxCarpetLength()}")
}
}
/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
/**
* Calculates the floor area of the dwelling.
* Implemented by subclasses where shape is determined.
*
* @return floor area
*/
abstract fun floorArea(): Double
/**
* Checks whether there is room for another resident.
*
* @return true if room available, false otherwise
*/
fun hasRoom(): Boolean {
return residents < capacity
}
/**
* Compares the capacity to the number of residents and
* if capacity is larger than number of residents,
* add resident by increasing the number of residents.
* Print the result.
*/
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
}
/**
* A square cabin dwelling.
*
* @param residents Current number of residents
* @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
/**
* Calculates floor area for a square dwelling.
*
* @return floor area
*/
override fun floorArea(): Double {
return length * length
}
}
/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
residents: Int, val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
/**
* Calculates floor area for a round dwelling.
*
* @return floor area
*/
override fun floorArea(): Double {
return PI * radius * radius
}
/**
* Calculates the max length for a square carpet
* that fits the circular floor.
*
* @return length of square carpet
*/
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
}
/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
// Capacity depends on the number of floors.
override val capacity = floors * 4
/**
* Calculates the total floor area for a tower dwelling
* with multiple stories.
*
* @return floor area
*/
override fun floorArea(): Double {
return super.floorArea() * floors
}
}
7. Summary
In this codelab you learned how to:
- Create a class hierarchy, that is a tree of classes where children inherit functionality from parent classes. Properties and functions are inherited by subclasses.
- Create an
abstract
class where some functionality is left to be implemented by its subclasses. Anabstract
class can therefore not be instantiated. - Create subclasses of an
abstract
class. - Use
override
keyword to override properties and functions in subclasses. - Use the
super
keyword to reference functions and properties in the parent class. - Make a class
open
so that it can be subclassed. - Make a property
private
, so it can only be used inside the class. - Use the
with
construct to make multiple calls on the same object instance. - Import functionality from the
kotlin.math
library