Introduction
This article demonstrates some advanced topics about OOPs in Kotlin, like comparing abstract classes and interfaces, interface delegation, Data Class, singletons, enums, and more. If you are a beginner and learning OOPs for the first time, you can read through the series below to learn Kotlin semantics first. This article covers some advanced OOPs concepts.
This article is part of the Kotlin Learning Series for programmers. You'll get the most value from this series if you go through the articles sequentially.
Previous Articles
About Object-oriented programming
Object-oriented programming (OOP) in Kotlin is a set of features that allow you to create more complex and sophisticated programs. These features include-
- Inheritance - You can use inheritance to create a new class based on an existing class. For example, you could create a class called
Car
that is based on the Vehicle
class. This would allow you to reuse the code from the Vehicle
class, such as the methods for starting the engine and driving the car.
- Polymorphism - You can use polymorphism to create classes that can perform the same tasks differently. For example, you could create a class called
Shape
that has methods for drawing a shape on the screen. You could then create subclasses Shape
for different types of shapes, such as Circle
and Rectangle
. The Shape
the class would have a method for drawing a shape, but the implementation of this method would be different for each subclass. This would allow you to draw different types of shapes on the screen using the same code.
- Abstraction - You can use abstraction to hide the implementation details of a class from its users. For example, you could create an abstract class called
Animal
that has methods for eating, sleeping, and moving. You could then create concrete classes for different types of animals, such as Dog
and Cat
. The Animal
the class would have the implementation of the methods for eating, sleeping, and moving, but the concrete classes would provide the specific implementation for each method. This would allow you to create different types of animals with different behaviors using the same code.
- Encapsulation - You can use encapsulation to control access to the data and methods of a class. For example, you could create a class called
BankAccount
that has properties for the account balance and the account number. You could then mark the properties as private, which would prevent other classes from accessing them directly. This would help to protect your data from unauthorized access.
These are just a few examples of how advanced OOP features can be used in Kotlin. Using these features, you can create more complex and sophisticated programs that are also more readable, maintainable, and secure.
Compare Abstract Classes And Interfaces
Kotlin interfaces and abstract classes are used to define common behavior or properties to be shared among other classes. Here are some points to remember about the abstract classes and interfaces.
-
Abstract Class can have constructors, whereas interfaces can't have constructors logic or store any state.
-
You cannot directly generate objects of an abstract class or an interface since neither of those kinds can be instantiated on its own.
-
To implement Abstract, unlike a regular class you don't need to mark them as open because they are always open, but properties and methods of an abstract class are non-abstract (not open); hence you have to mark them with the keyword abstract. That means subclasses can use them as given. If properties or methods are abstract, the subclasses must implement them.
-
You can implement only one abstract class from a subclass, whereas multiple interfaces can be implemented from a subclasses.
Let's describe abstract classes and interfaces using an example of the Car class. Here we create an abstract GeneralCar class for properties common to all cars and an interface named CarFunction to define behavior common to all cars.
Abstract Class
Open IntelliJ IDEA and create a new project name MyProject; create a new file called GeneralCar.kt. Create an abstract class, also called GeneralCar, and add a color variable with the abstract property.
abstract class GeneralCar : CarFunction {
abstract val color: String
}
GenetalCar has two subclasses, TataNexon and MarutiFronx. Because color
is abstract, the subclasses must implement it. Make TataNexon black and MarutiFronx white.
class TataNexon : GeneralCar(){
override val color: String = "black"
}
class MarutiFronx : GeneralCar(){
override val color: String = "white"
}
In Main.kt, create a buildCar()
function to test your classes. Instantiate a MarutiFronx and a TataNexon, then print the color of each.
Delete your earlier test code in Main()
and add a call to buildCar()
. Your code should look something like the code below.
fun buildCar() {
val marutiFronx = MarutiFronx()
val tataNexon = TataNexon()
println("Maruti Fronx : ${marutiFronx.color}")
println("Tata Nexon : ${tataNexon.color}")
}
fun main() {
buildCar()
}
Output
The following diagram represents the TataNexon
class and MarutiFronx
class, which subclass the abstract class, GeneralCar
.
Interface
Now let's create an interface named CarFunction with a method blowHorn().
interface CarFunction{
fun blowHorn()
}
Add CarFunction
to each of the subclasses and implement blowHorn()
by having it print what type of horn the car does.
class TataNexon :GeneralCar(), CarFunction{
override val color: String = "black"
override fun blowHorn() {
println("blowing horn with light sound ")
}
}
class MarutiFronx : GeneralCar(),CarFunction{
override val color: String = "white"
override fun blowHorn() {
println("blowing horn with heavy sound ")
}
}
Now let's call the blowHorn function on the buildCar function on each object of the general car subclasses.
fun buildCar() {
val marutiFronx = MarutiFronx()
val tataNexon = TataNexon()
println("Maruti Fronx : ${marutiFronx.color}")
marutiFronx.blowHorn()
println("Tata Nexon : ${tataNexon.color}")
tataNexon.blowHorn()
}
Output
The following diagram represents the Shark
class and the Plecostomus
class, both of which are composed of and implement the FishAction
interface.
Abstract classes versus interfaces when to use abstract classes versus interfaces
The aforementioned examples are straightforward, but if you have a large codebase with many classes, abstract classes, and interfaces can make your design simpler, more streamlined, and less difficult to maintain.
As was already mentioned, abstract classes can contain constructors, and interface can't, but aside from that, they are extremely similar. So when should each be used?
Use an abstract class when you want to share common code and behavior between multiple classes
Abstract classes are used when you want to define a common base for multiple related classes. An abstract class can contain both regular and abstract methods. It provides a way to define common behavior and attributes derived classes can inherit and override. Abstract classes can have constructors, properties, and non-abstract methods with default implementations. However, you cannot create instances of an abstract class directly. Instead, you need to derive a concrete class from the abstract class and create objects of the derived class.
Example- Use an abstract class any time you can't complete a class. For example, going back to the GeneralCar class, you can make all GeneralCar implement CarFunction, and provide a default implementation for blowHorn
while leaving color
abstract because there isn't a default color for each car.
interface CarFunction{
fun blowHorn()
}
abstract class GeneralCar : CarFunction {
abstract val color: String
override fun blowHorn() = println("Blowing default horn")
}
Use an interface when you want to define a contract that other classes must adhere to.
Interfaces, on the other hand, define a contract or a set of methods that a class must implement. An interface only contains abstract methods (methods without implementations) and can also define properties, but they must be abstract properties. A class can implement multiple interfaces, allowing it to provide implementations for all the methods defined in those interfaces. Interfaces are useful when you want to define a common behavior that can be shared across different classes, regardless of their inheritance hierarchy.
Example- Use an interface if you have a lot of methods and one or two default implementations, for example as in CarFunction
below.
interface CarFunction{
fun blowHorn()
fun wipeGlass()
fun fillTank()
fun drive(){
println("car is running")
}
}
Interface Delegation
Previously we have gone through abstract classes and interfaces; Interface delegation is an advanced technique where the methods of an interface are implemented by now the class itself rather than by a helper (or delegate) object. This technique can be useful when you use an interface in a series of unrelated classes, you add the needed interface functionality to a separate helper class, and each of the classes uses an instance of the helper class to implement the functionality.
Let's see Interface Delegation by example-
In the GeneralCar.kt file TataNexon() class is implemented by GeneralCar() abstract class and CarFunction() Interface. So let's implement this through interface delegation.
Create a new interface CarColor, and define colors as a string.
interface CarColor{
val color: String
}
Change the implementation of TataNexon() to implement two interfaces CarFunction and a CarColor. We must override colour from CarColor() and blowHorn() from CarFunction.
class TataNexon : CarColor,CarFunction{
override val color: String = "white"
override fun blowHorn() {
println("blowing horn with light sound ")
}
}
Your finished code should look something like this
interface CarColor{
val color: String
}
class TataNexon : CarColor,CarFunction{
override val color: String = "white"
override fun blowHorn() {
println("blowing horn with light sound ")
}
}
interface CarFunction{
fun blowHorn()
}
abstract class GeneralCar : CarFunction {
abstract val color: String
override fun blowHorn() = println("Blowing default horn")
}
Setup Delegation with a singleton class
Now we create delegation by creating a helper class that implements the CarColor, i.e. BlackColor and because it doesn't make any sense to create a multiple instance of BlackColor. We can declare a class using the keyword "object" instead of "class," which allows for the creation of a single instance. Kotlin automatically generates this instance, and it is referenced by the class name. Subsequently, all other objects can utilize this sole instance, and there is no provision for generating additional instances of the class. If you are acquainted with the singleton pattern, this is Kotlin's implementation approach for achieving singletons.
In the GeneralCar.kt create an object for BlackColor, which implement CarColor and thus override the color variable.
object BlackColor : CarColor{
override val color: String ="black"
}
So, we now had a helper method BlackColor ready to use as interface delegation.
In GeneralCar.kt remove the override color from TataNexon() class, and after CarColor, add "by BlackColor" to the class definition parameter, now the delegation is created. What this does here is that instead of implementing, CarColor
use the implementation provided by BlackColor
. So every time color
is accessed, it is delegated to BlackColor.
class TataNexon : CarFunction,CarColor by BlackColor{
override fun blowHorn() {
println("blowing horn with heavy sound ")
}
}
With this, we added interface delegation, but the TataNexon comes in different colors than black, so we can change the class to pass the color with its constructor and set its default to BlackColor.
class TataNexon(carColor: CarColor = BlackColor) : CarFunction,CarColor by carColor{
override fun blowHorn() {
println("blowing horn with heavy sound ")
}
}
Add interface delegation for CarFunction
We can implement interface delegation to MarutiFronx in the same way. In the GeneralCar.kt make a PrintingCarFunction class that implements CarFunction, which takes a String, horn type, then prints what type of horn the car blows.
class PrintingCarFunctions(val hornType : String) :CarFunction{
override fun blowHorn() {
println(hornType)
}
}
So with that, we have our delegate class ready, without waiting to just remove the hornBlow method from TataNexon and a delegate of CarFunction. With the delegation, there is no code in the body, so remove the curly braces like below.
class TataNexon(carColor: CarColor = BlackColor) : CarFunction by PrintingCarFunctions("light sound horn"), CarColor by carColor
Interface delegation allows for code reuse, as the implementation of the interface is delegated to another class. It also enables composition over inheritance, as the class can implement multiple interfaces by delegating their methods to different delegate objects. This pattern promotes modularity, flexibility, and separation of concerns in object-oriented design.
Data Class
The data class is primarily made for storing data as an object in Kotlin. It is a special type of class used to hold and represent data. It is designed to automatically generate several standard methods, such as equals()
, hashCode()
, toString()
, and copy()
, based on the properties defined in the class.
It is similar to a struct in other languages with extra utilities such as printing and copying. Let's Create a Data Class to understand the concept in detail.
Add a new Package MyCarParts under the kotlin file and create CarPartsData.kt file.
Create a new CarParts class.
class CarParts() {
}
Add a data prefix to make CarParts data a data class and add a String property tire to CarParts have some data.
data class CarParts(val tire : String) {
}
In the file, outside the class, add a makeCarParts()
function to create and print an instance of a CarParts tire with "
Ceat"
.
fun makeCarParts(){
val car1Parts = CarParts("Ceat")
println(car1Parts)
}
Add a main()
function to call makeCarParts()
, and run your program. Notice the sensible output that is created because this is a data class.
fun main() {
makeCarParts()
}
Output
In makeCarParts()
, instantiate two more CarParts objects that are both "MRF" and print them
fun makeCarParts(){
val car1Parts = CarParts("Ceat")
println(car1Parts)
val car2Parts = CarParts("MRF")
println(car2Parts)
val car3Parts = CarParts("MRF")
println(car3Parts)
}
In makeCarParts()
, add a print statement that prints the result of comparing Car1Parts with Car2Parts, and a second one comparing Car3Parts with Car2Parts. Use the equals() method that is provided by data classes.
Output
Destructuring in the data class
To get all the properties from a data object, we can use the destructing.
The normal approach
val tire = Car1Parts.tire
val sideGlass = Car1Parts.sideGlass
val staring = Car1Parts.staring
Using Destructing
val (tire,sideGlass,staring) = Car1Parts
This is a useful shorthand approach. The number of variables should match the number of properties, and the variables are assigned in the order in which they are declared in the class. Let's see an example here-
data class CarParts2(val tire : String, val sideGlass :String, val airCondition : Boolean )
fun makeCarParts(){
val c5 = CarParts2("MRF","Chevrolet Beat",true)
println(c5)
val (tire,sideGlass,airCondition) =c5
println(tire)
println(sideGlass)
println(airCondition)
}
Output-
In case you need only some properties, you can skip them by _ instead of a variable name like the below code.
val (tire,_,airCondition) =c5
Singletons, Enums, And Sealed Classes
There are some special-purpose classes in Kotlin; let's describe some of them that are used most frequently
- Singleton classes
- Enums
- Sealed classes
Singleton Classes
Earlier, we used a singleton BlackColor class.
object BlackColor : CarColor{
override val color: String ="black"
}
Because every instance of BlackColor does the same thing, it is declared as an object
instead of as a class
to make it a singleton. There can be only one instance of it.
Enums
An enum in Kotlin is a specific type that represents a collection of connected constants. It enables you to provide a predetermined range of values that an object may have. When you wish to represent a specific set of values or when you have a specified list of possibilities, enumerations might be helpful.
Enums can be declared using the keyword enum. Let's see a basic example of an enum below.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
Enum is declared as singletons where they can have only one and one value in the enumeration.
Try out another example
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
fun main() {
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
}
Output
Sealed classes
A sealed class in Kotlin is a class that can be extended by only the subclass inside the same file. This means a limited number of possible types of objects can be created from a sealed class. Try to subclass the class in a different file; you get an error.
Kotlin will know all the subclasses statically at the compile time and checks all the possibilities for types. Let's see this by an example-
In the GeneralCar.kt file, create a sealed class by the sealed keyword.
sealed class CarFuelType {
class Petrol : CarFuelType()
class Diesel : CarFuelType()
class Electric : CarFuelType()
}
To use this class, add a fillTank() method in the CarFunction interface and implement it. After that, just call the fill tank method on the car object. The end code will look like this-
GeneralCar.kt
interface CarColor{
val color: String
}
object BlackColor : CarColor{
override val color: String ="black"
}
class TataNexon(carColor: CarColor = BlackColor) : CarFunction by PrintingCarFunctions("light sound horn"), CarColor by carColor
class PrintingCarFunctions(val hornType : String) :CarFunction{
override fun blowHorn() {
println(hornType)
}
override fun fillTank(FuelType: CarFuelType) {
println(fillCarFuel(FuelType))
}
}
sealed class CarFuelType {
class Petrol : CarFuelType()
class Diesel : CarFuelType()
class Electric : CarFuelType()
}
fun fillCarFuel(carFuelType: CarFuelType) :String {
return when(carFuelType){
is CarFuelType.Petrol -> "Fill Petrol only"
is CarFuelType.Diesel -> "Fill disel only"
is CarFuelType.Electric -> "Charge this Car"
else -> "error fuel type"
}
}
interface CarFunction{
fun blowHorn()
fun fillTank(FuelType:CarFuelType)
}
Main.kt
fun buildCar() {
val tataNexon = TataNexon()
println("Tata Nexon : ${tataNexon.color}")
tataNexon.blowHorn()
tataNexon.fillTank(CarFuelType.Electric())
}
fun main() {
buildCar()
}
Output
Conclusion
Object-oriented programming (OOP) in Kotlin provides several features to create more complex and sophisticated programs. These features include inheritance, polymorphism, abstraction, and encapsulation. The choice between abstract classes and interfaces depends on the specific requirements of the codebase. Interface delegation is an advanced technique where the methods of an interface are implemented by the class itself rather than a separate helper object. Data classes are a special type of class designed to store and represent data. Destructuring is a feature in Kotlin that allows extracting properties from objects directly. These concepts can help create more complex, maintainable, and flexible programs. Hope this has been a helpful guide for you, dont forget to write your views about this article in the comments. Thank you!
FAQ's
Some possible questions & answers related to OOP in Kotlin include,
Q 1 - What are the main features of object-oriented programming (OOP) in Kotlin?
The main features of OOP in Kotlin include inheritance, polymorphism, abstraction, and encapsulation.
Q 2 - How does inheritance work in Kotlin?
Inheritance allows you to create a new class based on an existing class. It enables code reuse by inheriting the properties and methods of the parent class.
Q 3 - What is polymorphism in Kotlin?
Polymorphism allows you to create classes that can perform the same tasks differently. It enables you to define methods in a superclass and override them in subclasses with different implementations.
Q 4 - How does abstraction help in OOP?
Abstraction allows you to hide the implementation details of a class from its users. It provides a way to define common behavior and attributes in an abstract class or interface that can be shared by multiple classes.
Q 5 - What is encapsulation, and why is it important?
Encapsulation is a concept that allows you to control access to the data and methods of a class. It helps in protecting the data from unauthorized access and ensures that the class's internal state remains consistent.
Q 6 - What is the difference between abstract classes and interfaces in Kotlin?
Abstract classes can have constructors and can contain both regular and abstract methods. They are used when you want to define a common base for multiple related classes. Interfaces, on the other hand, define a contract of methods that a class must implement. They contain only abstract methods and are used to define common behavior shared across different classes.
Q 7 - When should you use an abstract class?
Abstract classes should be used when you want to define a common base for multiple related classes and share common code and behavior among them.
Q 8 - When should you use an interface?
Interfaces should be used when you want to define a contract of methods that a class must implement. They are useful for defining common behavior that can be shared across different classes, regardless of their inheritance hierarchy.
Q 9 - What is interface delegation in Kotlin?
Interface delegation is a technique where the methods of an interface are implemented by a separate helper class instead of the implementing class itself. It allows for code reuse and promotes modularity and flexibility in design.
Q 10 - What is a data class in Kotlin?
A data class is a special type of class in Kotlin that is designed for storing and representing data. It automatically generates several standard methods, such as equals(), hashCode(), toString(), and copy(), based on the properties defined in the class. Data classes are used to simplify working with immutable data objects.