Classes and inheritance in Kotlin

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 as if/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

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.

87e0a5eb0f85042d.png

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.

638655b960530d9.png

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.

c39a8aaa5b013de8.png

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.

1ce2b1646b8064ab.png

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.

de1387ca7fc26c81.png

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 of RoundTower.
  • 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.

  1. Go to the Kotlin Playground at https://developer.android.com/training/kotlinplayground.
  2. In the editor, delete println("Hello, world!") inside the main() function.
  3. Then add this code, below the main() function to create an abstract class called Dwelling.
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.

  1. Inside Dwelling, create a buildingMaterial variable of type String to represent the building material. Since the building material won't change, use val to make it an immutable variable.
val buildingMaterial: String
  1. 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.

  1. Add the abstract keyword onto the variable definition.
abstract val buildingMaterial: String
  1. Run your code, and while it does not do anything, it now compiles without errors.
  2. Make an instance of Dwelling in the main() function and run your code.
val dwelling = Dwelling()
  1. You'll get an error because you cannot create an instance of the abstract Dwelling class.
Cannot create an instance of an abstract class
  1. 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.

  1. In Dwelling, add an abstract integer val called capacity.
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.

  1. You can make residents a parameter that is passed into the constructor of the Dwelling class. The residents property is a var, 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.

  1. Add the hasRoom() function to the Dwelling class.
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. 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

  1. Below the Dwelling class, create a class called SquareCabin.
class SquareCabin
  1. Next, you need to indicate that SquareCabin is related to Dwelling. In your code, you want to indicate that SquareCabin extends from Dwelling (or is a subclass to Dwelling) because SquareCabin will provide an implementation for the abstract parts of Dwelling.

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()
  1. When extending from a superclass, you must pass in the required parameters expected by the superclass. Dwelling requires the number of residents as input. You could pass in a fixed number of residents like 3.
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)
  1. Run your code.
  2. 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.

  1. Inside the SquareCabin class, override the buildingMaterial property and assign it the value "Wood".
  2. Do the same for the capacity, saying 6 residents can live in a SquareCabin.
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

  1. Insert an empty main() function before the Dwelling and SquareCabin class definitions.
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. Within the main() function, create an instance of SquareCabin called squareCabin with 6 residents. Add print statements for the building material, the capacity, and the hasRoom() 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().

  1. 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
}
  1. In the main() function, change your print statements to use with.
  2. Delete squareCabin. in the print statements.
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. 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

  1. In the same way as the SquareCabin, add another subclass, RoundHut, to Dwelling.
  2. Override buildingMaterial and give it a value of "Straw".
  3. Override capacity and set it to 4.
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. In main(), create an instance of RoundHut with 3 residents.
val roundHut = RoundHut(3)
  1. 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()}")
}
  1. 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.

c19084f4a83193a0.png

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.

  1. Create a RoundTower class that is a subclass of RoundHut. Add the residents parameter to the constructor of RoundTower, and then pass that parameter to the constructor of the RoundHut superclass.
  2. Override the buildingMaterial to be "Stone".
  3. Set the capacity to 4.
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 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.

  1. Add the open keyword at the start of the RoundHut declaration.
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. In main(), create an instance of roundTower 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
}
  1. 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.

  1. Update the RoundTower constructor to take an additional integer parameter val floors for the number of floors. Put it after residents. Notice that you don't need to pass this to the parent RoundHut constructor because floors is defined here in RoundTower and RoundHut has no floors.
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. Run your code. There is an error when creating roundTower in the main() method, because you are not supplying a number for the floors 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.

  1. In your code, add = 2 after the declaration of floors to assign it a default value of 2.
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {
   
    ...
}
  1. Run your code. It should compile because RoundTower(4) now creates a RoundTower object instance with the default value of 2 floors.
  2. In the RoundTower class, update the capacity to multiply it by the number of floors.
override val capacity = 4 * floors
  1. 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

  1. First add an abstract floorArea() function to the Dwelling class. Return a Double. Double is a data type, like String and Int; 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.

  1. In the SquareCabin class, start with the keyword override followed by the actual implementation of the floorArea() function as shown below.
override fun floorArea(): Double {

}
  1. 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.

  1. Change the class definition of SquareCabin to add a length parameter of type Double. Declare the property as a val 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.

  1. In main() update the creation of the squareCabin instance. Pass in 50.0 to the SquareCabin constructor as the length.
val squareCabin = SquareCabin(6, 50.0)
  1. Inside the with statement for squareCabin, 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.

  1. Import PI from the Kotlin math library. Put this at the top of the file, before main().
import kotlin.math.PI
  1. Implement the floorArea() function for RoundHut.
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.

  1. Update the RoundHut constructor to pass in the radius.
open class RoundHut(
   residents: Int, 
   val radius: Double) : Dwelling(residents) {
  1. In main(), update the initialization of roundHut by passing in a radius of 10.0 to the RoundHut constructor.
val roundHut = RoundHut(3, 10.0)
  1. Add a print statement inside the with statement for roundHut.
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.

  1. Change the constructor of RoundTower to also take the radius. Put the radius after residents and before floors. It is recommended that variables with default values are listed at the end. Remember to pass radius to the parent class constructor.
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. Update the initialization of roundTower in main().
val roundTower = RoundTower(4, 15.5)
  1. And add a print statement that calls floorArea().
println("Floor area: ${floorArea()}")
  1. You can now run your code!
  2. Notice that the calculation for the RoundTower is not correct, because it is inherited from RoundHut and does not take into account the number of floors.
  3. 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.

  1. In RoundTower, update floorArea() to use the superclass implementation of floorArea(). Use the super keyword to call the function that is defined in the parent.
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. 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 for residents = residents + 1 to add 1 to the residents variable.
  1. Implement the getRoom() function in the Dwelling class.
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. Add some print statements to the with statement block for roundHut to observe what happens with getRoom() and hasRoom() 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.

2e328a198a82c793.png

  1. First import the sqrt() function from the kotlin.math library.
import kotlin.math.sqrt
  1. Implement the calculateMaxCarpetLength() function in the RoundHut class. The formula to calculate the length of the square carpet that can be fit in a circle is sqrt(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.

  1. The calculateMaxCarpetLength() method can now be called on RoundHut and RoundTower instances. Add print statements to roundHut and roundTower in the main() 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. An abstract 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

8. Learn more