0

Interfaces and Abstract Classes in Kotlin: Getting Started

 2 years ago
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.
Home Android & Kotlin Tutorials

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.

By Mattia Ferigutti Aug 23 2021 · Article (25 mins) · Intermediate

5/5 2 Ratings

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.
Note: This article assumes you’re familiar with the basics of Kotlin. If you’re new to Kotlin, look at Programming in Kotlin before you start.

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:

Animal list screen

Select Lion and you will see:

Animal details screen

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:

Structure of the project

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.

Note: In Kotlin, a class and its members are 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?

  1. Animal, an open class, defines the animal’s name, image, preferred food and habitat. getHungerAmount() defines how hungry the animal is.
  2. Mammal, another open class, extends from Animal and defines one more property, furColor, while implementing all the previous properties from Animal.
  3. 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:

  1. 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.
  2. 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.

  3. 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.

Note: Keep in mind, if a parent class hasn’t initialized a member, you’ll have to initialize it in the subclass.

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:

  1. Set Animal and Mammal as abstract. You can’t create an instance of these classes anymore, so you solved the first problem.
  2. 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 that Animal 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 both Animal and Mammal.
  3. Defined getHungerAmount() as abstract. Now, the compiler forces you to implement the method inside Lion. 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 flyingSpeeds 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 runningSpeeds 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:

  1. Bat, Parrot, Elephant, Giraffe and Lion implement a single interface and redefine a single method.
  2. 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.

Spinner in the main screen

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:

  1. Anytime a user clicks an item inside the Spinner, onItemSelected() is called.
  2. The when statement checks the position and filters the list based on the item the user selects.
  3. ListViewModel defines updateItem(). 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:

  1. Every animal has three properties: a name, speed and details. The first two are mutable while the last one is immutable.
  2. Both name and speed can get and store a value using a MutableMap. Note that the key of the Map is a reference to the Animal interface itself that changes every time a class implements it.
  3. The Animal interface has two private static mutableMaps that hold the state.
  4. Both Lion and Penguin implement Animal. They don’t have to define any members of Animal 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!

Average Rating

5/5

Add a rating for this content

Sign in to add a rating
2 ratings

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK