Instruction

Interfaces

Inheritance is a powerful tool for code reuse, but it focuses on the “is-a” relationship between classes. A subclass is a specialized version of its superclass. Interfaces, on the other hand, introduce a different concept: the “is” relationship.

Interfaces define a set of behaviors, or simply methods, that a class must implement. In Kotlin, interfaces also can have a default implementation, which makes them similar to abstract classes.

The abstract class is a special class that only partially defines the behavior and requires all subclasses to implement certain details. You’ll learn about the abstract class in future modules. For now, you’ll learn more about the interfaces. They focus purely on functionality and ensure consistency and clarity in your codebase.

But why use interfaces? Here are a few reasons:

  • Enforcing consistency: Interfaces define a contract of methods that classes must implement. This ensures that all classes adhering to the interface provide the same set of functionalities.
  • Promoting code reuse: By defining common behaviors in interfaces, you can reuse them across different classes. This helps you avoid duplicating code and ensures that the same functionality is implemented consistently.
  • Decoupling classes: Interfaces allow you to separate the definition of behavior from the implementation details. This promotes loose coupling between classes, making your code more modular and easier to maintain.
  • Supporting Multiple Inheritance: Unlike classes, a class can implement multiple interfaces. This allows a class to inherit behaviors from multiple sources, which is not possible with single inheritance.
  • Openness for collaboration: Interfaces can only define open members, which are accessible by any class that implements the interface. This promotes collaboration between classes because they can all interact with the shared methods defined in the interface. Private members, on the other hand, remain hidden within the interface itself.

Note: It’s important to remember that abstract methods, which are methods without a body, can’t be private within an interface. Since subclasses must implement these methods, they need to be publicly accessible.

Imagine an interface named Edible. This interface might define a single method called taste(). Any class that implements Edible, like a Vegetable class or a Fruit class, must provide its own implementation of the taste() method. This way, even though vegetables and fruits are completely different types of objects, they can both have “taste” because they adhere to the common behavior defined by the Edible interface.

Define an interface in Kotlin Playground:

interface Edible {
  val isItReallyEdible: Boolean
  
  fun taste(): String
}

Here, you defined:

  • The isItReallyEdible interface property which must be defined with val and can be overridden in the subclasses
  • The taste() method, which is declaration only.

Default Implementation

Remember how interfaces are all about defining behavior contracts? While interfaces primarily focus on what a class should do, they can sometimes help with how to do it. This is where default methods come in.

In Kotlin, interfaces can have methods with an actual body of code, seemingly contradicting the concept of an interface focusing purely on behavior. But there’s a catch: these methods are optional! They provide a default implementation that classes implementing the interface can inherit.

Why are default methods useful? You have an interface named Edible that defines a method, consumer(). This method essentially says, “Anything implementing Edible should be able to name the consumer of edible stuff.” However, some foods are edible for some consumers and not for others. For example, a Chocolate class might not be Edible for animals.

Add this method to the Edible interface:

  fun consumer(): String {
    return "Human"
  }

Here’s where default methods shine. You can define a default implementation for consumer() within the Edible interface. This default implementation might provide basic assumptions like that your app is for a restaurant where only Humans will eat. Classes implementing Edible can use this default behavior or override it with their own custom logic if needed.

Implementation

Now you’ll create two classes implementing the Edible interface:

class Fruit : Edible {
  override val isItReallyEdible = true

  override fun taste(): String {
    return "Sweet"
  }

}

class Mushroom : Edible {
  override val isItReallyEdible = false
  
  override fun taste(): String {
    return "Delicious"
  }

  override fun consumer(): String {
    return "Pig"
  }
}

As you can see, these two classes are not alike, but they still implement the same contract, which means the app can treat them the same way. For example, you can create a list of such things and print details of each instance in the loop. Add this code and run:

fun main() {
  val items = listOf<Edible>(Fruit(), Mushroom())
  items.forEach { item ->
    println("${item.javaClass.simpleName} really edible: ${item.isItReallyEdible}, taste: ${item.taste()}, consumed by ${item.consumer()}")
  }
}
// Fruit really edible: true, taste: Sweet, consumed by Human
// Mushroom really edible: false, taste: Delicious, consumed by Pig

As you can see, you must explicitly mention the list type, <Edible>, to let Kotlin know that you need this is how you’re going to treat items in the list.

The javaClass property in Kotlin is used to get the runtime reference to the current object’s class. It’s equivalent to the .getClass() method in Java. This property is useful when you need to access class-level information such as the class name, its superclass, the interfaces it implements, and its annotations.

In this example, the javaClass.simpleName expression returns the actual name of this class.

Note: When you declare a variable, item in the example above, it assumes the type of Edible, which means that you can only access properties and methods of the interface, not of a concrete instance. Each individual class can have as many methods as needed, but to access them, you would need the technique from the previous lesson, where you did the runtime checks and assumed a new type using the as operator.

Multiple Interfaces

