1. Overview

In this tutorial, we’ll learn how to extend a data class. First, we’ll show if it’s possible from another data class. Next, we’ll use an interface to extend a data class. Finally, we’ll show an inheritance from a non-data class.

2. Extend Data Class From Another Data Class

Let’s first have a look if it’s possible to extend a data class from another data class. Let’s create a Vehicle class:

data class Vehicle(val age: Int, val numberOfWheels: Int)

It describes a vehicle with the age and number of wheels. Now, let’s try to create a Car class that extends the Vehicle class. The solution similar to the one from Java would look like this:

data class Car(val numberOfDoors: Int) : Vehicle()

We want to extend the Vehicle with additional property numberOfDoors. But, it causes two compilation errors:

  • The function ‘component1’ generated for the data class conflicts with the member of supertype ‘Vehicle
  • This type is final, so it cannot be inherited from Vehicle

In other words, we must provide parameters for the Vehicle constructor. First, we must set the parameters in the Vehicle class as open:

data class Vehicle(open val age: Int, open val numberOfWheels: Int)

Then, we modify the Car class to provide values for the inherited constructor:

data class Car(override val age: Int, override val numberOfWheels: Int, val numberOfDoors: Int) : Vehicle(age, numberOfWheels)

Unfortunately, this code doesn’t compile either. The compiler returns the same exceptions as the first compilation. In short, data classes do not work well with inheritance. It’s partially caused by the nature of data classes, where methods like equals() and hashCode() are generated. Moreover, a data class has few restrictions. One of them is it can’t have a modifier open, and because of that, it can have only the default one, final.

3. Use Interface With Properties

Let’s now look at using an interface instead of the data class to provide the inheritance. For that purpose, we’ll create an IVehicle interface:

interface IVehicle {
    val age: Int
    val numberOfWheels: Int
}

The interface contains the same properties as fields in the Vehicle class. Now, we’ll use it in the Car class:

data class Car(override val age: Int, override val numberOfWheels: Int, val numberOfDoors: Int) : IVehicle

Unfortunately, fields from the interface must be declared in the data class as well. The interface usage may provide consistency between multiple implementations.

Now, let’s prove that the equals() method works as expected:

class CarUnitTest {

    @Test
    fun `given a Car object when compare with equals should get true`() {
        val fordFocus = Car(age = 2, numberOfWheels = 4, numberOfDoors = 4)
        val fordFocusClone = Car(age = 2, numberOfWheels = 4, numberOfDoors = 4)
        assertThat(fordFocus).isEqualTo(fordFocusClone)
    }
}

4. Inherit Data Class From Non-data Class

After that, let’s have a look at how to extend a non-data class by a data class. For that purpose, we’ll create a base class:

open class VehicleBase(open val age: Int, open val numberOfWheels: Int)

The base class has an open modifier, as classes in Kotlin are final by default. Additionally, both fields are open, as we’ll overwrite them in our data class.

Let’s now create the data class:

data class CarExtendingVehicle(override val age: Int, override val numberOfWheels: Int, val numberOfDoors: Int) : VehicleBase(age, numberOfWheels)

Firstly, we repeated fields from the base class. It is necessary because all are provided in the constructor. Moreover, the age and numberOfWheels fields have override modifiers. All properties from the superclass must be prefaced with the override modifier.

Now, let’s test the constructor and the equals method:

internal class CarExtendingVehicleUnitTest {

    @Test
    fun `given a carExtendingVehicle object when compare with equals should get true`() {
        val fordMustang = CarExtendingVehicle(age = 10, numberOfWheels = 2, numberOfDoors = 4)
        val fordMustangClone = CarExtendingVehicle(age = 10, numberOfWheels = 2, numberOfDoors = 4)
        assertThat(fordMustang).isEqualTo(fordMustangClone)
    }
}

The test shows that the constructor and the generated method work as expected. Additionally, the usage looks the same as in the previous example where we used the interface.

5. Conclusion

In this short article, we showed that it is not possible to extend a data class with another data class. Moreover, we showed that we could achieve inheritance with an interface or a non-data class.

As always, the source code of the examples is available over on GitHub.