DEV Community

Cover image for Exploring Kotlin's Abstract Classes and Specialized Class Types: A Guide to Object-Oriented Programming Concepts.
Daniel Brian
Daniel Brian

Posted on

Exploring Kotlin's Abstract Classes and Specialized Class Types: A Guide to Object-Oriented Programming Concepts.

In this article, we are going to look at two concepts that are crucial in Modern Android development.
By the end of this article, Our aim will be to dive into:
1. Abstract classes
2. Special kinds of classes in Kotlin

I. Abstract Classes

  • Abstract classes can be thought of as a hybrid of an open class and an interface.
  • An abstract class is declared using the abstract keyword in front of the class.

abstract class className {}

Image

  • Abstract classes can contain both abstract and non-abstract member properties in an abstract class. These properties are marked with the abstract modifier and they do not have a body as shown below.
abstract class className(val x : String) //non-abstract property
 {
    abstract var y : Int //Abstract property
    abstract fun method () //Abstract property
    fun method () //non-abstract method property
                 {
    println("Non abstract function")
                  }
}
Enter fullscreen mode Exit fullscreen mode
  • Abstract classes cannot be used to create objects. However, you can inherit subclasses from them but they need to be overridden.
  • The key advantage of abstract classes is that they can have non-open methods and non-abstract properties.
abstract class Employee (val name : String,val experience : Int){
    //abstract property
    abstract var salary : Double 
    abstract fun dateOfBirth(date : String)
    //non-abstract property
    fun employeeDetails() {
        println("Name of the employee : $name")
        println("Experience in years : $experience")
        println("Annual salary : $salary")
    }
}
//derived class
class Engineer (name: String, experience: Int): Employee(name, experience) {
    override var salary = 5000.00
    override fun dateOfBirth(date : String){
        println("Date of Birth is: $date")
    }
}
fun main () {
    val eng = Engineer("Praveen",2)
    eng.employeeDetails()
    eng.dateOfBirth("02 December 1994")
}
Enter fullscreen mode Exit fullscreen mode
  • Abstract classes are mainly used when you want to specify a set of generic operations for multiple classes. An abstract member of the abstract class can be overridden in all the derived classes.
  • In the below class we override the cal function in three derived classes of the calculator.
abstract class Calculator {
    abstract fun cal (firstNum : Int, secondNum : Int): Int
}
//Addition of two numbers
class Add : Calculator () {
    override fun cal (firstNum : Int, secondNum : Int) : Int{
        return firstNum + secondNum
    }
}
//Division of two numbers
class Divide : Calculator () {
    override fun cal (firstNum : Int, secondNum : Int) : Int{
        return firstNum / secondNum
    }
}
//Multiplication of two numbers
class Multiply : Calculator () {
    override fun cal (firstNum : Int, secondNum : Int) : Int{
        return firstNum * secondNum
    }
}
//Subtraction of two numbers
class Subtract : Calculator () {
    override fun cal (firstNum : Int, secondNum : Int) : Int{
        return firstNum - secondNum
    }
}

fun main () {
    var add : Calculator = Add()
    var add1 = add.cal(4,3)
    println("Addition of two numbers $add1")

    var subtract : Calculator = Subtract()
    var add2 = subtract.cal(4,3)
    println("Subtraction of two numbers $add2")

    var multiply : Calculator = Multiply()
    var add3 = multiply.cal(4,3)
    println("Multiplication of two numbers $add3")

    var divide : Calculator = Divide()
    var add4 = divide.cal(24,3)
    println("Division of two numbers $add4")
}
Enter fullscreen mode Exit fullscreen mode
  • In kotlin, we can override the non-abstract open member function of the open class using the override keyword followed by an abstract in the abstract class.
open class vertebrates {
     open fun Backbone () {
        println("All vertebrates have a backbone")
    }
}

abstract class Animal : vertebrates () {
    override abstract fun Backbone ()
}   

class Birds : Animal () {
    override fun Backbone () {
        println("Birds have a backbone")
    }
}

fun main () {
    val animals = vertebrates ()
    animals.Backbone()
    val birds = Birds()
    birds.Backbone()

}
Enter fullscreen mode Exit fullscreen mode

II. Special Kind of Classes

  • There are different classes each with a dedicated purpose. These classes include:
  1. Data classes
  2. Enum classes
  3. Exception classes
  4. Sealed classes
  5. Annotation classes

1. Data classes

  • Data classes are a special kind of class dedicated to serving as a holder of data. They help to compare, display, read, and modify this data more easily.

Image

  • Basic values like strings can be compared using the double equality sign.
class superCars (val name : String,val year : Int)
fun main () {
    var car1 = superCars("volvo",2020)
    var car2 = superCars("BMW" ,2021)
    var car3 = superCars("volvo",2020)
    var car4 = superCars("Benz",2019)

    println(car1 == car2)//false
    println(car1 == car1)//true
    println(car3 == car4)//false
}
Enter fullscreen mode Exit fullscreen mode
  • The result is true if the values are alike and false if the values vary.
  • When we use the dot syntax in our main function to call the properties of the class as shown below.
class superCars (val name : String,val Modelyear : Int)
fun main () {
    var car1 = superCars("volvo",2020)
    var car2 = superCars("BMW" ,2021)
    var car3 = superCars("volvo",2020)
    var car4 = superCars("Benz",2019)

    println(car1.name)
    println(car1.Modelyear)

    println(car4.name)
    println(car4.Modelyear) 

}
Enter fullscreen mode Exit fullscreen mode

The output of the code will volvo 2020 and Benz 2019

  • When we call the instances of our class without specifying the keyword data, the output gives the memory location of where the instance is stored in the class.
class superCars (val name : String,val Modelyear : Int)
fun main () {
    var car1 = superCars("volvo",2020)
    var car2 = superCars("BMW" ,2021)
    var car3 = superCars("volvo",2020)
    var car4 = superCars("Benz",2019)

    println(car1)//superCars@37f8bb67
    println(car2)//superCars@439f5b3d
           }
Enter fullscreen mode Exit fullscreen mode

The output of the code above is superCars@37f8bb67 for car1 and superCars@439f5b3d for car2 respectively.

  • When we specialize the keyword data before class the output of the code changes. This is because the behavior of equality has changed.
data class superCars (val name : String,val Modelyear : Int)
fun main () {
    var car1 = superCars("volvo",2020)
    var car2 = superCars("BMW" ,2021)
    var car3 = superCars("volvo",2020)
    var car4 = superCars("Benz",2019)
println(car1)
println(car2)
    }
Enter fullscreen mode Exit fullscreen mode
  • Data classes have a copy method that creates a copy of an object. It allows you to specify what modifications you would like to introduce to an object.
data class Person(val firstName: String, val familyName:String) {
var age = 0
}
fun main() {

val person1 = Person("John", "Doe").apply { age = 25 }
val(firstName,familyName) = person1

println(person1.copy(familyName = "Maye"))
}
Enter fullscreen mode Exit fullscreen mode
  • You use data modifiers for classes that are used to represent a bundle of data.
Pair and Triple
  • This is a data class with two constructor properties, you don't need to define it, as it is distributed with Kotlin.
  • Pair is used to keep two values on properties first, and second.
  • You can use first and second properties when working with pair and triple.
fun main () {

    val fruits = Pair ("orange","Banana")

    //Using the first property 
    println(fruits.first)

     //Using the second property 
    println(fruits.second)
}
Enter fullscreen mode Exit fullscreen mode
  • We can use to function between two values using the pair property.
fun main () {

    val Pair = 10 to "A"

    //Using the first property 
    println(Pair.first)

     //Using the second property 
    println(Pair.second)
}
Enter fullscreen mode Exit fullscreen mode
  • Triple is used to keep three values on properties first, second, and third.
fun main () {

    val pair = Triple("Daniel",45, 'M')

    //Using the first property 
    println(pair.first)

     //Using the second property 
    println(pair.second)

    //Using the third property 
    println(pair.third)  
           }
Enter fullscreen mode Exit fullscreen mode
  • we can also get the same output using the below code
