Kotlin: Enums, Interfaces and Generics

By Sruthy

By Sruthy

Sruthy, with her 10+ years of experience, is a dynamic professional who seamlessly blends her creative soul with technical prowess. With a Technical Degree in Graphics Design and Communications and a Bachelor’s Degree in Electronics and Communication, she brings a unique combination of artistic flair…

Learn about our editorial policies.
Updated March 7, 2024

In this tutorial, we will learn in-depth about Kotlin enums, Kotlin generics, and Kotlin interfaces with code examples:

All these concepts exist for almost all Object-oriented programming languages. An interface is nothing but a way to declare/define an abstract view of some functionality.

For example, an account abstract should have ways for a user to checkBalance, withdrawAmount, and depositAmount. All these are abstract behavior and different types of Accounts like – SavingsAccount, CheckingAccount, CurrentAccount, WalletAccount can provide their own implementations for this contract.

Kotlin supports defining interfaces with the help of the “interface” keyword. Enum (or enumeration) is a way to define pre-defined constants in an application. Kotlin uses the keyword “enum” to create/define an enum.

=> Read ALL Our Kotlin Tutorials Here

Understanding Kotlin: Enums, Interfaces, And Generics

Kotlin Enums, Interfaces and Generics

Generics are used to define Type Agnostic parameterized methods, classes, which would apply to parameters of the defined data types. For example, we can use generics to create a method that would add 2 numbers of different data types – Integers, Float, Double etc, without defining a specific data type.

We will look into all these concepts in the upcoming sections of the tutorial.

Kotlin Interface

As we know, interfaces are used to define the abstract behavior of an entity which can be concretely defined by different classes/enums that are implementing the interface.

Syntax:

interface {interfaceName} {
	// interface body
	// 1 or more abstract methods and properties
}

Let’s see an example: Suppose we want to define an interface named Account, with abstract methods for the deposit(), withdraw() and checkBalance()

interface IAccount {
    // method to get account Balance for a given account number
    fun checkBalance(accNo: String) : Int

    // function to deposit an amount in a given account
    fun deposit(accNo: String, amt: Int) : Boolean

    // function to withdraw amount from a given account
    fun withdraw(accNo: String, amt: Int) : Boolean
}

Interface body can have 1 or more abstract methods as well as properties. It’s also important to note that the Interface body can not contain any state.

For example:

interface ISomeInterface {
	// this is illegal and would throw compile error
	val name: String = "Hello World!"
}

Note: It’s generally recommended to name interfaces with an “I” appended as the prefix to their name to separate them out with the concrete classes implementing these.

For example, in the above example, we have used IAccounts as interface name for Account operations.

Implementing Kotlin Interface

A class in Kotlin can implement 1 (or more) interface(s) and all abstract methods are required to be implemented in the class.

Let’s see an example: We will write an implementation for the IAccount interface declared in the last section.

interface IAccount {
    // method to get account Balance for a given account number
    fun checkBalance(accNo: String) : Int

    // function to deposit an amount in a given account
    fun deposit(accNo: String, amt: Int) : Boolean

    // function to withdraw amount from a given account
    fun withdraw(accNo: String, amt: Int) : Boolean
}

class SavingsAccount : IAccount {
    var accounts = HashMap<String, Int>()

    override fun checkBalance(accNo: String): Int {
        return (accounts?.get(accNo)!!)
    }

    override fun deposit(accNo: String, amt: Int): Boolean {
        return if(amt > 0) {
            accounts[accNo] = accounts[accNo]!! + amt
            true
        } else {
            false
        }
    }

    override fun withdraw(accNo: String, amt: Int): Boolean {
        return if(accounts[accNo]!! >= amt) {
            accounts[accNo] = accounts[accNo]!! - amt
            true

        } else {
            false
        }
    }
}

fun main() {
    val savingsAccount = SavingsAccount()

    val accountNo = "ACC-123"
    savingsAccount.accounts[accountNo] = 200

    // calling checkBalance method
    println("--Initial acc balance--")
    println(savingsAccount.checkBalance(accountNo))

    // depositing money in account & checking balance
    savingsAccount.deposit(accountNo, 100)
    println("--Acc balance after depositing 100--")
    println(savingsAccount.checkBalance(accountNo))

    // withdrawing money from account & checking balance
    savingsAccount.withdraw(accountNo, 200)
    println("--Acc balance after withdrawing 200--")
    println(savingsAccount.checkBalance(accountNo))
}

//Output
--Initial acc balance--
200
--Acc balance after depositing 100--
300
--Acc balance after withdrawing 200--
100

