Introduction to Interfaces
source link: https://typealias.com/start/kotlin-interfaces/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Introduction to Interfaces
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()
}
}
Listing 12.1
-
Classes to represent a chicken and a farmer.
Now, we can instantiate the two classes, and Sue can greet Henrietta.
val sue = Farmer("Sue")
val henrietta = Chicken("Henrietta")
sue.greet(henrietta)
Listing 12.2
-
Instantiating classes, and calling the greet()
function.
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!")
}
}
}
Listing 12.3
-
Adding a Pig
class.
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)
Listing 12.4
-
Error: Type mismatch: Required Chicken. Found Pig.
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.classFarmer(valname: String) {fungreet(chicken: Chicken) {println("Good morning,${chicken.name}!")chicken.speak()}}This parameter is aChicken, 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()
}
}
Listing 12.5
-
Adding a greet(pig)
function - which is very similar to greet(chicken)
.
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!")
}
Listing 12.6
-
A Cow
class, which also has a name
property and a speak()
function.
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)
Listing 12.7
-
Error: None of the following functions can be called with the arguments supplied…
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()
}
}
Listing 12.8
-
Adding a third greet()
function. This one accepts a Cow
object.
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.fungreet(chicken: Chicken) {println("Good morning,${chicken.name}!")chicken.speak()}fungreet(pig: Pig) {println("Good morning,${pig.name}!")pig.speak()}fungreet(cow: Cow) {println("Good morning,${cow.name}!")cow.speak()}These are all the same except for the name andtype 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()
}
}
Listing 12.9
-
What we want: a function that can accept any kind of farm animal.
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.classChicken(valname: String,varnumberOfEggs: Int =0) {funspeak() =println("Cluck!")}classPig(valname: String,valexcitementLevel: Int) {funspeak() {repeat(excitementLevel) {println("Oink!")}}}classCow(valname: String) {funspeak() =println("Moo!")}Each of thesehas a speak()function...... and anameproperty.
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()
}
Listing 12.10
-
An interface that can represent any kind of farm animal.
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()
Listing 12.11
-
Error: Interface FarmAnimal does not have constructors.
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!")
}
Listing 12.12
-
Updating the Cow
class so that it implements the FarmAnimal
interface.
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:
- 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
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()
}
}
Listing 12.13
-
Updating the greet()
function to work with any implementations of the FarmAnimal
interface.
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(): UnitChicken+ name: String+ numberOfEggs: Int+ speak(): UnitPig+ name: String+ excitementLevel: Int+ speak(): UnitCow+ 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")
Listing 12.14
-
Explicitly specifying a supertype.
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) }
Listing 12.15
-
Using FarmAnimal
in a List
to put multiple implementations into a single collection.
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
Listing 12.16
-
Explicitly specifying the subtype instead of the supertype.
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
Listing 12.17
-
Error: Unresolved reference: numberOfEggs
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(): UnitFarmAnimal+ name: String+ speak(): UnitChicken+ name: String+ numberOfEggs: Int+ speak(): UnitFarmAnimalvalhenrietta: FarmAnimal =Chicken("Henrietta")valhenrietta: Chicken =Chicken("Henrietta")Class"Mask"(Interface)Class wearingthe 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()
}
}
Listing 12.18
-
Error: Unresolved reference: numberOfEggs
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()
}
Listing 12.19
-
Using an if
conditional with is
to perform a smart cast.
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.fungreet(animal: FarmAnimal) {println("Hello,${animal.name}!")if(animalisChicken) {println("I see you have${animal.numberOfEggs}eggs today!")}animal.speak()}Type of animal isFarmAnimal hereType of animal isFarmAnimal again hereType of animal isChicken 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()
}
Listing 12.20
-
Explicitly casting to a Chicken. This is an ‘unsafe cast’.
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? Chickenchicken?.let { println("I see you have ${it.numberOfEggs} eggs today!") }
animal.speak()
}
Listing 12.21
-
An explicit safe cast.
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.
Best Practice: Careful with Casting
Many developers prefer to avoid casting when possible, because it can make things harder to manage later.
For example, in Listing 12.13, the greet()
function only knew about the Animal
interface, but didn’t need to know anything about its subtypes. This was nice because changes you make to the Chicken
class wouldn’t require you to make changes to the greet()
function.
In Listing 12.21, though, a change to the Chicken
class might require a change to the greet()
function - such as if numberOfEggs
were to be renamed.
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
}
Listing 12.22
-
Two interfaces, split out from the FarmAnimal
interface.
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!")
}
Listing 12.23
-
A class that implements multiple interfaces.
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.NamedCow+ name: String+ speak(): UnitSpeaker+ speak(): UnitSpeakerCow+ name: String+ speak(): UnitNamed+ name: StringSpeakerNamedCow+ name: String+ speak(): Unit"Masks"(Interfaces)Class wearingthe "Named" maskClass 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)
}
Listing 12.24
-
Updating the Farmer
class so that it implements the Named
interface.
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")
)
Listing 12.25
-
Creating a list of Named
objects, including both the farmer and the animals.
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()
}
}
Listing 12.26
-
Error: Unresolved reference: FarmAnimal
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()
}
Listing 12.27
-
Duplicating the Speaker
and Named
interfaces in FarmAnimal
.
… 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!")
}
Listing 12.28
-
Implementing three interfaces in one class.
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(): UnitChicken+ name: String+ numberOfEggs: Int+ speak(): UnitPig+ name: String+ excitementLevel: Int+ speak(): UnitCow+ 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
Listing 12.29
-
Interface extension.
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!")
}
Listing 12.30
-
Declaring that Cow
implements FarmAnimal
. It’s implied that it also implements Named
and Speaker
, because FarmAnimal
extends those two interfaces.
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(): UnitChicken+ name: String+ numberOfEggs: Int+ speak(): UnitPig+ name: String+ excitementLevel: Int+ speak(): UnitCow+ 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) = // ...
Listing 12.31
-
A Cow
object can be used wherever a Cow
, FarmAnimal
, Named
, or Speaker
type is accepted.
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("...")
}
}
Listing 12.32
-
A default implementation in an interface.
Now, we can create a new class that implements the FarmAnimal
interface, but omits the speak()
function!
class Snail(override val name: String) : FarmAnimal
Listing 12.33
-
A class that uses the default implementation for the speak()
function.
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 "..."
Listing 12.34
-
Calling the default implementation of speak()
.
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"
}
Listing 12.35
-
Error: Property initializers are not allowed in interfaces
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"
}
Listing 12.36
-
A property getter.
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()
Listing 12.37
-
A class that implements FarmAnimal
, using the default implementation for both name
and speak()
.
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!
- In cases where the class has no constructor or body, it’d be as easy as writing
class Chicken : FarmAnimal
. [return] - 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]
- 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. [return]
Share this article:
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK