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'sImmutableList
).
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