LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 28: Constraints and inheritance tax

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Constraints and inheritance tax

In Kotlin, IntArray (equivalent to Java's int[]) is often more efficient than Array<Int> or List<Int> because it does not require boxing. However, IntArray is mutable and does not allow the use of List utilities directly. Therefore, a developer created a class called ImmutableIntList as shown below. This class is a wrapper for IntArray with a List interface, ensuring immutability. (Note: Kotlin's List differs from Java's List as it does not have methods like set or add.)

class ImmutableIntList(vararg values: Int): List<Int> {
    // It's safe to assign `values` because `vararg` does not keep the reference to the `*array` argument.
    private val valueArray : IntArray = values

    override operator fun get(index: Int): Int = valueArray[index]

    override val size: Int = valueArray.size

    ... snip
}

Now, let's say another developer wants to create various versions of ImmutableIntList, such as ImmutableSortedIntList that guarantees sorted elements. The following implementation makes ImmutableIntList inheritable (open class) and defines ImmutableSortedIntList as its subclass.

open class ImmutableIntList(vararg values: Int): List<Int> {
    
    protected val valueArray : IntArray = values

    override operator fun get(index: Int): Int = valueArray[index]

    ... snip
}

class ImmutableSortedIntList(vararg values: Int) :
    ImmutableIntList(*values.sorted().toIntArray())

class AnotherImmutableIntList(...): ImmutableIntList() {
    ... // may touch `valueArray` here.
}

Is there any problem with this structure?

Inheritance tax on immutability

If you want to ensure immutability, it is better to make the class non-inheritable. Even if a class is named Immutable, if it is inheritable, subclasses can make it mutable.

Especially since ImmutableIntList has a generic and widely used name, there is a possibility of "incorrect inheritance" in unexpected packages or modules. For example, a subclass that breaks immutability might be created somewhere unknown, as shown below.

class MutableIntList(...): ImmutableIntList(...) {
    operator fun set(index: Int, value: Int) {
        valueArray[index] = value
    }

    ...
}

val mutableIntList = MutableIntList(...)

// Although this is an `ImmutableIntList`, it's actually mutable
// because it's modifiable by accessing `mutableIntList`.
val immutableIntList: ImmutableIntList = mutableIntList

Even if you try to prevent this by making valueArray private, it is insufficient to maintain immutability. The get function is still overridable, allowing implementations that ignore valueArray.

class IntListWithMutableSuffix(...): ImmutableIntList(...) {

    var suffix: Int = 0

    override operator fun get(index: Int): Int =
        if (index != super.size) super[get] else suffix

    ...
}

Not limited to immutability, when making a basic or generic class inheritable, you need to carefully consider which members can be overridden. Also, consider defining independent types without inheritance relationships instead of making them inheritable.

Immutability and inheritance relationships

In general, classes of mutable objects and immutable objects should not have inheritance relationships with each other.

  • If mutable inherits from immutable: As mentioned earlier, the immutability constraint is broken.
  • If immutable inherits from mutable: Methods that modify (like add/set) need to be callable from the immutable object's class, leading to inevitable runtime errors (e.g., Guava's ImmutableList).

If a common parent class for mutable and immutable is needed, it is better to create it as "read-only (unmodifiable)". (Kotlin's List has such an inheritance structure.)

  • If mutable inherits from read-only: No problem. The "methods" of the parent class are a subset of the child class's methods. (Only the child class has set/add, and there are no methods valid only for the parent class)
  • If immutable inherits from read-only: No problem. The "constraints" of the parent class are a subset of the child class's constraints. (Only the child class has immutability, and there are no constraints only for the parent class)

In a nutshell

If you want to ensure immutability, it is better to make the class non-inheritable.

Keywords: immutability, inheritance, override

List of articles on techniques for improving code quality