Interfaces and Abstract Classes in Kotlin: Getting Started
source link: https://www.raywenderlich.com/25627013-interfaces-and-abstract-classes-in-kotlin-getting-started
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.
Interfaces and Abstract Classes in Kotlin: Getting Started
Learn how to best use interfaces and abstract classes to create class hierarchies in your Kotlin Android apps.
Version
Before Object-oriented programming (OOP), coding was more complex because languages like C don’t let you abstract concepts at a high level. Abstraction helps you structure a project by grouping common characteristics together to create an object. For example, fur color, tail length and age are characteristics that make a cat.
At its core, abstraction makes coding simpler by providing tools to define real-world objects. It also has other benefits like making code easier to read, understand and maintain. However, it can also complicate your life.
In this tutorial, you’ll explore the differences between Abstract Classes and Interfaces and learn when to use them. You’ll create Zoo Guide, an app which shows information about different animals. Along the way, you’ll learn:
- About Inheritance.
- How to implement abstract classes and interfaces.
- The differences between abstract classes and interfaces.
- Create an application which makes use of abstract classes, interfaces and inheritance.
Getting Started
Download the materials by clicking Download Materials at the top or bottom of this tutorial. Open Android Studio and select Open. Then, select the starter project.
Now, run the project and check its behavior:
Select Lion and you will see:
The app has two screens:
- The main screen shows a list of animals you can click.
- A second screen shows some details about the animal you’ve selected.
This is how the project is structured:
As you can see, the project includes six animals: Bat, elephant, giraffe, lion, parrot and penguin. There are also two main categories: Bird and Mammal. Finally, there’s the Animal
class.
Before you start coding, take a moment to learn about inheritance.
Inheritance
Inheritance defines an is-a relationship between a parent class and its subclasses. Every non-private member of a parent class is visible inside the subclasses if preceded by the keyword open
.
final
by default. So, if you want to extend them you have to add the keyword open
.
Have a look at the Animal, Mammal and Lion classes to see a demonstration of how inheritance works:
// 1 open class Animal( open val name: String, @DrawableRes open val image: Int, open val food: Food, open val habitat: Habitat ) { open fun getHungerAmount() : Int { return 0 } //... } // 2 open class Mammal( override val name: String, @DrawableRes override val image: Int, override val food: Food, override val habitat: Habitat, val furColor: List<Int> ) : Animal(name, image, food, habitat) // 3 class Lion : Mammal( "Lion", R.drawable.animal_elephant_sample, Food.ZEBRAS, Habitat.SAVANNA, listOf(Color.parseColor("#CB9C70")) ) { override fun getHungerAmount() : Int { return (0..100).random() } }
So, what’s going on here?
-
Animal, an open class, defines the animal’s
name
,image
, preferredfood
andhabitat
.getHungerAmount()
defines how hungry the animal is. -
Mammal, another open class, extends from Animal and defines one more property,
furColor
, while implementing all the previous properties fromAnimal
. -
Lion extends Mammal and again, inherits all of Mammal‘s properties and implements
getHungerAmount()
.
This is the principle of Inheritance. It is an OOP concept, and a basic way of establishing relationships between classes. There are three main issues with this approach:
- Since Animal and Mammal are
open
, you can create instances of them, which doesn’t make any sense since they’re only abstract concepts and not concrete, like Lion. This irregularity is extremely dangerous. - You need the subclasses to implement the methods and properties from the parent class. In this case, you can use the constructor to force the subclasses to implement a property from the parent class, but you can’t do the same thing with methods.
Overriding
getHungerAmount()
in Lion was optional; you didn’t have to implement it. The compiler wouldn’t give you any errors if you didn’t. - There are certain characteristics that some mammals have but others don’t. Classes can’t support this type of behavior.
What a mess. But what if abstract classes and interfaces could help you solve all these issues?
Abstract Classes
You can see abstract classes as a mixture of interfaces and regular classes. Abstract classes can have everything that interfaces have as well as properties and constructors. Therefore, you can properly hold state in abstract classes, but you can’t instantiate an abstract class.
You can think of an abstract class as a template. Think about the WordPress templates catalog, where you can choose a specific template with all the functions you need to build a website.
Maybe you want an e-commerce website with different built-in characteristics like a reviews section, cart and list of products. You choose the template that suits you and edit a few things to make it exactly as you want. Done!
Well, that was much simpler than creating the website from scratch.
Abstract classes do the same thing. They provide a common behavior for all subclasses. Use an abstract class to create a group of characteristics that many classes have in common.
Characteristics of Abstract Classes
Abstract classes can’t be final
because the main reason they exist is for other classes to extend them. However, their methods and properties can be final
. In fact, they’re final
by default.
Abstract classes can have both regular and abstract methods or properties. Regular methods and properties can have a body and a value, correspondingly.
In contrast, something defined as abstract doesn’t have a defined value yet. In the case of methods, that means it doesn’t have a body.
Whenever a class extends an abstract class with abstract members, the class must implement all the abstract members. This is essential when a subclass has to implement a method to create a specific behavior.
Take a look at this example:
// 1 abstract class Wrestler { abstract fun themeMusic(): String } // 2 class JohnCena : Wrestler() { override fun themeMusic() = "johncena_theme.mp3" }
In this example, every wrestler has their own themeMusic
that you have to define every time you create a new wrestler. When extending Wrestler
, the compiler will force the JohnCena
class to implement themeMusic()
so you don’t forget to.
Unlike interfaces, abstract classes can have a constructor and thus an init
block. Either of these are essential to initialize something when an object is created.
Now that you’ve seen how abstract classes work, it’s time to update the code above.
Implementing the Animal Abstract Class
Open Animal.kt from the model/animals package. Make the class abstract:
abstract class Animal
Remove the existing getHungerAmount()
method and move the properties from the constructor into the body of the class as abstract properties.
abstract fun getHungerAmount() : Int abstract val name: String @get:DrawableRes abstract val image: Int abstract val food: Food abstract val habitat: Habitat
Do the same for Mammal and this time add furColor
as an immutable abstract property:
abstract class Mammal : Animal() { abstract val furColor: List<Int> }
For Bird add a feathersColor
abstract property instead:
abstract class Bird: Animal() { abstract val feathersColor: List<Int> }
For Bat, this is how the code will look after the update:
class Bat : Mammal() { override val furColor: List<Int> get() = listOf( Color.parseColor("#463D4A") ) override fun getHungerAmount() : Int { return (0..100).random() } override val name: String get() = "Bat" override val image: Int get() = R.drawable.bat override val food: Food get() = Food.INSECTS override val habitat: Habitat get() = Habitat.CAVE }
Follow the same instruction for Elephant, Giraffe, Lion, Mammal and Penguin. Feel free to refer to the final project in the materials you downloaded at the start of the tutorial.
In the updates above, you:
- Set
Animal
andMammal
asabstract
. You can’t create an instance of these classes anymore, so you solved the first problem. - Moved the variables from the constructor into the class’s body. As you can see, the code is clearer now, but there are some other benefits to using this new approach:
- There’s less boilerplate.
Mammal
isn’t implementing all the members thatAnimal
defines because they’re both abstract classes. An abstract class doesn’t need to implement an abstract member. -
Lion
implements and initializes all the members implemented in bothAnimal
andMammal
.
- There’s less boilerplate.
- Defined
getHungerAmount()
asabstract
. Now, the compiler forces you to implement the method insideLion
. Try commenting it out and the compiler will scream at you. For now, you’ve solved the second problem, too.
The third issue is still present, and you need to solve it. Hint: You’ll need to use interfaces to overcome it. :]
Interfaces
Interfaces establish an is, rather than an is-a, relationship between the class and the interface. Unlike abstract classes, interfaces define a specific behavior rather than a template. From now on, I’ll often refer to an interface as a behavior because interfaces can connect objects that otherwise have nothing in common.
Characteristics of Interfaces
Every time your class implements an interface, it’s signing a contract. The contract says that once a class implements an interface, it must also implement all the members defined inside the interface. However, there’s an exception you’ll explore in the next section.
Since Kotlin doesn’t support multiple inheritances, the compiler won’t let a subclass have multiple parent classes. It can only extend one class. But there’s good news: You can implement as many interfaces as you want.
Think about the WordPress template example. You can’t use more than one template for a single site. However, you can add features.
So, if you’re building an e-commerce website and the template doesn’t have a map indicating your store’s position, you can add one with an external plugin. In fact, you can add as many plugins as you want. Kotlin works in the same way with interfaces.
Interfaces can only define open members. So every class that implements an interface can access every non-private member. Remember that abstract methods can’t be private.
Example of Interfaces Application
Imagine you have two classes, Microwave
and WashingMachine
, that only have one thing in common: They both need electricity to work. What would you do to connect them? Take a look at the code below for an example of how this could be done:
// 1 interface Pluggable { val neededWattToWork: Int //Measured in Watt fun electricityConsumed(wattLimit: Int) : Int fun turnOff() fun turnOn() } // 2 class Microwave : Pluggable { override val neededWattToWork = 15 override fun electricityConsumed(wattLimit: Int): Int { return if (neededWattToWork > wattLimit) { turnOff() 0 } else { turnOn() neededWattToWork } } override fun turnOff() { println("Turning off..") } override fun turnOn() { println("Turning on..") } } // 3 class WashingMachine : Pluggable { override val neededWattToWork = 60 override fun electricityConsumed(wattLimit: Int): Int { return if (neededWattToWork > wattLimit) { turnOff() 0 } else { turnOn() neededWattToWork } } override fun turnOff() { println("Turning off..") } override fun turnOn() { println("Turning on..") } }
The best way to solve this problem is to implement an interface that defines a common behavior. That’s exactly what Pluggable
does. Now every class that extends this interface needs to implement all the members. In doing so, they can connect to the power.
In Java, you can only define constant properties inside interfaces. So, every time you try to add a property in Kotlin like this:
String password = "Password1234";
The compiler adds public static final
under the hood. In Java, you have to initialize properties so you can’t do something like this:
String password; //This code won't run
Kotlin, on the other hand, lets you define non-static properties inside interfaces that can only be val
. This becomes useful when you handle only one value, and a method would be too much, as in the neededWattToWork
property above. It would be inconvenient to define a method only to return a value, even though, as the decompiled Java code shows, under the hood, the compiler creates a method for the property anyway.
public interface Pluggable { int getNeededWattToWork(); int electricityConsumed(int var1); void turnOff(); void turnOn(); }
However, Kotlin doesn’t let you create var
properties because a var
property includes three components:
- A
private
field to store its value. - A
getter
to take its value. - A
setter
to set its value.
The problem here is that an interface can’t store a value because it can’t have a property.
Default Methods of an Interface
Default methods are a great way to fight the boilerplate. With Kotlin, a method can have a body, and a property can hold a value inside it. Does it make sense to have methods with a body inside an interface?
The answer is yes. As you know, when a class implements an interface, it also has to implement all its members. Sometimes, however, this isn’t useful because the logic is independent of the classes that implement the interface. So instead of having each class provide the same implementation, it’s better to have the definition within the interface.
Considering the home appliances example above, it’d be cool to know the voltage each needs to work. To do that, you can add a new method, voltage()
, to Pluggable
like this:
fun voltage() : Int { return 230 }
Then override it in Microwave:
// There is no need to implement this method override fun voltage(): Int { return super.voltage() }
And also in WashingMachine:
override fun voltage(): Int { return 300 }
Since 99% of the home appliances use a classic voltage, implementing the method and returning the value every time wouldn’t make sense. By implementing a method with a body and default value, you don’t have to implement this method and return a value. The interface already does that for you.
You can implement the method and change the voltage whenever you need to redefine the voltage, as you did inside WashingMachine.
Now, it’s time to implement interfaces in Zoo Guide.
Implementing Interfaces in the Project
Animals have different abilities. For example, kangaroos can jump very high, cheetahs can run extremely fast and some fish can survive in the ocean’s greatest depths. However, animals often have similar abilities.
In this project, you’ll implement three main abilities animals share: Walking, flying and swimming. These abilities are represented by the following interfaces: Walkable
, Flyable
and Swimmable
.
While it may seem straightforward, animal categories are complicated. For example, not every mammal can walk and not every bird can fly. So Mammal
can’t define member runningSpeed()
if not all mammals can walk or run. You must implement these specific behaviors separately.
At the root of the project, create a package and name it interfaces. In this package, create the following interfaces in their respective files:
Walkable.kt
interface Walkable { val runningSpeed: Int }
Walkable.kt
interface Flyable { val flyingSpeed: Int }
Walkable.kt
interface Swimmable { val swimmingSpeed: Int }
Every interface implements a speed member representing how fast the animal can go. In this example, the code shows a single property for every interface, but feel free to play around with the code and add some abstract
methods and methods with bodies.
Finally, every animal has to implement the right interface. Let Bat
and Parrot
implement Flyable
. Then set their flyingSpeed
s to 95 and 30 respectively. For instance, update Bat
with:
class Bat : Mammal(), Flyable
override val flyingSpeed: Int get() = 95
Let Elephant
, Giraffe
and Lion
implement Walkable
. Then set their runningSpeed
s to 40, 60 and 80 respectively. For instance, update Elephant
with:
class Elephant : Mammal(), Walkable
override val runningSpeed: Int get() = 40
Lastly, let Penguin
implement both Walkable
and Swimmable
. Then set the runningSpeed
and swimmingSpeed
to 7 and 40 respectively. Here are the required changes:
class Penguin : Bird(), Walkable, Swimmable
override val runningSpeed: Int get() = 7 override val swimmingSpeed: Int get() = 40
Here’s a code breakdown:
-
Bat
,Parrot
,Elephant
,Giraffe
andLion
implement a single interface and redefine a single method. -
Penguin
has two skills. It can swim and run, even though he’s pretty slow. That’s why it’s crucial to have the ability to implement multiple interfaces.
The Practical Side of Interfaces
While you now understand why your future projects should implement interfaces, it’s often hard to grasp this argument in a practical way. In this section, you’ll explore the practical side of interfaces.
As you may have noticed, Zoo Guide is still missing an important feature, the filter.
The Spinner lets you choose between the three different behaviors defined above, Walkable, Flyable and Swimmable, and an All option that includes all the animals in the list. When you select one of these options, the list updates.
Navigate to ui/fragments package. Then open ListFragment.kt. Inside onItemSelected()
, replace the TODO
with:
val updatedList: List<Animal> = when(position) { 0 -> AnimalStore.getAnimalList() 1 -> AnimalStore.getAnimalList().filter { it is Flyable } 2 -> AnimalStore.getAnimalList().filter { it is Walkable } 3 -> AnimalStore.getAnimalList().filter { it is Swimmable } else -> throw Exception("Unknown animal") } viewModel.updateItem(updatedList)
Here’s a code breakdown:
- Anytime a user clicks an item inside the Spinner,
onItemSelected()
is called. - The
when
statement checks the position and filters the list based on the item the user selects. -
ListViewModel
definesupdateItem()
. You need to call it every time you update the list to notify the adapter that something has changed.
filter
will check every Animal
object in the list and choose only the one that respects the behavior the user selected.
Now you can see how an interface isn’t only a contract to implement methods and properties but also creates a sub-category of objects. You can instantiate a group of objects, such as arrays or lists, that share the same behavior. They may be completely different objects, but they share the same behavior.
Holding State Inside an Interface
If you’ve searched for interfaces on the internet, you’ve likely encountered some blogs that say interfaces can’t hold states. That was true until Java 8 introduced methods with a body inside interfaces. But, what’s the state?
The state is what an object has. For example, the name
property inside Bird defines a state of a Bird
object. If an object doesn’t have any instance properties or has some properties with unknown values that don’t change, it’s stateless.
Where can you store the state? You store it in a companion object property.
Consider the code below:
// 1 interface Animal { var name: String get() = names[this]?.toUpperCase() ?: "Not in the list" set(value) { names[this] = value } var speed: Int get() = speeds[this] ?: -1 set(value) { speeds[this] = value } val details: String get() = "The $name runs at $speed km/h" companion object { private val names = mutableMapOf<Animal, String>() private val speeds = mutableMapOf<Animal, Int>() } } // 2 class Lion: Animal // 3 class Penguin: Animal // 4 fun main() { val lion = Lion() lion.name = "Lion" lion.speed = 80 val penguin = Penguin() penguin.name = "Penguin" penguin.speed = 7 println(lion.details) // The LION runs at 80 km/h println(penguin.details) // The PENGUIN runs at 7 km/h }
Similar to Zoo Guide, this code will save a list of animals.
Here’s what the code does:
- Every animal has three properties: a
name
,speed
anddetails
. The first two are mutable while the last one is immutable. - Both
name
andspeed
can get and store a value using aMutableMap
. Note that thekey
of theMap
is a reference to the Animal interface itself that changes every time a class implements it. - The Animal interface has two private static mutableMaps that hold the state.
- Both
Lion
andPenguin
implementAnimal
. They don’t have to define any members ofAnimal
since all of them have been initialized.
Change the names
‘s visibility from private
to public
and iterate the list:
// 1 interface Animal { companion object { val names = mutableMapOf<Animal, String>() } } // 2 Animal.names.forEach { name -> println(name.value) } //Output: //Lion //Penguin
As you can see, Animal.names
holds all the values.
The scope of this example was only to show you that it’s possible to store the state inside an interface. However, you shouldn’t do that in real-world projects because it’s poorly managed and not cleaned properly by the garbage collector.
Abstract Classes vs Interfaces
What’s the difference between abstract classes and interfaces? This is one of the most asked questions during coding interviews. You’ve already learned many things about interfaces and abstract classes, but here are some more thoughts you can use in your future interviews.
Both abstract classes and interfaces:
- Can’t be instantiated.
- Define both abstract and regular members.
- Define
static
members. - Force subclasses to implement a behavior.
However, interfaces don’t have a constructor, and you can implement them multiple times.
From a features and characteristics point of view, these two tools are very similar. The thing that makes them distinct is the conceptual difference.
When you need to define a too-generic abstraction, it’s time to use an abstract class. You can see them as templates that subclasses can extend.
For example, in the Animal
class, food
is too general to be defined directly in the parent class. But any animal that extends Animal
must implement it and return the food the animal eats.
An interface abstracts a specific behavior that multiple classes can implement. For example, Walkable
implements a specific behavior for animals that can walk or run.
Where to Go From Here?
Download the completed project files by clicking Download Materials at the top or bottom of the tutorial.
Great job on making it all the way through! You’ve learned the difference between an abstract class and an interface and when to use them. However, there’s another approach to handling this kind of relationship between objects called composition.
Interfaces are the base for composition, and this article hopefully helped you understand them. If you want to find out more about composition, check out this Using Composition in Kotlin tutorial.
If you have any questions or comments, please join the forum discussion below!
raywenderlich.com Weekly
The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.
Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK