DEV Community 👩‍💻👨‍💻

Cover image for Kotlin in Action Summary - Part 3
Mahdi Abolfazli
Mahdi Abolfazli

Posted on

Kotlin in Action Summary - Part 3

This is part 3 of the series Kotlin in Action Summary. Here is the link to part 2 which we talked about chapter 3 of the book with the title "Defining and Calling Functions". As mentioned before, it is highly recommended to read the book first and consider this series as a refresher. We do NOT cover all the materials in the book. Let's start with part 3, chapter 4 of the book: "Classes, Objects and Interfaces"!

Contents

Interfaces

pages 68, 69, 70

Kotlin's interfaces are similar to that of Java 8. They can contain abstract methods and implementation of non-abstract methods(like Java 8 default methods), but they cannot have any state!
This is how we declare and use interfaces in Kotlin:

interface Clickable {
    fun click()
    fun showOff() = println("Clickable showOff")
}

interface Focusable {
    fun setFocus(b: Boolean)
    fun showOff() = println("Focusable showOff")
}

class Button: Clickable, Focusable {
    override fun click() = println("button clicked")
    override fun setFocus(b: Boolean) = println("set focus")

    override fun showOff() {
        super<Focusable>.showOff()
        super<Clickable>.showOff()

    }
}
Enter fullscreen mode Exit fullscreen mode

Here are some points to consider:

  • In Kotlin, we use colon : instead of implements and extends keywords
  • override is like the @Override annotation in Java, but it is mandatory to use in Kotlin.
  • To provide implementation for a method in Kotlin, we do not need to use the default keyword.
  • If we implement two interfaces in a class that contain the same method signature, we must provide the implementation of that method in our class.
  • To call the parent class's method, we need to use the super keyword in Kotlin. But to select a specific implementation we need to use super alongside with the ClassName inside angle brackets: super<Clickable>.showOff()

Open, Final and Abstract Modifiers

pages 70, 71, 72

In contrast to java, Kotlin classes are final by default. If we want to allow a class to be extended, we need to mark it with the open modifier. All properties and methods of that class are also final, so we have to use the open modifier for every one of them that we want to be overridden.

open class  RichButton : Clickable {
    // 1
    fun disable() {}

    // 2
    open fun animate() {}

    // 3
    override fun click() {}
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> disable() function is not open (methods are final by default), so it cannot be overridden
  • 2 -> animate() function is open, so it can be overridden
  • 3 -> click() function overrides an open method, so it is open itself and can be overridden

NOTE: If we override a member of a base class or interface, the overridden member is open by default. To prevent it from being overridden, we should mark it with final modifier.

As in Java, Kotlin supports abstract classes which cannot be instantiated. Why do we use abstract classes? Sometimes we need a contract but do not want to provide implementation. We want the implementation to be provided by the subclasses. Abstract members of an abstract class are always open and do not require us to use open keyword:

abstract class Animated {
    // 1
    abstract fun animate()

    // 2
    open fun stopAnimating() {}

    // 3
    fun animateTwice() {}
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> animate() function is marked with abstract keyword, so it is open and do not need the open modifier
  • 2 -> stopAnimating() function is marked with open, so it can be overridden
  • 3 -> animateTwice() function is neither abstract nor open, so it cannot be overridden by subclasses.

NOTE: A member in an interface is always open; we cannot declare it to as final. It's abstract if it has no body, but the keyword is not required.

Visibility Modifiers

pages 73, 74

Visibility modifiers help us to control access to declarations in our code base. Why should we do so? Because by enforcing encapsulation, we are sure that other classes do not depend on the implementation of our class, but rather on the abstraction. We can freely change the implementation and do not break anything in the client code!

Visibility modifiers in Kotlin are similar to those in Java. We have public, private, and protected with some differences. One difference is that, a class with no modifier is public by default and Kotlin does not support package-private modifier.
Why Kotlin does not have package-private modifier? Because they have replaced it with something better: we can use internal to declare a class to be visible inside a module.
What is a module? A module is a set of Kotlin files compiled together. It might be IntelliJ IDEA module, Gradle or Maven project.
What is the advantage? It provides real encapsulation for the implementation details of our module.

Another difference is that Kotlin allows the use of private visibility for top-level declarations, including classes, functions and properties. These are visible only in the file where they are declared.

To list the difference in visibility between Java and Kotlin:

  1. Kotlin default visibility is public
  2. Kotlin does NOT have package-private visibility
  3. Kotlin supports module private visibility with internal keyword.
  4. Kotlin supports private visibility for top-level declarations meaning visible inside the file.
  5. protected modifier in Kotlin is just visible to the class itself or its subclasses, but in Java, it is also visible to classes in the same package!
  6. Unlike Java, an outer class doesn't see private members of its inner (or nested) classes in Kotlin
  7. Kotlin forbids us to reference less visible types from more visible members(e.g. referencing a private class from a public function) because it will expose the less visible type!
// 1
fun TalkativeButton.giveSpeech() {
    // 2
    yell()

    // 3
    whisper()
}
Enter fullscreen mode Exit fullscreen mode
  • 1 ->
  • 2 ->
  • 3 ->

Inner and Nested Classes

pages 75, 76,

As in Java, you can declare a class inside another class in Kotlin. Difference? Unlike Java, the Kotlin inner class does not have access to the outer class instance, unless we specify so.

A nested class in Kotlin is the same as a static nested class in Java. To turn a nested class to an inner class (so the inner class has a reference to the outer class), we have to mark it with inner modifier.

Nested class vs Inner class

How do we reference an instance of an outer class inside the inner class? We use this@OuterClassName syntax as in the following example:

class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}
Enter fullscreen mode Exit fullscreen mode

In Java, we use OuterClassName.this syntax to refer to the outer class instance.

Sealed Classes

pages 77, 78

On the first glance sealed classes are similar to enums. The difference is quite obvious — enums are a group of related constants, sealed classes are a group of related classes with different states. The other difference is that, enum constant is available only in one instance, whereas sealed classes can have multiple instances with different values.

We use sealed classes when we want to impose restriction on the number of options and values to a super class. What would be the benefit? First, we don't have to provide an else branch in when expressions. Second, when we add another subclass to the super class, we get a compile time error if we don't cover that subclass in the when expressions that we have written.
Let's see an example:

interface Result
class Success(val data: Any) : Result
class Error(val error: Exception) : Result

fun result(result: Result) : Any =
    when (result) {
        is Success -> result.data
        is Error -> result.error
        else -> throw java.lang.IllegalArgumentException("Unknown Expression")
    }
Enter fullscreen mode Exit fullscreen mode

In the sample above, there are some problems. First problem is that we always have to provide an else branch for every when expression we write for Result interface. Also, if we decide to add another type to the subtypes of the Result, say NetworkError, then we will face an IllegalArgumentException at runtime.

To resolve these issues, we use sealed classes. When we use sealed classes and handle all the subclasses in a when expression, we don't have to provide the else branch. If we add another subclass and forget to handle that new subclass in a when expression, we'll get a compile time error. Let's see the above example converted to a sealed class:

// 1
sealed class Result<out T> {
    // 2
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val error: Exception) : Result<Nothing>()
}

fun <T> result(result: Result<T>) : Any? =
    // 3
    when (result) {
        is Result.Success<T> -> result.data
        is Result.Error -> result.error
    }
Enter fullscreen mode Exit fullscreen mode
  • 1 -> sealed classes are open by default (because they are implicitly abstract), we don't have to provide the open modifier
  • 2 -> we are able to subclass sealed classes just inside it or in the same file it is declared in
  • 3 -> when we use a sealed class inside a when expression and we cover all possible cases, we don't have to provide the else branch

NOTE: Under the hood, the sealed class has a private constructor which can be called only inside the class.

To list all the characteristics of sealed classes:

  • They are implicitly abstract
  • Subclasses must be declared inside itself or in the same file as it is in
  • They ease the pain of providing else branches when we cover all the instances
  • If we add another instance and we forget to cover that instance inside a when expression, we will get a compile time error

Constructors and Properties

pages 79, 80, 81, 82, 83, 84, 85, 86

Primary Constructors

pages 79, 80, 81

You can read about Kotlin classes and constructors at Kotlin Official site.

Kotlin, just like Java, allows us to declare more than one constructor. However, Kotlin makes distinction between a primary constructor and secondary ones.

class User(val name: String)
Enter fullscreen mode Exit fullscreen mode

The primary constructor is declared after the class name, outside the class body. We can use the constructor keyword, or omit that if we don't have annotations or visibility modifiers. The primary constructor roles are:

  1. specifying constructor parameters
  2. defining properties that are initialized by constructor parameters

If we want to write the equivalent Java way of the code above, it would look like this:

// 1
class User constructor(
    // 2
    _name: String
) {
    val name: String

    // 3
    init {
        name = _name
    }
}
Enter fullscreen mode Exit fullscreen mode

In the code snippet above we have two new keywords:

  • 1 -> constructor: begins the declaration of a constructor in Kotlin
  • 2 -> init: introduces an initializer block
  • 3 -> underscore: the underscore in constructor parameter is used to distinguish the parameter name from the property name. We could have used this.name = name instead.

What is an initializer block?
An initializer block contains code that's executed when the class is created and intended to be used together with primary constructors.
Why do we need initializer blocks? Because the primary constructors are concise in Kotlin and cannot contain logic, we need initializer blocks.

NOTE: We can have more than one initializer block in a class. Initializer blocks are executed in the same order as they appear in the code.

In the code snippet above, there can be two modifications:

  • we can omit the constructor keyword if there are not visibility modifiers or annotations on primary constructor
  • we can omit the initializer block and assign the property to to parameter directly.
class User(_name: String) {
    // 1
    val name = _name
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> This is called property initialization

Kotlin has an even more concise syntax for declaring properties and initializing them from the primary constructor. These properties can be declared as immutable (val) or mutable (var).

class User(val name: String)
Enter fullscreen mode Exit fullscreen mode

As with functions, we can declare constructors with default values in Kotlin:

class User(val name: String = "John Doe")
Enter fullscreen mode Exit fullscreen mode

NOTE: If all constructor parameters have default values, the compiler generates an additional constructor without parameters that uses the default values. It does so to make it easier to use Kotlin with libraries that instantiate classes without parameters.

If we extend another class, our primary constructor must initialize the super-class:

// 1
open class User(val name: String) {

}

// 2
class TwitterUser(username: String) : User(username) {

}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> mark the super-class open to be able to extend it
  • 2 -> TwitterUser must call its super-class constructor

Just like Java, if we don't provide any constructors, a no-arg constructor is generated for the class. And if we extend this class, we still need to call its constructor:

// 1
open class User

// 2
class InstagramUser : User() {

}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> We did not declare any constructor for the User, but the compiler generates a no-arg constructor for the class
  • 2 -> Although User does not have any declared constructor or parameters, we still need to call its generated constructor!

NOTE: Be aware of the difference between extending a class and implementing an interface in Kotlin. Because interfaces don't have any constructors, we don't put parentheses after their name when implementing them!

NOTE: To make sure that a class cannot be instantiated, we should make the primary constructor private:

class User private constructor() {

}
Enter fullscreen mode Exit fullscreen mode

Secondary Constructors

pages 81, 82, 83

Because Kotlin has default values for constructor parameters, secondary constructors are not used as widely as they are used in Java.
We declare secondary constructors (like primary constructors) with the constructor keyword.
Let's take a look at an example from the Android world:

open class View {
    constructor(ctx: Context) {

    }
    constructor(ctx: Context, attr: AttributeSet) {

    }
}

class TextView : View {
    // 1
    constructor(ctx: Context) : super(ctx) {

    }

    constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {

    }
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> if we extend a super-class and declare secondary constructors, we need to call the super class constructor in every constructor that we declare by using the super keyword (the exception is when we call another constructor of our class)

Just as in Java, we can call another constructor of our class using the this keyword:

class TextView : View {
    val myStyle =     // some code to instantiate myStyle 

    constructor(ctx: Context, attr: AttributeSet) : super(ctx, attr) {

    }

    // 1
    constructor(ctx: Context) : this(ctx, myStyle) {

    }
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> we use this keyword to call another constructor of our class

Properties in Interfaces

pages 83, 84

Kotlin interfaces can contain abstract properties.

interface User {
    // 1
    val name: String
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> The interface does not have a state because of this property because it does not specify whether it is a field or just a getter. It just means that classes that implement the interface need to provide a way to obtain the value of name. Pay attention that all classes are overriding the name property.
// Primary constructor property
class PrivateUser(override val name: String) : User

// Custom getter -> it does not have a backing field
class SubscribingUser(val email: String) : User {

    override val name: String
        get() = email.substringBefore('@')
}

// Property initializer
class FacebookUser(val accountId: Int) : User {
    override val name = getFacebookName(accountId)

    private fun getFacebookName(accountId: Int): String {
        //code to get the facebook name
    }
}
Enter fullscreen mode Exit fullscreen mode

Accessing Backing Fields

page 85

Let's see how we can access a backing field from the accessors. To do so, we use the field identifier in the body of a setter.

class User7(val name: String) {
    var address: String = "unspecified"
        // 1
        set(value) {
            // 2
            println(
                """
            address changed for $name: 
            $field -> $value
            """.trimIndent()
            )
            // 3
            field = value
        }
    get() {
        // 2
        return "$name address: $field"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> We define a setter by writing a set function under the property (the property must be mutable (var))
  • 2 -> We can access the property backing field by the field keyword.
  • 3 -> We change the value of the property

NOTE: If we provide custom accessors for a property and do not use the field keyword, the compiler won't generate a backing field for that property (for getter in val property and for both accessors in var property).

class User(val name: String) {
    // 1
    var address: String = "unspecified"
        set(value) {
            println(
                "address for $name: $value"
            )

        }
    get() {
        return "$name address: "
    }
}
Enter fullscreen mode Exit fullscreen mode

If we write such a code, it won't compile with the error Initializer is not allowed here because this property has no backing field. Why? Because we did not use the field keyword in neither the setter nor the getter.

Changing Accessor Visibility

page 86

The accessor's visibility is the same as the property's visibility. We can change the accessor's visibility by putting a visibility modifier before the get or set keyword.

class User(val name: String) {
    // 1
    var id: Int = 0
        // 2
        private set(value) {
            field = getAutoGeneratedId()
        }


    private fun getAutoGeneratedId(): Int {
        // code to get an auto generated id
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 1 -> the property's visibility is public and can be accessed outside the class
  • 2 -> we limit the setter's visibility to the class because we do not want other classes to set this property

NOTE: Accessors cannot be more visible than the property itself. Meaning that if the property is protected, the accessors can be private but cannot be public!

Universal Object Methods

pages 87, 88, 89

We use some classes specifically to hold data. These classes usually need to override some methods like toString(), equals(), and hashCode(). Let's see how we should implement them in Kotlin.
First, we declare a Person class.

class Person(
    val name: String,
    val phoneNumber: String,
    val address: String
)
Enter fullscreen mode Exit fullscreen mode

toString()

toString() method is used primarily for debugging and logging. It should be overridden in almost every class to make the debugging easier. If we do not override the toString() method, when we print an object, the result looks like Person@23fc625e

class Person(
    val name: String,
    val phoneNumber: String,
    val address: String
) {
    override fun toString(): String {
        return "Person(name=$name, phone number=$phoneNumber, address=$address"
    }
}
Enter fullscreen mode Exit fullscreen mode

equals()

equals() method is used to check whether two objects' values are equal, not the references!

class Person(
    val name: String,
    val phoneNumber: String,
    val address: String
) {
    override fun toString(): String {
        return "Person(name=$name, phone number=$phoneNumber, address=$address"
    }

    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Person)
            return false
        return name == other.name &&
                phoneNumber == other.phoneNumber &&
                address == other.address
    }
}
Enter fullscreen mode Exit fullscreen mode

hashCode()

The hashCode() method should be always overridden together with the equals() method! If two objects are equal, they must have the same hash code.

class Person(
    val name: String,
    val phoneNumber: String,
    val address: String
) {
    override fun toString(): String {
        return "Person(name=$name, phone number=$phoneNumber, address=$address"
    }

    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Person) 
            return false
        return name == other.name &&
                phoneNumber == other.phoneNumber &&
                address == other.address
    }

    override fun hashCode(): Int {
        return name.hashCode() + 
                phoneNumber.hashCode() + 
                address.hashCode()
    }

}
Enter fullscreen mode Exit fullscreen mode

NOTE: Why the hashCode() method should be always overridden together with the equals() method? Because some clients (like HashSet) first use the hashCode() method to check for equality and if the hash codes are equal, then they check the equals() method!

Data Classes

pages 89, 90, 91

Kotlin has made it easier for us to declare classes to hold data. We just mark that class with the data modifier and the Kotlin compiler would generate the previously mentioned methods for us! The compiler will generate toString(), equals(), hashCode(), (also copy(), and componentN()) functions from the properties declared in the primary constructor.

data class Person(
    val name: String,
    val phoneNumber: String,
    val address: String
)
Enter fullscreen mode Exit fullscreen mode

The Person class above is somehow equal to the Person class we declared before this (with toString(), equals(), hashCode() methods overridden).

Data classes must fulfill the following requirements:

  1. The primary constructor needs to have at least one parameter.
  2. All primary constructor parameters need to be marked as val or var.
  3. Data classes cannot be abstract, open, sealed, or inner.

NOTE: As we mentioned earlier, the compiler just takes into account the properties in the primary constructor. In other words, we can declare properties in the class body to exclude it from generated implementations(like toString() and hashCode()).

copy() Method

It's strongly recommended that we use read-only (val) properties to make the instances of data classes immutable. This is mandatory if we want to use such instances as keys in a HashMap or a similar container, because otherwise the container could get into an invalid state if the object used as a key is modified after it was added to the container.


Class Delegation

Sometimes we need to change the behavior of a class, but we cannot extend that class (either our class has another parent class, or the class we want to extend is not open). In this situation, we can use the decorator pattern:

  • We implement the same interface that the class we want to extend is implementing
  • We declare a property of type of the class we want to extend
  • We forward the requests that we don't want to change to that property
  • If we want to change the behavior of a method, we will override that method in our class Let's see this pattern in practice:
class DelegatingCollection<T> : Collection<T> {

    private val innerList = arrayListOf<T>()

    override val size: Int get() = innerList.size

    override fun isEmpty() = innerList.isEmpty()

    override fun iterator() = innerList.iterator()

    override fun containsAll(elements: Collection<T>) = innerList.containsAll(elements)

    override fun contains(element: T) = innerList.contains(element)

}
Enter fullscreen mode Exit fullscreen mode

Here, we have used the decorator pattern as we use it in Java. We forward every call to the innerList variable of type ArrayList. If we want to change the behavior of the method isEmpty(), we can easily do so be defining our own implementation.

Fortunately, Kotlin has provided a first-class support for delegation. To use this feature:

  • We extend the same interface as our class we want to extend
  • We declare a property of the class we want to extend
  • We use the by keyword after the interface implementation.
class DelegatingClass<T>(
    val innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList 
Enter fullscreen mode Exit fullscreen mode

This is just the same as the implementation before with much less boilerplate code! We have used the by keyword to delegate the requests to the object specified(innerList here)!

The object Keyword

pages 93, 94, 95, 96, 97, 98, 99, 100, 101

The object keyword in Kotlin is used to define a class and create an instance of that class at the same time.

Object Declaration

pages 93, 94, 95

Singleton design pattern is used fairly common in object-oriented design systems. Kotlin provides object declaration which combines the class declaration and a declaration of a single instance of that class.

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}
Enter fullscreen mode Exit fullscreen mode

Object declarations are created with object keyword. An object declaration defines a class and a variable of that class in a single statement.

NOTE: If we pay attention to its name (object declaration), we understand its purpose. We declare an object, and to declare an object, we must create an instance of that object. With object declarations, we combine the two steps.

An object declaration is just like a normal class with one difference: it does not allow constructors to be declared in it. Objects of object declarations are created immediately at the point of definition, not by calling their constructors.

NOTE: Object declaration, like a variable declaration, is not an expression and cannot be used on the right-hand side of an assignment statement!

Object declarations allow us to call their methods and access their properties by using the class name and the . character:

val numberOfDataProviders = DataProviderManager.allDataProviders.size

DataProviderManager.registerDataProvider(/*...*/)
Enter fullscreen mode Exit fullscreen mode

Object declarations can inherit from classes and interfaces. This is often useful when the framework requires us to implement an interface, but our implementation does not contain any state! The Comparator interface is a great example for this. A Comparator implementation receives two objects and returns an integer indicating which of the two objects is greater! Comparators never store any data, so we usually need a single Comparator instance for a particular way of comparing objects. This is a perfect use for object declaration!

data class Person(val name: String, val age: Int)

object AgeComparator : Comparator<Person> {
    override fun compare(p1: Person, p2: Person): Int {
        return p1.age - p2.age
    }
}
Enter fullscreen mode Exit fullscreen mode

We can use singleton objects in any context where an ordinary object can be used.

We can also declare objects in a class. Such objects also have just a single instance; they don't have a separate instance per instance of the containing class. Take the Person class as an example. Isn't it more logical to put different Comparator implementations inside the class itself for further use?

data class Person(val name: String, val age: Int) {

    object AgeComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int {
            return p1.age - p2.age
        }
    }

    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int {
            return p1.name.compareTo(p2.name)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To refer to the instance of the singleton object in Java which is created by the object declaration, we should use the INSTANCE static field:

AgeComparator.INSTANCE.compare(person1, person2);
Enter fullscreen mode Exit fullscreen mode

Companion Objects

pages 96, 97, 98, 99, 100

Kotlin does not have the static keyword which exists in Java. Most of the time, our needs for static members and methods is satisfied with top-level member and functions in Kotlin. However, we cannot access private members of the class. So, if we need to write a function that can be called without an instance of the class, but has access to the private members of the class, we can use the object declaration inside that class.

NOTE: Please be aware that companion object (just like static methods) cannot access instance members of the class. To do so, we must provide an instance of the class for the companion object:

class Person {

    private lateinit var name: String

    object Print {
        fun printName1(person: Person) {
            println(person.name)
        }

        fun printName2() {
            val person = Person()
            println(person.name)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If we mark the object declaration with the keyword companion, we can access the methods and properties inside that object declaration directly through the name of the containing class (without specifying the object name).

For example, to call the methods in the above example, we use this syntax:

Person.Print.printName1(Person())
Person.Print.printName2()
Enter fullscreen mode Exit fullscreen mode

Now, if we omit Print (the object declaration name) and put the companion keyword before object, we can use the methods like static methods in Java:

class Person {

    private lateinit var name: String

    companion object {
        fun printName1(person: Person) {
            println(person.name)
        }

        fun printName2() {
            val person = Person()
            println(person.name)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To use the methods above, we use this syntax:

Person.printName1(personInstance)
Person.printName2()
Enter fullscreen mode Exit fullscreen mode

Note that we do not use any object name (Print in previous example) to call the methods inside companion object.

One of the most important use-cases of companion object is to implement the factory pattern. Why? Because companion objects have access to the private members of the class, including the constructors.

class Person private constructor(val name: String, val role: Role){

    companion object {
        fun newManager(name: String) = 
            Person(name, Role.MANAGER)

        fun newEmployee(name: String) =
            Person(name, Role.EMPLOYEE)
    }

    enum class Role {
        MANAGER, EMPLOYEE
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we have used factory methods to create instances of the Person object. To create instances of Person, we use the class name with the method from the companion object:

Person.newManager("Tom")
Enter fullscreen mode Exit fullscreen mode

Factory methods are useful for several reasons:

  • They can be named according to their purpose
  • Can return subclasses depending on the method that we invoke
  • We can also avoid creating new objects when it's not necessary.

The downside with using factory methods instead of constructors is that we cannot override them in subclasses!

Companion Objects Extension

We can define an extension function (just like normal extension functions) for companion objects. The only requirement is that the class should have at least one companion object. If it does not have any, we should define an empty one to extend it.
Suppose we want to add an extension function (say setupFakeManager()) to the Person class we declared earlier. Here is a sample code:

fun Person.Companion.setupFakeManager() : Person {
    return Person.newManager("FakeManager")
}
Enter fullscreen mode Exit fullscreen mode

Because we had a companion object in the Person class, we just extended the companion object to have another function. Note the Companion keyword after the class name. By using the Companion keyword, we are indicating that we are extending the companion object!

Object Expressions

The third use-case of the object keyword is to declare anonymous objects. Anonymous objects are here to replace the Java's anonymous inner classes. For instance, we can use them to setup click listeners in Android:

button.setOnClickListener(object : View.OnClickListener {
    override fun onClick(view: View?) {
        // Do some work here
    }

})
Enter fullscreen mode Exit fullscreen mode

As we see, the syntax is the same as the object declaration but we have dropped the object name!

NOTES: Object expressions in Kotlin:

  • can implement multiple interfaces or no interfaces(Java can just extend one class or implement one interface).
  • can access the variables in the function where they are created.
  • are useful when we want to override multiple methods in our anonymous object. Otherwise, we are better to use lambda expressions.
  • if we need the instance of it, we can assign the expression to a variable.

This is the end of part 3 which was the summary of chapter 4 (a long one:-) ). There were other parts in chapter 4 that I did not cover here because I felt they are not as important and they would have made the article too long. Anyhow, if you found this article useful, please like it and share it with other fellow developers. If you have any questions or suggestions, please feel free to comment. Thanks for your time.

Top comments (0)

🌚 Friends don't let friends browse without dark mode.

Sorry, it's true.