Each class can implement multiple interfaces. It’s a valuable tool for building flexible, reusable, and well-designed code in Kotlin. It promotes loose coupling and clear separation of concerns, making your code easier to understand, maintain, and evolve. Here are just a few benefits:

  • Increased Flexibility and Code Reusability: A class can implement functionalities from various interfaces without being restricted to a single inheritance hierarchy. This helps you create classes that you can use in different contexts and promotes code reuse across different parts of your app.
  • Improved Design with Clearer Contracts: A class explicitly declares the behaviors it supports by implementing multiple interfaces. This enhances code readability and maintainability because developers can easily understand a class’s capabilities by looking at the interfaces it implements.
  • Achieving Loose Coupling: Interfaces define behavior without dictating implementation details. When a class implements multiple interfaces, it’s not tightly coupled to any specific implementation. This allows for easier testing, maintenance, and potential changes in how the functionalities are provided.
  • Modeling Real-World Scenarios: Many objects in the real world have multiple functionalities. Implementing multiple interfaces helps you accurately represent these objects in your code. For example, a Car class could implement interfaces like Drivable, Maintainable, and Drawable, reflecting its ability to be driven, maintained, and visually represented on the screen.

Here’s another example with more interfaces:


interface Edible {
  fun taste(): String
}

interface Sweet {
  fun andSour(): String
}

class Fruit(val name: String, val sour: Boolean = false) : Edible, Sweet {
  override fun taste(): String {
    return "$name is very good"
  }

  override fun andSour(): String {
    return if(sour)"Maybe" else "Definitely not!"
  }
}

class Garlic : Edible {
  override fun taste(): String {
    return "delicious"
  }
}

Here, you get two interfaces, Edible and Sweet. The Fruit class implements both, but Garlic implements only Edible. This way, you get flexibility without compromising the whole structure. If garlic isn’t Sweet, there’s no point in implementing interface methods related to sweetness.

Now, use the example above and try to print some properties:

fun main() {
  // Error: Type mismatch: inferred type is Garlic but Sweet was expected
  val sweetItems = listOf<Sweet>(Fruit("Peach"), Garlic())
  sweetItems.forEach { item ->
    println("${item.javaClass.simpleName} sour: ${item.andSour()}")
  }
}

Here, you get a pretty self-descriptive error because Garlic isn’t implementing the Sweet interface, and when you declare the type of the list as Sweet, it doesn’t work.

Try again, but now with the Edible interface:

fun main() {
  val items = listOf<Edible>(Fruit("Peach"), Garlic())
  items.forEach { item ->
    println("${item.javaClass.simpleName} taste: ${item.taste()}")
  }
}
//Fruit taste: Peach is very good
//Garlic taste: delicious

It worked! Now you know that Garlic is delicious. So, how will you redo the example for the Sweet interface?

Try again:

fun main() {
  //   Same, but for Sweet
  val sweetItems = listOf<Sweet>(Fruit("Peach"), Fruit("Tomato", false))
  sweetItems.forEach { item ->
    // Error:
    // Unresolved reference: name
    // Unresolved reference: taste
    println("${item.name} taste: ${item.taste()}. Sour? ${item.andSour()}")
  }
}

What’s going on? It should have worked! Remember, when you interact with a specific interface, you can’t access the name from the Fruit class or the taste() method from the Edible interface. While this might not seem convenient, it encourages careful coding practices and results in more maintainable code when you’re developing your own app.

Note: The examples you’re using here have instances created on the spot, but in most apps, you can’t do that or verify where it’s coming from. Most of the time, it would be a method parameter of an interface type, so the consumer of that method would provide instances, and Kotlin will ensure that instances are of the right type. For example:

fun printList(val sweets: List<Sweet>)

Try again, but this time, let’s make it right!

fun main() {
  // Same, but name is not available
  val sweetItems = listOf<Sweet>(Fruit("Peach"), Fruit("Tomato", true))
  sweetItems.forEach { item ->
    println("${item.javaClass.simpleName} is Sweet. Sour? ${item.andSour()}")
  }
}
//Fruit is Sweet. Sour? Definitely not!
//Fruit is Sweet. Sour? Maybe

It worked! Nice one. You started with the assumption that all the objects you have are Sweet and used only methods and properties of that interface.

Note: You might have observed that I referred to properties of the Sweet interface, even though it doesn’t have any. To clarify, the Sweet interface doesn’t have any properties, which is different from having ‘none’. In other words, the count of properties in the Sweet interface is zero. This fact enables us to make statements like:

“All properties are accounted for”, or “Every property in this class has been defined”.

Okay, enough jokes. Back to business…

What if you want to work on the Fruit class properties rather than the Sweet interface? If you follow the same approach, you’ll get this result:

fun main() {
  // Same, but for Fruit
  val fruitItems = listOf<Fruit>(Fruit("Peach"), Fruit("Tomato", true))
  fruitItems.forEach { item ->
    println( "${item.name} taste: ${item.taste()}. Sour? ${item.andSour()}")
  }
}
//Peach taste: Peach is very good. Sour? Definitely not!
//Tomato taste: Tomato is very good. Sour? Maybe

Make sense? You declared the list is of type Fruit and it was treated as such. It feels like magic, but it isn’t.

In essence, interfaces provide a blueprint for behavior. By composing classes based on interfaces, you build apps using well-defined functionalities that can be mixed and matched to achieve desired outcomes. This approach leads to cleaner, more maintainable, and more scalable code.

Here’s an analogy: Imagine building with Legos. Each Lego brick represents a functionality defined in an interface. You can combine these bricks, or classes implementing interfaces, in various ways to create different structures, or complex functionalities. This modularity and reusability are what makes Legos so versatile, and the same principles apply to using interfaces for composition in Kotlin.

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo