1. Previous: Chapter 13
  2. Next: Chapter 15
Kotlin: An Illustrated Guide • Chapter 14

Abstract and Open Classes

Chapter cover image

In Chapter 12, we saw how we could use interfaces to create subtypes, and in the last chapter, we saw how we could use delegation with interfaces in order to share general code among specific classes. In this chapter, we’ll learn how we can extend open and abstract classes to accomplish these same things with a different approach.

Modeling a Car

Let’s start by modeling a simple car that can increase its speed with an accelerate() function.

class Car {
    private var speed = 0.0
    private fun makeEngineSound() = println("Vrrrrrr...")

    fun accelerate() {
        speed += 1.0
        makeEngineSound()
    }
}

Each time that we call the accelerate() function, the car will increase its speed by 1.01 and make an engine sound.

val myCar = Car()
myCar.accelerate()
Vrrrrrr...

This works great for many cars because they make a “Vrrrrrr…” sound.

An average, everyday car.

But wait… here comes Old Man Keaton in his rundown, puttering clunker of a car. Instead of a smooth “Vrrrrrr…” sound, it sounds like “putt-putt-putt”!

Old Man Keaton puttering along in his Clunker.

And whizzing by him is Rico in his 1969 muscle car! Go, Rico!

Rico zooming by in his muscle car.

Well, it seems like “Vrrrrrr…” is a fine sound for lots of cars, but we’ll need to allow different kinds of cars to have different engine sounds. How can we accomplish this in Kotlin?

This problem sounds familiar. In Chapter 12, in order to create multiple animal classes that could each have its own sound, we created an interface called FarmAnimal, and classes for each specific animal, like Chicken, Pig, and Cow.

UML showing the relationship of the interface and classes from Chapter 12. «interface» FarmAnimal Chicken Pig Cow

If that approach worked for animals making sounds, can it also work for cars and their engine sounds? Could we convert our Car class into an interface, like FarmAnimal, and create subtypes for the different kinds of cars?

UML of a potential solution creating car subclasses from a Car interface. «interface» Car Clunker SimpleCar MuscleCar

Well, it’s possible to convert the Car class into an interface, but it brings with it some rather significant and undesirable changes. Here’s how it looks as an interface, compared to the original class. Can you spot the differences?

Comparison of code for a Car interface with the code for the original Car class. interface Car { var speed : Double fun makeEngineSound () = println ( "Vrrrrrr" ) fun accelerate () { speed += 1.0 makeEngineSound () } } class Car { private var speed = 0.0 private fun makeEngineSound () = println ( "Vrrrrrr" ) fun accelerate () { speed += 1.0 makeEngineSound () } } New Car Interface Original Car Class

Here are the changes we had to make when converting Car into an interface:

  1. Visibility - An interface cannot have private members, so we had to make speed and makeEngineSound() public. This means that code outside of Car would be able to set the speed, without going through the accelerate() function. Similarly, it’d be possible to call makeEngineSound() without accelerating.
  2. State - Although an interface can declare a property, it can’t contain state. In other words, it can’t itself contain a value for that property. So, we had to remove the = 0.0 from the speed property. The implementing classes will have to initialize it, instead.
  3. Instantiation - An interface cannot be instantiated. To work around this, we would have to introduce a class that implements the Car interface, and instantiate that, instead.

Those are some concerning changes, so it’d be great if we could create a subtype without introducing them. Thankfully, in addition to creating subtypes from an interface, Kotlin also allows us to create subtypes from a class. But we can’t create a subtype from just any old class. By default, a class is final, which means that Kotlin will not allow us to create subtypes from it.

Instead, we have to modify the class declaration so that Kotlin knows we want to create subtypes from it. One way to do this is with an abstract class.

Introduction to Abstract Classes

Abstract classes are a lot like interfaces, but they can include private functions, private properties, and state. To create an abstract class, just add the abstract modifier when declaring it. As you can see below, the only difference between the new abstract class and the original class is the word abstract at the beginning.