fun main () {

    val pair = Triple("Daniel",45,'M')
    val(name,age,gender) = pair

    //Getting the first property 
    println(name)

     //Getting the second property 
    println(age)

    //Getting the third property 
    println(gender)  
    }
Enter fullscreen mode Exit fullscreen mode

2. Enum classes

  • An enum has its own specialized type, indicating that something has a number of possible values.

Image

  • Enums are defined by using the enum keyword in front of a class.
enum class days {
sunday,
monday,
tuesday,
wednesday,
thursday,
friday,
saturday
}
Enter fullscreen mode Exit fullscreen mode
  • Enum have a constructor. Their constants are instances of an enum class. The constants can be initialized by passing specific values to the primary constructor.
  • we can access the colour of the ripe banana using an instance that we create in our main function.
enum class bananas (val color : String) {
    ripe("yellow"),
    unripe("green")
}
fun main () {

   val colour = bananas.ripe.color
    println(colour)
}
Enter fullscreen mode Exit fullscreen mode
  • Kotlin enum classes has some inbuilt properties and functions which can be used by a programmer.
  • In properties we have ordinal and name. The ordinal property stores the ordinal value of the constant, which is usually a zero-based index. The name property stores the name of the constant.
  • In methods we have values and valueOf. The values property returns a list of all constants defined within the enum class.
  • The valueOf property returns the enum constant defined in enum, matching the input string. If the constant, is not present in the enum, then an illegalArgumentException is thrown.
enum class HalfYear {
    January,
    February,
    March,
    April,
    May,
    June
}
fun main () {
    for(month in HalfYear.values()){
        println("${month.ordinal} = ${month.name}")
        }
    }
Enter fullscreen mode Exit fullscreen mode
  • We can access the individual month from the class using the valueOf property in the main function.

println("${HalfYear.valueOf("April")}")

  • Enum classes can be combined with when expression. Since enum classes restrict the value a type can take, so when is used with the when expression and the definitions for all the constants provided, then the need for the else clause is completely eliminated.
enum class Planets {
    Mercury,
    Venus,
    Earth,
    Mars,
    Jupiter,
    Saturn,
    Uranus
}

fun main() {
    for (planet in Planets.values()) {
        when (planet) {
            Planets.Mercury -> println("The Swift Planet")
            Planets.Venus -> println("The Morning and evening star")
            Planets.Earth -> println("The Goldilocks Planet")
            Planets.Mars -> println("The Red Planet")
            Planets.Jupiter -> println("The Gas giant Planet")
            Planets.Saturn -> println("The Ringed Planet")
            Planets.Uranus -> println("The Ice giant Planet")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Enum constants also behave as anonymous classes by implementing their own functions along with overriding the abstract functions of the class.
  • The most important thing is that each enum constant must be overridden.
enum class seasons (var weather : String) {

    Summer ("hot") {
        override fun foo () {
            println("Hot days of the year")
        }
    },

     Winter ("cold") {
        override fun foo (){
            println("Cold days of the year")
        }
    },

     Rainny ("rain") {
        override fun foo (){
            println("Rainny days of the year")
        }
    };

 abstract fun foo ()
}

fun main () {
    seasons.Winter.foo()
}
Enter fullscreen mode Exit fullscreen mode
  • In enum classes functions are usuallly defined within the companion object so that they do not depend on specific instances of the class.
  • However, they can be defined without companion objects also.
enum class DAYS(val isWeekend: Boolean = false){
    SUNDAY(true),
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY(true);

    companion object{
        fun today(obj: DAYS): Boolean {
            return obj.name.compareTo("SATURDAY") == 0 || obj.name.compareTo("SUNDAY") == 0
        }
    }
}

fun main(){
    for(day in DAYS.values()) {
        println("${day.ordinal} = ${day.name} and is weekend ${day.isWeekend}")
    }
    val today = DAYS.MONDAY;
    println("Is today a weekend ${DAYS.today(today)}")
}
Enter fullscreen mode Exit fullscreen mode

3. Exception classes

  • Exceptions are the errors which comes at the runtime and disrupts your flow of execution of the program.
  • We have two types of execution;

Image
i. Checked Exception - these are exceptions that occur at the compile time eg IOException.
ii. Unchecked Exception - these are exceptions that occur at runtime eg OutOfBoundException. In kotlin we use unchecked exceptions.

  • We have some keywords which help us to handle exceptions.

i. Try - It helps us to find the exceptions.
ii. Throw - If the exception is found it throws the exception.
iii. Catch - After throwing it will catch the exception and
execute the body.
iv. Finally - It will always execute either we get
exception or not.

  • If the program exit by exitProcess(int) or absort(), then the finally will not be executed.
  • The syntax :
try {
    // your code
    // may throw an exception
}
catch (ex : ExceptionName)
{
    // Exception handle code
}
finally
{
    //This will execute everytime
    //It will execute whether we find an exceptiion or not
}
Enter fullscreen mode Exit fullscreen mode
  • We can use the above syntax to create an exception class to divide a number by zero.
fun main () {

 try {

 divide (10,0)

    }
catch (ex : Exception)
{
    println(ex.message)
  }
}

fun divide (a : Int, b : Int){

    if(b == 0)
    throw Exception("Divide by Zero")
    println("Division is :" +a /b)
} 
Enter fullscreen mode Exit fullscreen mode
  • We can add the finally block in our code above. In the below below finally is executed in both cases either exceptions occur or not.
 fun main()
   {
    try
    {
    divide(20, 10)
    }
    catch (ex : Exception)
    {
    println(ex.message)
    }
    finally
    {
    println("I'm executed")
    }

    // 2nd try block
    try
    {
    // throw an exception
    divide(10, 0)
    }
    catch (ex : Exception)
    {
    println(ex.message)
    }
    finally
    {
    println("I'm executed")
    }
}

fun divide(a : Int, b : Int)
{
    if (b == 0)
        throw Exception("Divide by zero")
    println("Division is :" + a / b)
}
Enter fullscreen mode Exit fullscreen mode

4. Sealed classes

  • A sealed class defines a set of subclasses within it. Sealed classes ensure type safety by restricting the types to be matched at compile-time rather than at runtime.

Image

  • The syntax declaration of sealed classes is as shown below : sealed class NameOfTheClass
  • Sealed classes constructors are protected by default. Sealed class is implicitly abstract and hence it cannot be instantiated.
sealed class DemoClass
    fun main () {
        var InstanceDemo = Demo ()
    }
Enter fullscreen mode Exit fullscreen mode
  • The above code would not run because of compile error as sealed classes cannot be instantiated.
  • All subclasses of a sealed class must be defined within the same kotlin file.
sealed class School {
    class Public:School(){
        fun display(){
            println("They are funded by the government")
        }
    }
    class Private:School(){
        fun display(){
            println("They are funded by individuals")
        }
    }
}

fun main(){
    val obj = School.Public()
    obj.display()
    val obj1= School.Private()
    obj1.display()
}
Enter fullscreen mode Exit fullscreen mode
  • You cannot define a subclass of a sealed class inside a subclass because the sealed class woukd not be visible.
  • Sealed classes mostly uses when and eliminates the else clause.
// A sealed class with a string property
sealed class Fruit(val x : String)
{
    // Two subclasses of sealed class defined within
    class Apple : Fruit("Apple")
    class Mango : Fruit("Mango")
}

// A subclass defined outside the sealed class
class Pomegranate: Fruit("Pomegranate")

// A function to take in an object of type Fruit
// And to display an appropriate message depending on the type of Fruit
fun display(fruit: Fruit)
{
    when(fruit)
    {
        is Fruit.Apple -> println("${fruit.x} is good for iron")
        is Fruit.Mango -> println("${fruit.x} is delicious")
        is Pomegranate -> println("${fruit.x} is good for vitamin d")
    }
}
fun main()
{
    // Objects of different subclasses created
    val obj = Fruit.Apple()
    val obj1 = Fruit.Mango()
    val obj2 = Pomegranate()

    // Function called with different objects
    display(obj)
    display(obj1)
    display(obj2)
}
Enter fullscreen mode Exit fullscreen mode

5. Annotation classes

  • Annotations allows the programmer to embed supplemental information into the source file. The information does not change the actions of the program.

Image

  • Annotations contains compile-time constants parameters which include;

    1. primitive types(Int,Long etc)
    2. strings
    3. enumerations
    4. class
    5. other annotations
    6. arrays of the above-mentioned types
  • We can apply annotation by putting its name prefixed with the @ symbol in front of a code element.

 `@Positive val i : Int`
Enter fullscreen mode Exit fullscreen mode
  • A parameter can be passed in parenthesis to an annotation similar to function call.
 `@Allowedlanguage("Kotlin")`
Enter fullscreen mode Exit fullscreen mode
  • When an annotation is passed as parameter in another annotation, then we should omit the @ symbol. Here we have passed Replacewith() annotation as parameter.
` @Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))`
Enter fullscreen mode Exit fullscreen mode

When an annotation parameter is a class object, we should add ::class to the class name as:

@Throws(IOException::class) 
Enter fullscreen mode Exit fullscreen mode
  • To declare an annotation, the class keyword is prefixed with the annotation keyword.
  • By their nature, declarations of annotation cannot contain any code.
  • While declaring our custom annotations, we should specify to which code elements they might apply and where they should be stored.
    The simplest annotation contains no parameters

    annotation class MyClass

  • An annotation that requires parameter is much similar to a class with a primary constructor

    annotation class Suffix(val s: String)

  • We can also annotate the constructor of a class. It can be done by using the constructor keyword for constructor declaration and placing the annotation before it.

class MyClass@Inject constructor(dependency: MyDependency) {  
//. . .   
}
Enter fullscreen mode Exit fullscreen mode
  • We can annotate the properties of class by adding an annotation to the properties.
class Lang (
    @Allowedlanguages(["Java","Kotlin"]) val name: String)
}
Enter fullscreen mode Exit fullscreen mode
  • Kotlin also provides certain in-built annotations, that are used to provide more attributes to user-defined annotations.

@Target

  • This annotation specifies the places where the annotated annotation can be applied such as classes, functions, constructors, type parameters, etc.
  • When an annotation is applied to the primary constructor for a class, the constructor keyword is specified before the constructor.
@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.LOCAL_VARIABLE)
annotation class AnnotationDemo2

class ABC @AnnotationDemo2 constructor(val count:Int){
    fun display(){
        println("Constructor annotated")
        println("Count is $count")
    }
}
fun main(){
    val obj =  ABC(5)
    obj.display()
    @AnnotationDemo2 val message: String
    message = "Hello"
    println("Local parameter annotated")
    println(message)
}
Enter fullscreen mode Exit fullscreen mode

@Retention

  • This annotation specifies the availability of the annotated annotation i.e whether the annotation remains in the source file, or it is available at runtime, etc.
  • Its required parameter must be an instance of the AnnotationRetention enumeration that has the following elements:
  • SOURCE
  • BINARY
  • RUNTIME Example to demonstrate @Retention annotation:
//Specifying an annotation with runtime policy
@Retention(AnnotationRetention.RUNTIME)
annotation class AnnotationDemo3

@AnnotationDemo3 fun main(){
    println("Main function annotated")
}
Enter fullscreen mode Exit fullscreen mode

@Repeatable

  • This annotation allows an element to be annotated with the same annotation multiple times.
  • As per the current version of Kotlin 1.3, this annotation can only be used with the Retention Policy set to SOURCE.
@Repeatable
@Retention(AnnotationRetention.SOURCE)
annotation class AnnotationDemo4 (val value: Int)

@AnnotationDemo4(4)
@AnnotationDemo4(5)
fun main(){
    println("Repeatable Annotation applied on main")
}
Enter fullscreen mode Exit fullscreen mode

💫💫Thank you for taking the time to read my article, I appreciate your interest and support. I hope you enjoyed😇😇🥳🥳

Image

Top comments (0)