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.0
1 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
Car
class into an interface, likeFarmAnimal
, and create subtypes for the different kinds of cars?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?Here are the changes we had to make when converting
Car
into an interface:
- Visibility - An interface cannot have
private
members, so we had to makespeed
andmakeEngineSound()
public. This means that code outside ofCar
would 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.0
from thespeed
property. 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
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 wordabstract
at the beginning.So far, so good! Just like with our original
Car
code, thespeed
property is private and initialized to0.0
. ThemakeEngineSound()
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.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
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 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.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 theClunker
class, and just relay the argument to the constructor of theCar
class.Relaying constructor arguments from a subclass (e.g.,
Clunker
) to a superclass (e.g.,Car
) is a very common thing to do. Note thatacceleration
inClunker
’s constructor does not include theval
orvar
keyword - we’re just passing it along to theCar
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 aNamed
interface and aSpeaker
interface.In the code above, the
FarmAnimal
interface inherits a few things:
- It gains the
name
property from theNamed
interface.- It gains the
speak()
function from theSpeaker
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 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 actualinterface
in 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 theinterface
declares.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 theCow
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 theoverride
keyword, 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 aprivate
visibility 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
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 theprivate
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 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
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 theClunker
subclass. 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
abstract
modifier 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 theabstract
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 wantCar
to 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’sopen
instead 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
Clunker
still 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
abstract
noropen
), 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
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 keywordabstract
with the keywordopen
: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 areabstract
. That makes sense, though - imagine if this openCar
class 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
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 thespeed
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!") } }
ErrorThe problem is that, in the
Car
class, thespeed
property has aprivate
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 thespeed
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 isprotected
, so subclasses ofCar
can get its value.- The
speed
property’s setter visibility isprivate
, which means only theCar
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 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
Clunker
an 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,
Clunker
is both a subclass ofCar
and 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
Any
TypeSupertypes 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
, andFloat
are 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 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 theAny
class in common with each other.The
Any
class 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
speed
is 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”. ↩︎