shinvu
shinvu

Reputation: 851

Why does Kotlin have Mutable versions of collections?

I have a general question about Kotlin collections.

Why are there mutable versions of so many collections (like the MutableList) when we have the val vs var distinction?

Well....ok...actually, I understand that val doesn't have anything to do with the 'mutability' of the object, but rather the 're-initializability' of the object.

But then that raises the question....why isn't MutableList the default?

Upvotes: 2

Views: 862

Answers (2)

shinvu
shinvu

Reputation: 851

TL;DR

Individually, mutable and immutable collections are able to expose useful features that can't co-exist in a single interface:

  1. Mutable collections can be read from and written to. But Kotlin strives to avoid all runtime failures, therefore, these mutable collections are invariant.
  2. Immutable collections are covariant, but they're...well...immutable. Still, Kotlin does provide mechanisms for doing useful things with these immutable collections (like filtering values or creating new immutable collections from existing ones). You can go through the long list of convenience functions for Kotlin's (immutable) List interface for examples.

Immutable collections in Kotlin cannot have elements added or removed from them; they can only be read from. But this apparent restriction makes it possible to do some subtyping with the immutable collections. From the Kotlin docs:

The read-only collection types are covariant...the collection types have the same subtyping relationship as the element types.

This means that, if a Rectangle class is a child of a Shape class, you can place a List<Rectangle> object in a List<Shape> variable whenever required:

fun stackShapes(val shapesList: List<Shape>) {
    ...
}

val rectangleList = listOf<Rectangle>(...)

// This is valid!
stackShapes(rectangleList)

Mutable collections, on the other hand, can be read from and written to. Because of this, no sub-typing or super-typing is possible with them. From the Kotlin docs:

...mutable collections aren't covariant; otherwise, this would lead to runtime failures. If MutableList<Rectangle> was a subtype of MutableList<Shape>, you could insert other Shape inheritors (for example, Circle) into it, thus violating its Rectangle type argument.

val rectangleList = mutableListOf<Rectangle>(...);
val shapesList: MutableList<Shape> = rectangleList // MutableList<Rectangle>-type object in MutableList<Shape>-type variable

val circle = Circle(...)
val shape: Shape = circle // Circle-type object in Shape-type variable

// Runtime Error!
shapesList.add(shape) // You're actually trying to add a Circle to a MutableList<Rectangle>
// If rectanglesList couldn't be put into a variable with type MutableList<Shape> in the first place, you would never have run into this problem.

At this point, you might be thinking: "So what? Kotlin could just add type-checks to all of the write-methods of Mutable Collections...then you could allow them to be covariant, and you wouldn't need separate immutable collections!"

Which is true, except that it would go completely against a core Kotlin philosophy; to avoid nulls and runtime errors whenever possible. You see, the methods of such a Collection would have to return null - or raise an Exception - whenever a type-check fails. This would only become apparent at runtime, and since that can be avoided by simply making Mutable Collections invariant...that's exactly what Kotlin does.

Upvotes: 1

Carlos
Carlos

Reputation: 6021

From the Kotlin docs:

The read-only collection types are covariant. This means that, if a Rectangle class inherits from Shape, you can use a List<Rectangle> anywhere the List<Shape> is required. In other words, the collection types have the same subtyping relationship as the element types. Maps are covariant on the value type, but not on the key type.

In turn, mutable collections aren't covariant; otherwise, this would lead to runtime failures. If MutableList<Rectangle> was a subtype of MutableList<Shape>, you could insert other Shape inheritors (for example, Circle) into it, thus violating its Rectangle type argument.

To paraphrase, if it's immutable you know all the types are the same. If not, you might have different inheritors.

Upvotes: 0

Related Questions