Kotlin: An Illustrated Guide • Chapter 12

Introduction to Interfaces

Chapter cover image

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.

Farmer Sue greets her chicken.

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!

Farmer Sue greets her pig.
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 a name property and a speak() function, but it doesn’t have a numberOfEggs property. Instead, it has an excitementLevel 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)

Well, that makes sense, of course. The greet() function takes a Chicken, not a Pig.

The greet() function accepts a Chicken object, not a Pig object. class Farmer( val name : String) { fun greet (chicken: Chicken) { println ( "Good morning, ${ chicken. name } !" ) chicken. speak () } } This parameter is a Chicken, not a Pig!

To remedy this, we can add a new function that takes a Pig. We can still name the new function greet(), but instead of accepting a Chicken, this one will accept a Pig. When we create a function that has the same name as another function but different parameters, it’s called overloading the function. Let’s create an overload for greet() that accepts a Pig 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!

Farmer Sue greets her cow.

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)

Again, 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.

The three greet() functions are all the same except for the name and type of the parameter. 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 () } These are all the same except for 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 a speak() function.

The Chicken, Pig, and Cow classes each includes a name property and a speak() function. class Chicken( val name : String , var numberOfEggs : Int = 0 ) { fun speak () = println ( "Cluck!" ) } class Pig( val name : String , val excitementLevel : Int) { fun speak () { repeat ( excitementLevel ) { println ( "Oink!" ) } } } class Cow( val name : String ) { fun speak () = println ( "Moo!" ) } Each of these has a speak() function... ... and a name property.

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 a name property and a speak() function. As you might recall, so far, we’ve always created new types by using the class keyword. However, instead of introducing the FarmAnimal 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()

This makes sense - after all, since the FarmAnimal has no function body for speak(), what would we expect donkey.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 a Chicken, Pig, or Cow. To start with, let’s update the Cow class, to mark it as a FarmAnimal:

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 a name property and a speak() function, but says nothing about the sound the animal should make when speak() is called… just that the function must exist. The class, however, provides the implementation - that is, it has a speak() function that does have a function body.

A class that implements an interface needs to include a few things:

  1. 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
  2. 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.
When a class implements an interface, it must indicate which interface(s) it implements, and must add 'override' keywords to each property and function that it implements. class Cow( override val name : String) : FarmAnimal { override fun speak () = println ( "Moo!" ) } Interface name goes here "override" keyword goes here

Once we make these same changes to Chicken and Pig, we can proceed to remove all those greet() functions on the Farmer 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 the FarmAnimal interface. So, as new donkeys, goats, and llamas are added to the farm, we’ll just need to declare that they implement the FarmAnimal interface, and Farmer Sue can greet them, without any new overloads… in fact, without any changes to the Farmer 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 a FarmAnimal, all chicken objects are now both a Chicken (a more specific type) and FarmAnimal (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.

UML diagram showing a FarmAnimal interface, with three implementations - Chicken, Pig, and Cow. «interface» FarmAnimal + name: String + speak(): Unit Chicken + name: String + numberOfEggs: Int + speak(): Unit Pig + name: String + excitementLevel: Int + speak(): Unit Cow + name: String + speak(): Unit

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 a Chicken, Cow, or Pig. We already saw this with the greet(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 a Chicken.

val henrietta: FarmAnimal = Chicken("Henrietta")

Similarly, we can create a list of FarmAnimal objects, and give it a Chicken, a Cow, and a Pig. Here’s a revised version of Listing 12.7 that uses a List.

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 a Chicken, Pig, or Cow, because each of those classes implements FarmAnimal.

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 a FarmAnimal), you lose the ability to do specific things with it. For example, the Chicken class has a numberOfEggs property. You can use this property just fine when the object is assigned to a Chicken variable, like this:

val henrietta: Chicken = Chicken("Henrietta")
henrietta.numberOfEggs = 1

However, after simply changing the type of this variable from a Chicken to a FarmAnimal, you can’t do anything with numberOfEggs:

val henrietta: FarmAnimal = Chicken("Henrietta")
henrietta.numberOfEggs = 1

Why 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 a Chicken variable (as in Listing 12.16), the Chicken class isn’t wearing a mask, so you can see all of its properties and functions. However, in Listing 12.17, the Chicken object is assigned to a FarmAnimal variable, so it’s wearing a FarmAnimal mask, which only lets Kotlin see the things declared in the FarmAnimal interface - name and speak().

An object that is assigned to a variable declared with the same exact type allows you to see all of its properties and functions. However, assigning it to a variable declared with a supertype will cause any properties or functions that aren't in the supertype to be 'hidden', as if behind a mask. Chicken + name: String + numberOfEggs: Int + speak(): Unit FarmAnimal + name: String + speak(): Unit Chicken + name: String + numberOfEggs: Int + speak(): Unit FarmAnimal val henrietta: FarmAnimal = Chicken ( "Henrietta" ) val henrietta: Chicken = Chicken ( "Henrietta" ) Class "Mask" (Interface) Class wearing the mask

Because it’s wearing a mask, the properties and functions that are declared in FarmAnimal are visible, but any properties or functions declared in Chicken 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 the greet() 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")
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()
    }
}

It’s good that Kotlin prevents us from doing this. After all, if we were to pass a Cow object instead of a Chicken object, the Cow would have no numberOfEggs 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 a Chicken? To do that, we need the animal 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 to Chicken, 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 becomes Chicken. But outside of that body, it’s still a FarmAnimal.

The type of `animal` changes throughout the function. It is only a `Chicken` inside the `if` block. fun greet (animal: FarmAnimal) { println ( "Hello, ${ animal. name } !" ) if (animal is Chicken) { println ( "I see you have ${ animal . numberOfEggs } eggs today!" ) } animal. speak () } Type of animal is FarmAnimal here Type of animal is FarmAnimal again here Type of animal is Chicken here

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 a Chicken.

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 a Chicken - for example, if you called greet() with a Cow, 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 a Chicken object, then chicken would be the same object instance as animal, but would have a compile-time type of Chicken?. In other words, it took off the mask… but you still have a nullable type to deal with. On the other hand, if greet() is called with a Cow object, then the chicken 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 or as? 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 the speak() function and one for the name 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.

The mask changes depending on the type that the `Cow` object is assigned to. Named Cow + name: String + speak(): Unit Speaker + speak(): Unit Speaker Cow + name: String + speak(): Unit Named + name: String Speaker Named Cow + name: String + speak(): Unit "Masks" (Interfaces) Class wearing the "Named" mask Class wearing the "Speaker" mask

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 a name property, so we can easily update it to implement the Named 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 into Speaker and Named interfaces, though, we’ve done away with the FarmAnimal interface. That means the greet() function doesn’t work any more.

class Farmer(override val name: String) : Named {
    fun greet(animal: FarmAnimal) {
        println("Good morning, ${animal.name}!")
        animal.speak()
    }
}

Let’s fix that next!

Interface Inheritance

In order to get the greet() function to work again, we could simply reintroduce the FarmAnimal 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.

UML diagram showing three interfaces and three classes. Each class implements each interface. «interface» FarmAnimal + name: String + speak(): Unit «interface» Named + name: String «interface» Speaker + speak(): Unit Chicken + name: String + numberOfEggs: Int + speak(): Unit Pig + name: String + excitementLevel: Int + speak(): Unit Cow + name: String + speak(): Unit

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 the Speaker and Named interfaces, like this.3

interface 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 a name property and a speak() function on it. However, by inheriting from the Speaker and Named interfaces, FarmAnimal is now a subtype of them! This means that every FarmAnimal is also a Speaker and a Named.

Now we can remove the Speaker and Named declarations from Listing 12.28 above, because they come along automatically as a part of FarmAnimal:

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 type Named and Speaker as well. The result is a UML diagram that looks like this:

UML diagram showing interface extension, and the three classes implementing `FarmAnimal`. «interface» FarmAnimal + name: String + speak(): Unit «interface» Named + name: String «interface» Speaker + speak(): Unit Chicken + name: String + numberOfEggs: Int + speak(): Unit Pig + name: String + excitementLevel: Int + speak(): Unit Cow + name: String + speak(): Unit

This diagram looks much better!

Now these classes can be used in a lot of situations! For example, because Cow is a subtype of FarmAnimal, Named, and Speaker, a Cow 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 the speak() function.

interface Speaker {
    fun speak() {
        println("...")
    }
}

Now, we can create a new class that implements the FarmAnimal interface, but omits the speak() function!

class Snail(override val name: String) : FarmAnimal
Cartoon drawing of a snail.

This Snail class has no class body, let alone a speak() function. However, we can still call the speak() function on a Snail, 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"
}

Instead, 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 that get() 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 both name and speak(), 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 the FarmAnimal interface, then the Chicken, Pig, and Cow classes would all need to be updated to have that new property, or else you’d get a compile-time error. However, if the FarmAnimal interface had a default implementation for that property (for example, it could be set to null 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 the Cow 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:

In addition to interfaces, Kotlin gives us a few other ways to create new subtypes and supertypes. In the next chapter, we’ll use abstract classes and open classes to introduce new types and make our code reusable. See you then!

Thanks to James Lorenzen for reviewing this chapter!


  1. In cases where the class has no constructor or body, it’d be as easy as writing class Chicken : FarmAnimal. [return]
  2. 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 the next chapter. [return]
  3. 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 inherit name and speak(), and then add some more properties or functions of its own. [return]

Share this article:

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