Ever since Chapter 1, we used a variety of built-in Kotlin types, like Int
, String
, and Bool
. Then we introduced our own custom types, like Circle
, by writing classes. In this chapter, we’ll dive into interfaces, which will allow objects to have more than one type at a time!
We’ve got lots of fun stuff to cover, so let’s get started!
Sue Starts a Farm
Sue just bought a lot of land out in the country, and she’s ready to start her farm! To kick things off, she got her very first chicken, named Henrietta. Every morning, Sue greets Henrietta, and Henrietta faithfully clucks a greeting back to Sue.
Let’s write some Kotlin code to represent Henrietta the chicken and Sue the farmer.
class Chicken(val name: String, var numberOfEggs: Int = 0) { fun speak() = println("Cluck!") } class Farmer(val name: String) { fun greet(chicken: Chicken) { println("Good morning, ${chicken.name}!") chicken.speak() } }
Now, we can instantiate the two classes, and Sue can greet Henrietta.
val sue = Farmer("Sue") val henrietta = Chicken("Henrietta") sue.greet(henrietta)
When you run this, you get the following output:
Good morning, Henrietta! Cluck!Now that Sue’s farm is off to a good start, she’s ready to add another animal resident - this time it’s a pig! Farmer Sue will also greet her pig, Hamlet, every morning!
class Pig(val name: String, val excitementLevel: Int) { fun speak() { repeat(excitementLevel) { println("Oink!") } } }
This looks very similar to the
Chicken
class. It also has aname
property and aspeak()
function, but it doesn’t have anumberOfEggs
property. Instead, it has anexcitementLevel
property that determines how many “oink” sounds it makes. We can update the script so that Sue greets both the chicken and the pig, but when we do that, we’ll get a compile-time error:val sue = Farmer("Sue") val henrietta = Chicken("Henrietta") val hamlet = Pig("Hamlet", 1) sue.greet(henrietta) sue.greet(hamlet)
ErrorWell, that makes sense, of course. The
greet()
function takes aChicken
, not aPig
.To remedy this, we can add a new function that takes a
Pig
. We can still name the new functiongreet()
, but instead of accepting aChicken
, this one will accept aPig
. When we create a function that has the same name as another function but different parameter types, it’s called overloading the function. Let’s create an overload forgreet()
that accepts aPig
object.class Farmer(val name: String) { fun greet(chicken: Chicken) { println("Good morning, ${chicken.name}!") chicken.speak() } fun greet(pig: Pig) { println("Good morning, ${pig.name}!") pig.speak() } }
With this update, the code in Listing 12.4 now compiles and runs as expected.
Now that Sue’s farm is doing great with a chicken and pig, she’s ready to add a cow! As with the other animals, she greets her cow, Dairy Godmother, every morning, and hears a “Moo” in response!
Let’s create a class for a
Cow
.class Cow(val name: String) { fun speak() = println("Moo!") }
As with the pig, when we try to make Sue greet the cow, we get an error.
val sue = Farmer("Sue") val henrietta = Chicken("Henrietta") val hamlet = Pig("Hamlet", 1) val dairyGodmother = Cow("Dairy Godmother") sue.greet(henrietta) sue.greet(hamlet) sue.greet(dairyGodmother)
ErrorAgain, to fix this, we can add a new overload to greet the pig.
class Farmer(val name: String) { fun greet(chicken: Chicken) { println("Good morning, ${chicken.name}!") chicken.speak() } fun greet(pig: Pig) { println("Good morning, ${pig.name}!") pig.speak() } fun greet(cow: Cow) { println("Good morning, ${cow.name}!") cow.speak() } }
Yikes! This is becoming unwieldy! Every time Sue adds a new kind of farm animal, we have to create another overload of the
greet()
function. That’s a shame, because Farmer Sue has some big plans! She wants to add a donkey, a goat, and a llama! That means even more overloads…All of these functions are so similar. In fact, the only difference is the name and type of the parameter.
Instead of adding a new function for each new farm animal, it’d be amazing if the
greet()
function could work with any kind of farm animal, whether it’s a chicken, pig, cow, donkey, goat, llama, or anything else. In other words, we want something like this:class Farmer(val name: String) { fun greet(animal: FarmAnimal) { println("Good morning, ${animal.name}!") animal.speak() } }
Thankfully, Kotlin gives us an easy way to do this - with interfaces!
Introducing Interfaces
When we look at the animal classes, we can see that they all look very similar - they all have a
name
property and aspeak()
function.Any new animals that join Susan’s farm will also have a name and a sound that they make when they speak to her. So, we can introduce a new type called
FarmAnimal
, and give it aname
property and aspeak()
function. As you might recall, so far, we’ve always created new types by using theclass
keyword. However, instead of introducing theFarmAnimal
type with a class, we’ll use an interface.Like a class, an interface describes what properties and functions a type has. However, unlike a class, you don’t have to actually include any function bodies! For example, we can create the
FarmAnimal
interface like this:interface FarmAnimal { val name: String fun speak() }
Notice that we didn’t add any body to the
speak()
function here.One difference between classes and interfaces, though, is that we can’t instantiate an interface. For example, this won’t work:
val donkey = FarmAnimal("Phyllis") donkey.speak()
ErrorThis makes sense - after all, since the
FarmAnimal
has no function body forspeak()
, what would we expectdonkey.speak()
to actually do?So then, if interfaces cannot be instantiated, how can we use them?
We update our existing classes, to tell Kotlin that each of them is a
FarmAnimal
in addition to being aChicken
,Pig
, orCow
. To start with, let’s update theCow
class, to mark it as aFarmAnimal
:class Cow(override val name: String) : FarmAnimal { override fun speak() = println("Moo!") }
When a class is marked with an interface like this, we say that the class implements the interface. In other words, the
FarmAnimal
interface says that each implementing class must include aname
property and aspeak()
function, but says nothing about the sound the animal should make whenspeak()
is called… just that the function must exist. The class, however, provides the implementation - that is, it has aspeak()
function that does have a function body.A class that implements an interface needs to include a few things:
- It must declare that it implements the interface. To do this, add a colon and the name of the interface between the primary constructor and the opening brace of the class body.1
- It must add the
override
keyword to each property and function that the class implements from the interface. This keyword tells Kotlin that this property or function corresponds to the one from the interface.Once we make these same changes to
Chicken
andPig
, we can proceed to remove all thosegreet()
functions on theFarmer
class, and replace them with a single function, as we did in Listing 12.9 (repeated here):class Farmer(val name: String) { fun greet(animal: FarmAnimal) { println("Good morning, ${animal.name}!") animal.speak() } }
And now, we can call
greet()
with any class that implements theFarmAnimal
interface. So, as new donkeys, goats, and llamas are added to the farm, we’ll just need to declare that they implement theFarmAnimal
interface, and Farmer Sue can greet them, without any new overloads… in fact, without any changes to theFarmer
class.Let’s look closer at the relationship between classes and the interfaces that they implement, to understand why this works.
Subtypes and Supertypes
Any tangible, real-life object can usually be categorized in multiple ways. For example, if someone points to a chicken and asks you what it is, you might say “it’s a chicken”, or you might say, “it’s a farm animal.” One is more specific, and one is more general - but both are correct.
This idea of specific and general types also applies in Kotlin. Now that the
Chicken
class is marked as aFarmAnimal
, all chicken objects are now both aChicken
(a more specific type) andFarmAnimal
(a more general type).When we’re talking about types in Kotlin…
- A class that implements an interface is called a subtype of that interface, because the class is a more specific (or, “lower” - and therefore “sub”) type.
- Conversely, an interface is called a supertype of a class that implements it, because an interface is a more general (or, “higher” - and therefore “super”) type.
Back in Chapter 4, we created some UML diagrams to describe our classes. Here’s a simple diagram showing that subtype/supertype relationship.
Subtypes and Substitution
A specific type is suitable when someone requests a more general type. For example, if Farmer Sue asks you for a “farm animal” and you give her a chicken, she would be satisfied because a chicken is indeed a kind of farm animal. Alternatively, you could give her a cow or a pig. She would be satisfied with any of those, because each of those is a kind of farm animal.
Similarly, in Kotlin, you can use a subtype anywhere that the code expects a supertype. So for instance, if a variable, property, or function expects a
FarmAnimal
, you can give it aChicken
,Cow
, orPig
. We already saw this with thegreet(animal)
function in Listing 12.13, but this also applies to variables.To demonstrate this, we can explicitly specify the type of a variable as
FarmAnimal
, but assign it aChicken
.val henrietta: FarmAnimal = Chicken("Henrietta")
Similarly, we can create a list of
FarmAnimal
objects, and give it aChicken
, aCow
, and aPig
. Here’s a revised version of Listing 12.7 that uses aList
.val sue = Farmer("Sue") val animals: List<FarmAnimal> = listOf( Chicken("Henrietta"), Pig("Hamlet", 1), Cow("Dairy Godmother"), ) animals.forEach { sue.greet(it) }
In both of these cases, we declared a type of
FarmAnimal
, but were able to provide aChicken
,Pig
, orCow
, because each of those classes implementsFarmAnimal
.However, when you assign an object with a more specific type (e.g., a
Chicken
object) to a more general variable or parameter (e.g., one whose type is aFarmAnimal
), you lose the ability to do specific things with it. For example, theChicken
class has anumberOfEggs
property. You can use this property just fine when the object is assigned to aChicken
variable, like this:val henrietta: Chicken = Chicken("Henrietta") henrietta.numberOfEggs = 1
However, after simply changing the type of this variable from a
Chicken
to aFarmAnimal
, you can’t do anything withnumberOfEggs
:val henrietta: FarmAnimal = Chicken("Henrietta") henrietta.numberOfEggs = 1
ErrorWhy is that?
When an object of a specific type is assigned to a variable of a more general type, it’s kind of like the object is wearing a mask. That mask hides the things that are declared in the specific type, but lets you see through to the properties and functions that are declared in the more general type.
For example, when assigning a
Chicken
object to aChicken
variable (as in Listing 12.16), theChicken
class isn’t wearing a mask, so you can see all of its properties and functions. However, in Listing 12.17, theChicken
object is assigned to aFarmAnimal
variable, so it’s wearing aFarmAnimal
mask, which only lets Kotlin see the things declared in theFarmAnimal
interface -name
andspeak()
.Because it’s wearing a mask, the properties and functions that are declared in
FarmAnimal
are visible, but any properties or functions declared inChicken
are hiding behind the mask, so they can’t be seen. In some cases, this might prevent you from doing something you want to do. For example, if we update thegreet()
function so that Farmer Sue also says how many eggs she sees, we’ll get an error at compile time.val chicken: Chicken = Chicken("Henrietta") farmer.greet(chicken) class Farmer(val name: String) { fun greet(animal: FarmAnimal) { println("Hello, ${animal.name}!") println("I see you have ${animal.numberOfEggs} eggs today!") animal.speak() } }
ErrorIt’s good that Kotlin prevents us from doing this. After all, if we were to pass a
Cow
object instead of aChicken
object, theCow
would have nonumberOfEggs
property, so it wouldn’t make sense to print out a line about eggs at all!So, how can we get Kotlin to print the line about eggs only when the
animal
actually is aChicken
? To do that, we need theanimal
object to cast aside its mask!Casting
If you want to use a property or function that’s declared on a subtype, you have to tell the object to take off that metaphorical mask first. Changing the type, such as from
FarmAnimal
toChicken
, is called casting. There are a few ways to do this.Smart Casts
One common way to cast the type is to use a conditional with the
is
keyword, like this:fun greet(animal: FarmAnimal) { println("Hello, ${animal.name}!") if (animal is Chicken) { println("I see you have ${animal.numberOfEggs} eggs today!") } animal.speak() }
Now, inside the body of that conditional, the type of
animal
becomesChicken
. But outside of that body, it’s still aFarmAnimal
.This is called a smart cast. If this looks familiar, it’s because we already saw a smart cast in Chapter 6 when we looked at Kotlin’s null-safety features. It’s the same thing here, but instead of casting from a nullable to a non-nullable type, we’re casting from a
FarmAnimal
to aChicken
.Smart casts can only be used when Kotlin can be certain that the value won’t change between the conditional and the expression where it’s used. For example, in the code above, it’s not possible for the value of
animal
to be reassigned, so Kotlin knows it’s safe to use a smart cast here.However, in other situations, it’s entirely possible for the value to change after the conditional was evaluated. For example, Kotlin won’t smart cast a
var
property of a class, because it’s possible that other code might be running at the same time, and that code could reassign a new value to that property.. (So far, we haven’t written any code that runs concurrently like that, but we’ll see that in a future chapter about coroutines!)Explicit Casts
Smart casts are an easy way to cast a type, but you can also cast them explicitly yourself. To do this, you can use the
as
keyword. Here’s how that looks:fun greet(animal: Animal) { println("Hello, ${animal.name}!") val chicken: Chicken = animal as Chicken println("I see you have ${chicken.numberOfEggs} eggs today!") animal.speak() }
The problem with this is that if
animal
is not actually aChicken
- for example, if you calledgreet()
with aCow
, then you’ll get an error at runtime. That’s why this is sometimes called an unsafe cast.Alternatively, you can use the
as?
keyword (with the question mark), which is called a safe cast. Here’s how it looks.fun greet(animal: FarmAnimal) { println("Hello, ${animal.name}!") val chicken: Chicken? = animal as? Chicken chicken?.let { println("I see you have ${it.numberOfEggs} eggs today!") } animal.speak() }
The
as?
operator will try to cast the object to the specified type. It will evaluate to one of two things:
- If that object actually is that specified type, then it evaluates to that object.
- Otherwise, it evaluates to null.
In the code above, if
greet()
is called with aChicken
object, thenchicken
would be the same object instance asanimal
, but would have a compile-time type ofChicken?
. In other words, it took off the mask… but you still have a nullable type to deal with. On the other hand, ifgreet()
is called with aCow
object, then thechicken
variable would be null.Keep in mind that because
as?
evaluates to a nullable type, you’ve got to use null-safety tooling in order to deal with the null. For example, in Listing 12.21, we used a scope function for a null check.A smart cast tends to be the more elegant approach in many cases, so if you find yourself needing to do a cast, that’s a great place to start. Consider using
as
oras?
only if it fits the situation well.Multiple Interfaces
It’s possible for a class to implement more than one interface. To demonstrate this, let’s split up the
FarmAnimal
interface into two separate interfaces - one for thespeak()
function and one for thename
property:interface Speaker { fun speak() } interface Named { val name: String }
To update the classes so that they implement both of these interfaces, simply separate the names of the interfaces with a comma, like this:
class Cow(override val name: String) : Speaker, Named { override fun speak() = println("Moo!") }
When you split things up into multiple interfaces like this, it’s as if you’re creating multiple “masks”, each of which exposes only a small part of the class.
This makes it possible to use the type in a broader variety of situations. For example, you could imagine collecting a roster of everyone on the farm, including Farmer Sue. The
Farmer
class already has aname
property, so we can easily update it to implement theNamed
interface.class Farmer(override val name: String) : Named { // (eliding the class body for now) }
With that change, now we can collect a list of everyone on the farm!
val roster: List<Named> = listOf( Farmer("Sue"), Chicken("Henrietta"), Pig("Hamlet", 1), Cow("Dairy Godmother") )
By splitting the
FarmAnimal
interface intoSpeaker
andNamed
interfaces, though, we’ve done away with theFarmAnimal
interface. That means thegreet()
function doesn’t work any more.class Farmer(override val name: String) : Named { fun greet(animal: FarmAnimal) { println("Good morning, ${animal.name}!") animal.speak() } }
ErrorLet’s fix that next!
Interface Inheritance
In order to get the
greet()
function to work again, we could simply reintroduce theFarmAnimal
interface, like this…interface Speaker { fun speak() } interface Named { val name: String } interface FarmAnimal { val name: String fun speak() }
… and then update the classes so that they implement all three of these interfaces, like this:
class Cow(override val name: String) : Speaker, Named, FarmAnimal { override fun speak() = println("Moo!") }
This results in a diagram that looks like this.
Wow - there are lots of lines going everywhere! When a diagram is this confusing to look at, it usually means there’s room for improvement in our code. Although this three-interface approach works, Kotlin gives us a more concise way to do this: an interface can inherit from other interfaces.2
When this happens, it automatically includes all of the properties and functions from the interfaces that it inherits from. For example, we can update the code from Listing 12.27 so that
FarmAnimal
inherits from both theSpeaker
andNamed
interfaces, like this.3interface Speaker { fun speak() } interface Named { val name: String } interface FarmAnimal : Speaker, Named
As in Listing 12.27, a class that implements this
FarmAnimal
interface will still need to have aname
property and aspeak()
function on it. However, by inheriting from theSpeaker
andNamed
interfaces,FarmAnimal
is now a subtype of them! This means that everyFarmAnimal
is also aSpeaker
and aNamed
.Now we can remove the
Speaker
andNamed
declarations from Listing 12.28 above, because they come along automatically as a part ofFarmAnimal
:class Cow(override val name: String) : FarmAnimal { override fun speak() = println("Moo!") }
Even though this class only declares that it’s a
FarmAnimal
, it’s also still of typeNamed
andSpeaker
as well. The result is a UML diagram that looks like this:This diagram looks much better!
Now these classes can be used in a lot of situations! For example, because
Cow
is a subtype ofFarmAnimal
,Named
, andSpeaker
, aCow
object can be sent as an argument to any of these functions:fun milk(cow: Cow) = // ... fun feed(animal: FarmAnimal) = // ... fun introduce(name: Named) = // ... fun listenTo(speaker: Speaker) = // ...
Default Implementations
Default Functions in Interfaces
Most of the time, interfaces themselves do not contain any code - they don’t usually include function bodies. However, you can actually include a function body, in order to provide a default implementation. This default implementation will be used if the class doesn’t provide its own implementation of that function. For example, let’s update the
Speaker
interface so that it has a default implementation of thespeak()
function.interface Speaker { fun speak() { println("...") } }
Now, we can create a new class that implements the
FarmAnimal
interface, but omits thespeak()
function!class Snail(override val name: String) : FarmAnimal
This
Snail
class has no class body, let alone aspeak()
function. However, we can still call thespeak()
function on aSnail
, and when we do that, it’ll print...
, suggesting that the snail doesn’t say much!val snail = Snail("Slick") snail.speak() // prints "..."
Default Properties in Interfaces
You can also provide a default implementation for properties, but you can’t just directly assign a value. For example, this won’t work:
interface Named { val name: String = "No name" }
ErrorInstead, you can create a getter for the property. Some programming languages call this a computed property.
interface Named { val name: String get() = "No name" }
A getter is essentially an underlying function that is called whenever you get the property’s value. So when you do
println(something.name)
, it calls thatget()
function and evaluates to the result of that function. Here, we’re simply returning the value,"No name"
.Now that
FarmAnimal
has a default implementation for bothname
andspeak()
, we can create a class that implements the interface, but has no properties or functions at all.class UnkownAnimal : FarmAnimal val unknown = UnknownAnimal()
Default implementations can be especially helpful when adding a new property or function to an existing interface. For example, if we were to add a new
nickname
property to theFarmAnimal
interface, then theChicken
,Pig
, andCow
classes would all need to be updated to have that new property, or else you’d get a compile-time error. However, if theFarmAnimal
interface had a default implementation for that property (for example, it could be set tonull
by default), then the code would compile successfully, even with no other changes to the classes. Then, if cows were the only ones who ever had nicknames, you could implement that property only in theCow
class.Summary
Well, Sue’s farm is now in great shape! As she adds more and more animals, she’ll be able to greet them all with ease. This chapter introduced the concept of an interface, and covered these topics:
- Subtypes and supertypes.
- Casting types with smart casts, unsafe casts, and safe casts.
- Implementing multiple interfaces.
- Inheriting from an interface.
- Default implementations.
In the next chapter, we’ll see how we can use interfaces to easily delegate function and property calls to other objects. See you then!
Thanks to James Lorenzen for reviewing this chapter!
In cases where the class has no constructor or body, it’d be as easy as writing
class Chicken : FarmAnimal
. ↩︎Some languages call this “extending” another interface, but since this term could easily be confused with extensions, Kotlin developers prefer to call this “inheriting”. Inheritance applies to more than just interfaces, as we’ll see in Chapter 14. ↩︎
The
FarmAnimal
interface here does not include a body, but often when you extend an interface, you would give it one. That way,FarmAnimal
would inheritname
andspeak()
, and then add some more properties or functions of its own. ↩︎