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 by1.01 and make an engine sound.val myCar = Car() myCar.accelerate()Vrrrrrr...This works great for many cars because they make a “Vrrrrrr…” sound.
![]()
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”!
![]()
And whizzing by him is Rico in his 1969 muscle car! Go, Rico!
![]()
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, likeChicken,Pig, andCow.If that approach worked for animals making sounds, can it also work for cars and their engine sounds? Could we convert our
Carclass into an interface, likeFarmAnimal, and create subtypes for the different kinds of cars?Well, it’s possible to convert the
Carclass 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?Here are the changes we had to make when converting
Carinto an interface:
- Visibility - An interface cannot have
privatemembers, so we had to makespeedandmakeEngineSound()public. This means that code outside ofCarwould be able to set the speed, without going through theaccelerate()function. Similarly, it’d be possible to callmakeEngineSound()without accelerating.- 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.0from thespeedproperty. The implementing classes will have to initialize it, instead.- Instantiation - An interface cannot be instantiated. To work around this, we would have to introduce a class that implements the
Carinterface, 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
abstractmodifier when declaring it. As you can see below, the only difference between the new abstract class and the original class is the wordabstractat the beginning.So far, so good! Just like with our original
Carcode, thespeedproperty is private and initialized to0.0. ThemakeEngineSound()function is also private.There’s one problem, though. As with the interface version of
Carabove, 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
Carclass, 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.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.
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!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
Carclass.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
Clunkersubclass, we can pass it a rate of acceleration that’s slower than the default of1.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.25for all clunkers. If we don’t want all clunkers to have the same acceleration, we could also add it as a constructor parameter of theClunkerclass, and just relay the argument to the constructor of theCarclass.Relaying constructor arguments from a subclass (e.g.,
Clunker) to a superclass (e.g.,Car) is a very common thing to do. Note thataccelerationinClunker’s constructor does not include thevalorvarkeyword - we’re just passing it along to theCarclass, 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
FarmAnimalhas a name and it can speak, so we were able to inherit from both aNamedinterface and aSpeakerinterface.In the code above, the
FarmAnimalinterface inherits a few things:
- It gains the
nameproperty from theNamedinterface.- It gains the
speak()function from theSpeakerinterface.So even though the
FarmAnimalinterface 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
Clunkersubclass contains a function calledaccelerate(), even though it’s not explicitly declared in its class body, because it inherits the function fromCar.For what it’s worth, it technically also contains
acceleration,speed, andmakeEngineSound(), 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:
- 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 actualinterfacein the class declaration.- 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.
- 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 theinterfacedeclares.When a class inherits from an
interfaceor 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
interfacedoes not include a default implementation, the class inherits the interface, but must provide its own implementation, such as in theCowcode 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 : SpeakerAs 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
Clunkera special engine sound! We can’t just add theoverridekeyword, though, or we’ll get a compiler error.class Clunker(acceleration: Double) : Car(acceleration) { override fun makeEngineSound() = println("putt-putt-putt") }ErrorThe problem here is that the
makeEngineSound()has aprivatevisibility modifier in the superclass, as in Listing 14.3 above. When a function or property isprivate, 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
privatein 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 theprivatemodifier, which would make it a public function, but if we do that, then it would be possible to make the engine sound without calling theaccelerate()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, calledprotected. Let’s updatemakeEngineSound()so that it’sprotected: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
protectedwill 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 theClunkersubclass. Are we ready to override it now?class Clunker(acceleration: Double) : Car(acceleration) { override fun makeEngineSound() = println("putt-putt-putt") }ErrorWe’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
abstractmodifier to the function or property. When a function is marked withabstract…
- 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 theabstractmodifier 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-puttAgain, marking a function or property as
abstractmeans that each non-abstract subclass must implement it. But what if you wantCarto have a default implementation formakeEngineSound(), 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 ourmakeEngineSound()function so that it’sopeninstead ofabstract, 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
Clunkerstill overrides themakeEngineSound()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
abstractnoropen), 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()ErrorInstead, you have to instantiate one of its subclasses. To fix this, instead of making
Caran 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
Carclass from an abstract class to an open class by simply replacing the keywordabstractwith the keywordopen:open class Car(private val acceleration: Double = 1.0) { // ... }With this simple change, we can now instantiate a
Cardirectly.val myCar = Car()There’s a catch, though - while an open class can have functions and properties that are either
openor final, it cannot contain any that areabstract. That makes sense, though - imagine if this openCarclass had an abstract function calledhonk(), which naturally could have no body. Now, if we were to instantiate and callhonk()on the car, what could we possibly expect to happen?So again, open classes cannot contain
abstractmembers. 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 thespeedvariable, 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!") } }ErrorThe problem is that, in the
Carclass, thespeedproperty has aprivatevisibility 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
protectedmodifier so that subclasses can see thespeedproperty.open class Car(private val acceleration: Double = 1.0) { protected var speed = 0.0 // ... }With this change, our
MuscleCarcode 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
speedvalue 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
speedproperty isprotected, so subclasses ofCarcan get its value.- The
speedproperty’s setter visibility isprivate, which means only theCarclass 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), NamedThe 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 aMuscleCar, 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
Clunkeran open class, and extend it with a new class calledJunker.open class Clunker(acceleration: Double) : Car(acceleration) { override fun makeEngineSound() = println("putt-putt-putt") } class Junker : Clunker(0.0)Now,
Clunkeris both a subclass ofCarand a superclass ofJunker. 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: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
AnyTypeSupertypes 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, andFloatare all subclasses of an abstract class calledNumber.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 CircleEven 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 theAnyclass in common with each other.The
Anyclass provides a few essential functions that are inherited by all other classes -equals(),hashCode(), andtoString(). 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
Congratulations for completing this large chapter! Here’s what you learned:
- What abstract classes are.
- What it means to extend a class.
- How functions and properties are inherited from superclasses.
- The difference between the concepts of interface and implementation.
- How to override functions and properties from a superclass.
- How protected visibility gives subclasses access to the members of a superclass.
- The difference between abstract members and open members.
- How open classes can be instantiated and include default implementations.
- How getters and setters can have different visibility modifiers.
- How one class can both extend a class and implement interfaces.
- How classes form a class hierarchy.
- How the Any type is at the top of the class hierarchy in Kotlin.
In the next chapter, we’ll look at data classes. See you then!
Thanks to James Lorenzen and @gbagd24 for reviewing this chapter!
To keep things simple, I’m not including a unit of speed. If it helps, feel free to imagine that the
speedis in kilometers per hour, miles per hour, meters per second, or any other unit you like! ↩︎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. ↩︎
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. ↩︎
More precisely, this is called subtype polymorphism. There’s another kind called “parametric polymorphism”, which we typically just refer to as “generics”. ↩︎