1. Overview

In this tutorial, we’re going to compare Array<T> and List<T> in Kotlin. To do that, we’ll categorize their differences into various groups. Then we’ll explore each group in more detail.

2. Array<T> vs. List<T>

2.1. Data Structure

From a data structure point of view, the Kotlin compiler compiles the Array<T> type as a JVM array. Therefore, arrays in Kotlin are also a fixed-sized sequence of elements of the same type. This is because the number of elements is always determined upon array creation and can’t be changed.

For instance, let’s consider a simple example:

val cities = arrayOf("Tehran", "Sari", "Neka")

The Kotlin compiler (kotlinc) compiles this into bytecode that, when viewed using javap, looks like:

0: iconst_3
1: anewarray     #8     // class java/lang/String

The anewarray opcode creates an array of reference types. Also, the iconst_3 pushes integer 3 to the operand stack. Later on, the anewarray instruction will use this integer as the array length.

On the other hand, the List<T> and MutableList<T> types are interfaces with different implementations. For instance, the ArrayList<T> implementation is a sequence of elements of type T that can grow over time. Since this implementation is using an array under the hood, most runtime characteristics (for example, the time complexity of indexing) are similar to those of arrays.

In addition to ArrayList<T>, the LinkedList<T> implementation uses a series of linked elements to achieve the same contract with different runtime characteristics. For instance, adding an element is always O(1) while the indexing may be as bad as O(n).

As opposed to arrays in Kotlin, the memory representation of lists in Kotlin can vary, as they’re completely up to the concrete implementations.

2.2. Mutability

Arrays in Kotlin are always mutable, as we can replace the value of each segment with something new:

cities[0] = "Berlin"

But we can’t prepend or append new values to arrays as they’re fixed-sized.

On the other hand, the List<T> interface only provides immutable operations. So we can neither replace existing values nor add new ones.

If we want mutability, we should use MutableList<T> in Kotlin. With MutableList<T>, it’s possible to mutate the list in every possible way:

val colors = mutableListOf("Blue") // [Blue]
colors[0] = "Green" // replace: [Green]
colors.add(0, "Red") // prepend: [Red, Green]
colors.add("Blue") // append: [Red, Green, Blue]

As opposed to Array<T> and List<T>, the MutableList<T> can grow or shrink in size, as it provides add() and remove() methods.

2.3. Generic Variance

The concept of variance in terms of generics determines how generic types with the same base type and different type parameters relate to each other.

For instance, we all know that String is a subtype of Any in Kotlin. Now given that, what’s the relationship between List<String> and List<Any>? In Kotlin, List<T> is defined as:

public interface List<out T> : Collection<T> {
    override fun iterator(): Iterator<T>
    public operator fun get(index: Int): T

    // omitted
}

Since the type argument T is always used in the out positions, the List<T> is covariant. That is, the List<T> preserves subtyping relationships, as the List<String> is a subtype of List<Any>:

val colors = listOf("Red")
val colorsAsAny: List<Any> = colors // this works

Because of the covariance, here we’re assigning a value of List<String> to a variable of type List<Any>.

On the other hand, the same thing is not true for MutableList<T>, as the type parameter is used in both in and out positions:

public interface MutableList<T> : List<T>, MutableCollection<T> {
    override fun add(element: T): Boolean
    override fun remove(element: T): Boolean
    // omitted
}

So the MutableList<T> is invariant. Consequently, MutableList<String> has no relationship with MutableList<Any>:

val colors = mutableListOf("Red")
val colorsAsAny: MutableList<Any> = colors

Because of the invariance, the above snippet won’t even compile. Quite similar to MutableList<T>, the Array<T> is also invariant:

val colors: Array<Any> = arrayOf<String>("Red")

The variance of these types can be confusing, especially if we’re coming from a Java background. In Java, generic types are invariant and arrays are covariant!

2.4. Specialized Primitives

In Kotlin, we have special functions to create arrays of primitive types:

val bytes = byteArrayOf(42)
val shorts = shortArrayOf(42)
val ints = intArrayOf(42)
val longs = longArrayOf(42)
val floats = floatArrayOf(42.0f)
val doubles = doubleArrayOf(42.0)
val chars = charArrayOf('a')
val booleans = booleanArrayOf(true)

This is mostly because the JVM has special opcodes to create and manipulate such arrays:

0: iconst_1
1: newarray  byte

Here, the bytecode uses the newarray opcode (as opposed to anewarray) to create an array of bytes. The bottom line is, arrays have optimized representations for primitive data types, making them more suitable for some performance-sensitive cases.

On the other hand, there is no optimized version for primitives in lists in the Kotlin standard library.

2.5. Other Subtle Differences

To have good interoperability with Java, Kotlin treats some Java types specifically. Such types are not loaded from Java “as is”, but are mapped to corresponding Kotlin types. More specifically, List<T> and MutableList<T> are among those mapped types. Similarly, the Array<T> is also a mapped type but the rules are different for it.

In addition to all these, there are some even more subtle differences between the two. For instance, the “==” operator is using reference equality for arrays and content equality for lists:

println(intArrayOf(1) == intArrayOf(1)) // false
println(listOf(1) == listOf(1)) // true

3. Conclusion

In this article, we enumerated the differences between arrays and lists in Kotlin. To sum up, arrays are fixed-sized sequences of elements, they are mutable and invariant in terms of generics, and they come with more optimized primitive versions. Moreover, they’re taking advantage of JVM special treatments for arrays.

On the other hand, List<T> and MutableList<T> are interfaces with dozens of concrete implementations. They are more flexible in terms of representations and can possibly grow or shrink in size when they’re mutable.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.