You can see a complete working example above. Let’s understand the implementation:

  1. We’ve created a class named SavingsAccount – which is implementing the IAccount Interface.
  2. We’ve added implementations for all 3 methods
    • checkBalance will look into the account Map and return the balance.
    • deposit will increase the account balance for a given account number with the deposited amount.
    • withdraw the implementation first checks if the available balance is account is > then the amount being withdrawn. If that’s true, it debits the account with the amount that’s being withdrawn.
  3. Finally, we’ve created the main function to call the interface methods and created a dummyAccount named “ACC-123” and added an initial balance of $200.
    We have then tried to credit the account with $100 and finally tried to debit the account by $200.
  4. In the output section, you can see we’ve performed deposit and withdrawal actions and called checkBalance after each operation.

It’s also possible to implement multiple interfaces in a class. In such a scenario, Interface names can be comma separated – and all abstract members of both the interfaces would need to be implemented.

Note: We won’t go into internal details of how these individual methods are implemented. We’ve used concepts like if-else conditions and Maps (which would be covered in depth in other tutorials)

Interfaces With Methods Having Default Implementation

In Kotlin, we can also define interface methods by the default implementation. This is a useful feature as it allows interfaces to be extended easily with default definitions and the implementing classes can decide to implement or to not implement the methods having default implementation in the interface.

interface IDefaultImp {
    // abstract method
    fun absMethod() : String

    // default implementation
    fun defaultMethod() : String =  "default Implementation"
}


class ImplementInterface : IDefaultImp {
    override fun absMethod(): String {
        return "abstract method being called!!"
    }

    //overriding method with default implementation - and also calling the default implementation
    override fun defaultMethod(): String {
        println("Custom implementation being called")
        return super.defaultMethod()
    }
}

fun main() {
    val implementInterface = ImplementInterface()

    println("--calling abstract method--")
    println(implementInterface.absMethod())

    println("--calling overridden method with default implementation--")
    println(implementInterface.defaultMethod())
}

//Output
--calling abstract method--
abstract method being called!!
--calling overridden method with default implementation--
Custom implementation being called
default Implementation

Above, we have a method named defaultMethod in Interface IDefaultImp

For the implementing class, if we want to override, we can and call the default implementation using the “super” keyword – ex. super.defaultMethod()

Interfaces Extending Another Interface

It’s also possible to have an interface extend to another interface. While these are rarely used, Kotlin provides this support.

Let’s see an example:

interface INumbers {
    // abstract property
    val firstNum : Int

    // abstract property
    val secondNum : Int
}

interface IAddition : INumbers {
    fun add() : Int = firstNum + secondNum
}

class InterfaceInheritance : IAddition {
    override val firstNum: Int
        get() = 10
    override val secondNum: Int
        get() = 20
}

fun main() {
    val interfaceInheritance = InterfaceInheritance()

    // calling method with default implementation
    println("Sum of ${interfaceInheritance.firstNum} and ${interfaceInheritance.secondNum} is :")
    println(interfaceInheritance.add())
}

//Output
Sum of 10 and 20 is :
30

Kotlin Enums

Enums are more expressive ways to define constants in an application and make the core more readable and maintainable. Think of defining an enum (which is a short form for enumeration) for anything that can be tied to a particular state.

For example, customer loan applications in a bank can move through different states – APPLICATION, DOCUMENT VERIFICATION, DISBURSEMENT, REPAYMENT, REPAID, etc. All these different loan states can be defined using a LoanState enum.

Syntax:

enum class {enumName} {
    //1 or more enum values
}

Let’s see an example enum with Name AccountTypes and values – SAVINGS_ACCOUNT, CHECKING_ACCOUNT, and WALLET.

enum class AccountType {
    SAVINGS_ACCOUNT,
    CHECKING_ACCOUNT,
    WALLET
}

Enums With Properties

We can add 1 or more associated properties with enums as well as part of the constructor.

Let’s see with the help of an example:
Suppose in the AccountsType enum, we want to add a minBalance property to all the account types. Here is how we can add a property and define values for all the enum data values.

enum class AccountType(val minBalance: Int) {
    SAVINGS_ACCOUNT(0),
    CHECKING_ACCOUNT(100),
    WALLET(100)
}

Adding Static Methods In An Enum

We can also add static methods in enum by defining a companion object (//Add link to companion object Article here) inside enum.

Let’s try to understand with the help of an example:

Extending the same AccountType enum, suppose we want to create a static method, which would return a comma-separated String of all enum values.

enum class AccountType(val minBalance: Int) {
    SAVINGS_ACCOUNT(0),
    CHECKING_ACCOUNT(100),
    WALLET(100);

    companion object {
        fun getAccTypes() : String {
            var accTypes = ""
            for(accType in AccountType.values()) {
                accTypes = accTypes + accType.name + "\n"
            }
            return accTypes
        }
    }
}

fun main() {
    // calling static method in Enum
    println(AccountType.getAccTypes())
}

//Output
SAVINGS_ACCOUNT
CHECKING_ACCOUNT
WALLET

Implementing Interfaces In enums

In Kotlin, you can also have enums implement interfaces. In such cases, each data value of enum would need to provide an implementation of all the abstract members of the interface.

interface IAccountInterestRates {
    fun getInterestRate() : Double
}

enum class AccountTypes(val minBalance: Int) : IAccountInterestRates {
    SAVINGS_ACCOUNT(0) {
        override fun getInterestRate(): Double {
            return(1.2)
        }
    },
    CHECKING_ACCOUNT(100) {
        override fun getInterestRate(): Double {
            return(0.5)
        }
    },
    WALLET(100) {
        override fun getInterestRate(): Double {
            return(0.0)
        }
    }
}

fun main() {
    // calling static method in Enum
    println("Savings bank interest rate is : ${AccountTypes.SAVINGS_ACCOUNT.getInterestRate()}")
}

//Output
Savings bank interest rate is : 1.2

Above, you can see, we have implemented interface – IAccountInterestRates in the enum AccountTypes

We would need to implement the abstract method for each of the enum values. Here for each account type, we have implemented the abstract method – getInterestRate()

While calling these interface methods, we can directly use the enum value and call the method.

Example:

AccountTypes.SAVINGS_ACCOUNT.getInterestRate()

Kotlin Generics

Generics in simple words are parameterized classes. This functionality is also available in a lot of other Object Oriented programming languages like Java, C# etc.

Generics largely help to achieve code reuse and flexibility.

It’s important to note about Generics, that these are type-safe and are checked during compile time to avoid any conflicts at run time.

Generic With No Type Restriction

Generics can be defined with type parameters, without any restriction on Type. In such a case, any type can be passed when defining an object of a Generic class.

generics

Let’s see how we can define and use Generics in Kotlin.

Generic can be applied at various levels like classes, methods, interfaces, etc.

Let’s try understanding the concept with the help of examples:

// Generic class with a typed parameter T
class GenericClass<T>(val data: T) {

    //Generic function returning object of type T
    fun getDataType(): T {
        return data
    }
}

data class SomeObject(
    val name: String = "abc",
    val age : Int = 10
)

fun main() {

    println("--Object of type String--")
    val a = GenericClass("Hello World!!")
    println(a.getDataType())
    println(a.getDataType() is String)

    println("--Object of type Long--")
    val b = GenericClass(123L)
    println(b.getDataType())
    println(b.getDataType() is Long)


    println("--Object of type SomeObject--")
    val c = GenericClass(SomeObject())
    println(c.getDataType())
    println(c.getDataType() is SomeObject)
    
}

//Output
--Object of type String--
Hello World!!
true
--Object of type Long--
123
true
--Object of type SomeObject--
SomeObject(name=abc, age=10)
true

Few notes about the above code

  • We have declared a class named GenericClass that accepts a parameter of type ‘T’ – which means it’s a generic parameter and while declaring objects we can define objects of any type.
  • The constructor expects an object of type T.
  • Also, note here the letter T is just symbolic of Type. You could use any other letters like – A, B, C. It would work the same way.
  • The class has a method getData() which returns an object of the type T which was defined in the class constructor.
  • In the main method, we can declare objects with explicit type or omit type (as Kotlin compiler is smart to judge the type of object.

For example: Here both these declarations are valid:

val a = GenericClass("Hello World!!")
val a = GenericClass<String>("Hello World!!")

Bounded Generics

The Type parameter in Generics can also be defined to be bounded. What this means is you can restrict the type values that can go in. For example – if you would just want your Generic classes to support classes extending a given class, you can specify that during the type declaration.

Let’s see an example:

abstract class Shape {
    abstract fun area() : Int
}


class Rectangle(var length: Int, val breadth: Int) : Shape() {

    override fun area() : Int {
        return length * breadth
    }

}

class Square(var length: Int) : Shape() {

    override fun area(): Int {
        return length * length
    }
}

class AreaString<T : Shape>(val shape : T) {

    fun getAreaString() : String {
        return("Area is: ${shape.area()}")
    }
}

data class InvalidObject(val name: String)

fun main() {

    // declare object of type Rectangle
    val rectangle = Rectangle(10,20)

    // declare object of type Square
    val square = Square(10)

    // get area string thru generic class
    val rectangleAreaString = AreaString(rectangle)
    println(rectangleAreaString.getAreaString())

    // get area string thru generic class
    val squareAreaString = AreaString(square)
    println(squareAreaString.getAreaString())

    // try to assign some other object
    // this will not compile - because the passed object does not implement the class Shape
    //val areaOfOInvalidObject = AreaString(InvalidObject("Hello"))
}

// Output:
Area is: 200
Area is: 100

Bounded Generics

In the above code,

  • We’ve declared an abstract class Shape, which is implemented by – Classes Rectangle and Square, and a concrete implementation for function area is also provided
  • We’ve declared a Generic class named AreaString which expects a parameter of Type T inherited from class Shape – the declaration is as class AreaString<T : Shape>(val shape : T). What this simply means is – AreaString would accept only those objects, which inherit from the Shape class.
  • In the main function, we have called the AreaString class.
    • With Objects of type – Rectangle and Square and called the getAreaString() method in the generic class which would print the area of the given shape.
    • When we try to declare an object of AreaString with some other object that does not implement the Shape class, we will get a compiler error, as that assignment is not allowed.

kotlin_generics_1

Generics Used From Kotlin Language

Kotlin Collections heavily use Generics for their implementation.

For example, consider using an array list in Kotlin.

ArrayList<E>

Here ‘E’ is a generic type and should be defined when an object of ArrayList class is declared.

Most of the Collection classes use Generics, which allow the user to create Collection classes with different types of objects and use the commonly implemented functions.

Frequently Asked Questions

Q #1) How do you call an interface in Kotlin?

Answer: An interface can not be called directly. In order to call, you would need to instantiate it with the object of a concrete class that’s implementing the interface.

Q #2) Can Java implement a Kotlin interface?

Answer: Yes, as we know Java and Kotlin code are completely interoperable, it’s perfectly valid for Java to implement an interface written in Kotlin.

Q #3) How do I get the enum value in Kotlin?

Answer: In Kotlin, you can get enum value or name using the .name attribute. It would return the actual value of the enum.

For example:

enum class CustomerLoyaltyTier{
    SILVER,
    GOLD,
    PLATINUM
}

fun main() {

    println(CustomerLoyaltyTier.SILVER.name)
}
// Output
SILVER

Q #4) Can enum classes have Kotlin methods?

Answer: Just like any other class, Enum can have methods in 2 different ways:

  • Declaring a companion object inside enum, which could have static methods (and would apply to all enum values)
  • Enum Implementing an interface – where every enum value would need to implement that method and can be called through enum value.

Q #5) How do you use the generic class in Kotlin?

Answer: Generic classes allow the use of parametrized classes. You can declare a class with type parameter ex – T and define functions on it. While declaring, you can specify the type for which you are declaring the Kotlin class.

Refer to this example for declaration of Generic class:

class GenericClass<T>(val data: T) {

    fun getDataType(): T {
        return data
    }
}

Q #6) How do I inherit an interface in Kotlin?

Answer: In Kotlin, an interface can implement another interface. The class implementing this interface would need to provide concrete definitions for the methods of both interfaces.

For example:

interface Interface1 {
    fun testFunction1()
}

interface Interface2 : Interface1 {
    fun testFunction2()
}

// This class would need to be provide implementations for both the interfaces
class ConcreteClass : Interface2 {
    override fun testFunction2() {
        TODO("Not yet implemented")
    }

    override fun testFunction1() {
        TODO("Not yet implemented")
    }

}

Conclusion

In this tutorial, we learned about 3 important concepts in Kotlin namely – Enums, Interfaces, and Generics. All these form the basic building blocks while writing any application in Kotlin language.

Enums are nothing but named collections and used widely to enhance code readability and reuse.

Interfaces are used to define abstract behavior and is a contract that needs to be followed by all the classes implementing an interface. In Kotlin, we can also have default implementations in Interface, which greatly makes it more flexible and extensible for any new changes.

Generics is another powerful tool, which is used for defining typed parameters – i.e. Something that needs to be implemented for objects of multiple types, that can be implemented through generic classes for typed parameters.

Kotlin Generics also offers strict compile time checking, which makes it even robust and safe to be used.

PREV Tutorial | FIRST Tutorial

Was this helpful?

Thanks for your feedback!

Leave a Comment