Comparing code for an abstract Car class to code for the original Car class. abstract class Car { private var speed = 0.0 private fun makeEngineSound () = println ( "Vrrrrrr" ) fun accelerate () { speed += 1.0 makeEngineSound () } } class Car { private var speed = 0.0 private fun makeEngineSound () = println ( "Vrrrrrr" ) fun accelerate () { speed += 1.0 makeEngineSound () } } New Abstract Car Class Original Car Class

So far, so good! Just like with our original Car code, the speed property is private and initialized to 0.0. The makeEngineSound() function is also private.

There’s one problem, though. As with the interface version of Car above, we can’t directly instantiate this abstract class - we can only instantiate its subtypes. Later in this chapter, we’ll remedy this, but for now, let’s see how we can create a subtype from our new abstract class!

Extending Abstract Classes

Now that we’ve got this abstract Car class, we can create a subtype from it. When we create a subtype class from an abstract class, we usually say that the subtype class extends the abstract class. This differs from when we create a subtype class from an interface, in which case we say that the class implements the interface.

UML comparing class extension with interface implementation. «interface» Car Clunker "Clunker implements the Car interface" Car Clunker "Clunker extends the Car class"

When creating a subtype from a class, the class being extended is called a superclass, and the class that’s extending it is called a subclass.

UML with notes pointing out the superclass and subclass. Car Clunker Subclass Superclass

So, how can we create a subclass from Car? Thankfully, the syntax for subtyping a class looks a whole lot like the syntax for subtyping an interface!

Comparing code for subtyping a Car class with code for subtyping a Car interface. class Clunker : Car { } class Clunker : Car () { } Subtyping a Car Interface Subtyping a Car Class

The only difference between these two is the parentheses. Why is it that we need parentheses when we subtype a class, but not when we subtype an interface? Because classes have constructors, but interfaces don’t. The parentheses here are invoking the constructor of the Car class.

This is much easier to see if we were to add a constructor parameter, so let’s add one for the rate of acceleration.

abstract class Car(private val acceleration: Double = 1.0) {
    private var speed = 0.0
    private fun makeEngineSound() = println("Vrrrrrr...")

    fun accelerate() {
        speed += acceleration
        makeEngineSound()
    }
}

Now, when we create our Clunker subclass, we can pass it a rate of acceleration that’s slower than the default of 1.0. Now that we’re passing a constructor argument, it’s easier to see that we’re calling the constructor with those parentheses!

class Clunker : Car(0.25)

In this case, we hard-coded the acceleration to 0.25 for all clunkers. If we don’t want all clunkers to have the same acceleration, we could also add it as a constructor parameter of the Clunker class, and just relay the argument to the constructor of the Car class.

Relaying a parameter from the subclass constructor to the superclass constructor. class Clunker (acceleration: Double) : Car (acceleration) val clunker = Clunker ( 0.25 )

Relaying constructor arguments from a subclass (e.g., Clunker) to a superclass (e.g., Car) is a very common thing to do. Note that acceleration in Clunker’s constructor does not include the val or var keyword - we’re just passing it along to the Car class, which will store it as a property.

Inheritance

Back in Chapter 12, we learned how one interface can inherit the functions and properties of another interface. As the example that we used in that chapter, a FarmAnimal has a name and it can speak, so we were able to inherit from both a Named interface and a Speaker interface.

UML and code demonstrating interface inheritance. «interface» FarmAnimal + name: String + speak(): Unit «interface» Named + name: String «interface» Speaker + speak(): Unit interface Named { val name : String } interface Speaker { fun speak () } interface FarmAnimal : Named, Speaker

In the code above, the FarmAnimal interface inherits a few things:

  • It gains the name property from the Named interface.
  • It gains the speak() function from the Speaker interface.

So even though the FarmAnimal interface doesn’t explicitly declare those members, it inherited them from the other interfaces.

Similarly, when extending an abstract class, the subclass will inherit the functions and properties from the superclass. So, the Clunker subclass contains a function called accelerate(), even though it’s not explicitly declared in its class body, because it inherits the function from Car.

UML and code demonstrating class inheritance. Clunker + accelerate(): Unit Car - speed: Double - acceleration: Double - makeEngineSound(): Unit + accelerate(): Unit abstract class Car( private val acceleration ) { private var speed = 0.0 private fun makeEngineSound () = fun accelerate () { /*...*/ } } class Clunker(acceleration ) : Car (acceleration)

For what it’s worth, it technically also contains acceleration, speed, and makeEngineSound(), but since those are private, they won’t be visible to the subclass. We’ll see how to work around this in a moment.

Interface and Implementation

To understand inheritance, it can be helpful to distinguish between two parts of a class:

  1. The visible function and property signatures (that is, their names, parameter types, and return types) of a class make up its interface. This term can be confusing, because languages like Kotlin also include a code element called an interface, which we covered in Chapter 12. So, when we talk about the “interface of a class”, it’s not always clear whether we’re talking about the public surface area of the class, or an actual interface in the class declaration.
  2. The code in the body of a function or property is called its implementation.

To help visualize this, let’s look at the code for one of the first classes we wrote, way back in Chapter 4 - a Circle.

class Circle(
    var radius: Double
) {
    private val pi: Double = 3.14

    fun circumference() = 2 * pi * radius
}

Now, let’s take the exact same code, but indent the implementation off to the right, in order to help distinguish between the interface and implementation.

Drawing a distinction between the external interface and the internal implementation for the Circle class. class Circle( var radius : Double ) { private val pi : Double = 3.14 fun circumference ()       = 2 * pi * radius } Interface Implementation
  • We can think of an interface as the way a class looks from the “outside” - its name, its properties and their types, its functions and their parameter and return types, and so on.
  • The implementation, on the other hand, is what a class looks like from the “inside” - its functions and properties that are not externally visible, the body of its functions and properties, and so on. Essentially, its inner workings.

So, when we say that a class is implementing an interface (referring to the code element here), what we really mean is that it’s providing an implementation - that is, the code in the body - for each function and property that the interface declares.

Code for the `Speaker` interface, and the `Cow` class, drawing a distinction between the interface and implementation. interface Speaker { fun speak () } class Cow : Speaker { override fun speak ()     = println ( "Moo!" ) } Speaker interface Cow's Interface Cow's Implementation

When a class inherits from an interface or a class, what exactly does it inherit? The interface, or the implementation?

  • In some cases, it just inherits the interface - that is, the function and property signatures. For example, when an interface does not include a default implementation, the class inherits the interface, but must provide its own implementation, such as in the Cow code above.
  • In other cases, it also inherits the implementation of those functions and properties. For example, when an interface includes a default implementation, the inheriting class can inherit that implementation, such as in the following code.
interface Speaker {
    fun speak() = println("...")
}

class Cow : Speaker

As we’ll see in a moment, these two things hold true when extending an abstract class, as well.

When a subclass inherits an implementation from its superclass, it might also have an opportunity to replace or augment the implementation that the superclass provides. This is called overriding,2 and it’s how we can customize the sound of a Clunker’s engine! Let’s look at overriding next.

Overriding Members

Just like when using delegation, you can override functions and properties from an abstract class in order to specialize the behavior of the subclass - such as to give a Clunker a special engine sound! We can’t just add the override keyword, though, or we’ll get a compiler error.

class Clunker(acceleration: Double) : Car(acceleration) {
    override fun makeEngineSound() = println("putt-putt-putt")
}
Error

The problem here is that the makeEngineSound() has a private visibility modifier in the superclass, as in Listing 14.3 above. When a function or property is private, it’s so private that even its own subclasses can’t see it! We can fix that with a different visibility modifier.

Protected Visibility

A function marked as private in the superclass isn’t visible in its subclasses. And if you can’t see it, you can’t override it! Of course, one option is to remove the private modifier, which would make it a public function, but if we do that, then it would be possible to make the engine sound without calling the accelerate() function, like this:

val car = Clunker(0.25)
car.makeEngineSound()

We only want the car to make an engine sound when accelerating. It’d be great if we could make it so that the makeEngineSound() function is visible to subclasses, but not to any other code. For these situations, Kotlin provides another visibility modifier, called protected. Let’s update makeEngineSound() so that it’s protected:

abstract class Car(private val acceleration: Double = 1.0) {
    private var speed = 0.0
    protected fun makeEngineSound() = println("Vrrrrrr...")

    fun accelerate() {
        speed += acceleration
        makeEngineSound()
    }
}

A function or property marked as protected will be visible to both the current class (e.g., Car) and its subclasses (e.g., Clunker), but invisible to code everywhere else. With this, makeEngineSound() is now visible in the Clunker subclass. Are we ready to override it now?

class Clunker(acceleration: Double) : Car(acceleration) {
    override fun makeEngineSound() = println("putt-putt-putt")
}
Error

We’re still getting a compiler error! Remember how classes are final by default? Well, it’s the same thing with functions… by default, a function is final. In other words, it can’t be overridden in subclasses unless we explicitly state that it’s allowed. There are two ways to do this.

Abstract Functions and Properties

The first way is to add the abstract modifier to the function or property. When a function is marked with abstract

  • It cannot be implemented in the abstract class, and…
  • It must be implemented in the subclass… unless the subclass itself is also abstract!

So, let’s remove the function body from makeEngineSound(), and add the abstract modifier to it.

abstract class Car(private val acceleration: Double = 1.0) {
    private var speed = 0.0
    protected abstract fun makeEngineSound() // no body allowed here!

    fun accelerate() {
        speed += acceleration
        makeEngineSound()
    }
}

With this, we can finally override the makeEngineSound() function:

class Clunker(acceleration: Double) : Car(acceleration) {
    override fun makeEngineSound() = println("putt-putt-putt")
}

And now we can instantiate and accelerate a clunker…

val clunker = Clunker(0.25)
clunker.accelerate()

…which makes that putt-putt-putt sound that follows Old Man Keaton around everywhere he goes!

putt-putt-putt

Again, marking a function or property as abstract means that each non-abstract subclass must implement it. But what if you want Car to have a default implementation for makeEngineSound(), so that subtypes don’t have to override it? For this, we have to turn to a different modifier, which we’ll explore next.

Open Functions and Properties

The second way to allow subclasses to override a function or property is to mark it as open. Open members can have a default implementation in the superclass, so that subclasses don’t have to provide their own implementation. But they can if they want to. Let’s change our makeEngineSound() function so that it’s open instead of abstract, and add the body to that function again.

abstract class Car(private val acceleration: Double = 1.0) {
    private var speed = 0.0
    protected open fun makeEngineSound() = println("Vrrrrrr...")

    fun accelerate() {
        speed += acceleration
        makeEngineSound()
    }
}

With this change, we can run the code from Listing 14.13 again, and we’ll get the exact same result, because Clunker still overrides the makeEngineSound() function.

Let’s introduce another subclass that does not override it.

class SimpleCar(acceleration: Double) : Car(acceleration)

When we instantiate it, and call accelerate()…

val car = SimpleCar(1.2)

car.accelerate()

…it’ll use the default engine sound of “Vrrrrrr…”.

Vrrrrrr...

So, to summarize, abstract classes can be extended by other classes. Their functions and properties can be:

  • abstract, in which case they have no body in the abstract class, but subclasses must implement them.
  • open, in which case they have a body in the abstract class, but subclasses may override them.
  • Final (i.e., neither abstract nor open), in which case subclasses cannot override them.

There’s still one problem with our code. As mentioned earlier, like an interface, an abstract class doesn’t let you instantiate it directly.

val myCar = Car()
Error

Instead, you have to instantiate one of its subclasses. To fix this, instead of making Car an abstract class, we can consider making it an open class.

Introduction to Open Classes

An open class is a class that can be both extended and instantiated directly. We can change our Car class from an abstract class to an open class by simply replacing the keyword abstract with the keyword open:

open class Car(private val acceleration: Double = 1.0) {
    // ...
}

With this simple change, we can now instantiate a Car directly.

val myCar = Car()

There’s a catch, though - while an open class can have functions and properties that are either open or final, it cannot contain any that are abstract. That makes sense, though - imagine if this open Car class had an abstract function called honk(), which naturally could have no body. Now, if we were to instantiate and call honk() on the car, what could we possibly expect to happen?

So again, open classes cannot contain abstract members. Next, let’s look at how we can use a visibility modifier to give subclasses special access to the functions and properties of a superclass.

Getter and Setter Visibility Modifiers

Let’s create another subclass of Car. This one’s a muscle car, and the sound of its engine depends on how fast it’s going. Unfortunately, when we try to reference the speed variable, we get a compiler error:

class MuscleCar : Car(5.0) {
    override fun makeEngineSound() = when {
        speed < 10.0 -> println("Vrooooom")
        speed < 20.0 -> println("Vrooooooooom")
        else         -> println("Vrooooooooooooooooooom!")
    }
}
Error

The problem is that, in the Car class, the speed property has a private visibility modifier on it.

open class Car(private val acceleration: Double = 1.0) {
    private var speed = 0.0
    // ...
}

As we saw earlier, we can use the protected modifier so that subclasses can see the speed property.

open class Car(private val acceleration: Double = 1.0) {
    protected var speed = 0.0
    // ...
}

With this change, our MuscleCar code from Listing 14.20 now compiles just fine!

Let’s not celebrate just yet though. With this change, subclasses can now bypass the accelerate() function, and directly set the speed to anything they want!

class Clunker(acceleration: Double) : Car(acceleration) {
    override fun makeEngineSound() {
        println("putt-putt-putt")
        speed = 999.0 // Yikes! Shouldn't be able to increase the
                        // speed without calling accelerate()!
    }
}

What we really want here is to let the subclasses get the speed value but prevent them from setting it. Thankfully, in Kotlin, a property’s getter can have a different visibility modifier from its setter. The syntax can seem a little unnatural at first, but here’s how it looks:

open class Car(private val acceleration: Double = 1.0) {
    protected var speed = 0.0
        private set
    // ...
}

This code indicates that:

  • The speed property is protected, so subclasses of Car can get its value.
  • The speed property’s setter visibility is private, which means only the Car class itself can set the value.

By the way, if you prefer to keep everything on one line, you can just use a semicolon to separate them, like this:

open class Car(private val acceleration: Double = 1.0) {
    protected var speed = 0.0; private set
    // ...
}

For what it’s worth, this is one of two occasions when I might use a semicolon in Kotlin. The other is when adding functions to an enum class.

Combining Interfaces and Abstract/Open Classes

As we saw in Chapter 12, a class can implement multiple interfaces. It’s also possible to implement interfaces and extend a class. To do this, just separate the names of the interfaces and/or superclass with a comma, like this:

class NamedCar(override val name: String) : Car(3.0), Named

The biggest critical difference between interfaces and abstract/open classes is that a subclass can only extend one class. Implement as many interfaces as you want, but you’re stuck with no more than one superclass.3 This is why interfaces can be much more flexible than abstract and open classes.

So, when should we use interfaces, and when should we use abstract or open classes?

Comparing Interfaces, Abstract Classes, and Open Classes

Between interfaces, abstract classes, and open classes, there are a lot of options for creating subtypes, and it can be hard to know which option is the most appropriate for different circumstances. Software design decisions like this are the subject of many books (and many debates!).

Although software analysis and design aren’t in scope for this book, it’s still worth summarizing the significant characteristics of each option, so I’ve included the following handy-dandy chart to help get you pointed in the right direction!

Characteristic Interface Abstract Class Open Class
Can inherit from it? Yes Yes Yes
Can inherit from multiple? Yes No No
Can be instantiated directly? No No Yes
Can include non-implemented members? Yes Yes No
Can include default implementation? Yes Yes Yes

Subclasses and Substitution

As mentioned back in Chapter 12, we can use a subtype anywhere that the Kotlin code expects a supertype. This is true not only for interfaces, but also for abstract and open classes. So, we can explicitly specify the type of a variable as a superclass (e.g., Car), while actually assigning an instance of a subclass (e.g., MuscleCar).

val car: Car = MuscleCar()

The same holds true for calling a function.

fun drive(car: Car) {
    // ...
}

drive(MuscleCar())

If a function has a parameter of type Car, it will happily receive a MuscleCar, because - by definition - the subclass has at least all the same functions and properties as its superclass. It could have more than its superclass, but it will never have fewer.

This ability to use a subtype where a supertype is expected, along with the ability of the subtypes to override functions and properties, is called *polymorphism4. It’s a big word that, apart from software development, probably doesn’t mean anything to you (unless you happen to be a biologist), but it’s still important to know, because it’s considered one of the pillars of object-oriented programming.

Class Hierarchies

So far, every subclass we’ve created has been a final class, but it’s entirely possible for a subclass itself to also be an abstract or open class. For example, a clunker that doesn’t drive at all might be classified as a “junker”. To accommodate this, we could make Clunker an open class, and extend it with a new class called Junker.

open class Clunker(acceleration: Double) : Car(acceleration) {
    override fun makeEngineSound() = println("putt-putt-putt")
}

class Junker : Clunker(0.0)

Now, Clunker is both a subclass of Car and a superclass of Junker. Once you’ve got more than a few classes, it can be helpful to visualize the relationships of the different classes with a UML class diagram, like this:

A UML class diagram of Car and its subtypes. Car Clunker Junker MuscleCar

This visualization makes it easy to see how these classes are related in a class hierarchy, where the general classes are toward the top, and as you go down the diagram, the classes become more specific. The depth of a class hierarchy is determined by how many layers of classes there are in the hierarchy. In the diagram above, we see three layers of depth.

Generally, it’s a good idea to limit the depth of a class hierarchy to only a few layers. Otherwise, it gets hard to keep track of which superclasses are providing the different functions and properties, which subclasses are depending on them, and in what ways they’re depending on them.

The Any Type

Supertypes and subtypes are not limited to our own classes and interfaces. Many of the classes in Kotlin’s standard library implement interfaces and extend abstract or open classes. For example, the basic number types like Int, Double, and Float are all subclasses of an abstract class called Number.

A UML class diagram of Number and its subtypes. Int Float Double Number

In fact, every Kotlin class that you write will have at least one superclass. For example, way back in Listing 4.1, we created the simplest class possible:

class Circle

Even though this class doesn’t explicitly extend a class, it still implicitly extends an open class called Any. This class is at the very top of the class hierarchy in Kotlin, so even classes that are otherwise unrelated have the Any class in common with each other.

A UML class diagram with Car, Circle, and Number class hierarchies, all rolling up to the Any class. Number Int Float Double Car Clunker Junker MuscleCar Circle Any

The Any class provides a few essential functions that are inherited by all other classes - equals(), hashCode(), and toString(). These three functions can be overridden, but it’s not very often that we need to do so, because Kotlin has a special kind of class that will override those functions for us, with the implementations that we usually need. We’ll learn all about that in the next chapter, as we explore data classes!

Summary

Enjoying this book?
Pick up the Leanpub edition today!

Kotlin: An Illustrated Guide is now available on Leanpub See the book on Leanpub

Congratulations for completing this large chapter! Here’s what you learned:

In the next chapter, we’ll look at data classes. See you then!

Thanks to James Lorenzen and @gbagd24 for reviewing this chapter!


  1. To keep things simple, I’m not including a unit of speed. If it helps, feel free to imagine that the speed is in kilometers per hour, miles per hour, meters per second, or any other unit you like! [return]
  2. As you might recall, we did the same kind of thing when we used class delegation in the last chapter, and the term “override” is the same as we used then. [return]
  3. Like many other programming languages, Kotlin does not allow multiple class inheritance because of the ambiguity created when two superclasses have different implementations of the same function. For more information about this, see The Diamond Problem in Wikipedia’s article on Multiple Inheritance. [return]
  4. More precisely, this is called subtype polymorphism. There’s another kind called “parametric polymorphism”, which we typically just refer to as “generics”. [return]

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit