1. Overview

When we work with Kotlin, we often need to combine or concatenate two lists into a single, unified list.

In this tutorial, we’ll take a closer look at this fundamental operation and explore different ways to do it.

2. Introduction to the Problem

Merging two lists is a routine task in our everyday programming. Let’s consider the scenario where we want to join list1 and list2. Depending on our needs, there are two possible outcomes:

  • In-Place Join –  list1 includes all the elements from both list1 and list2. We don’t intend to create a new list, and list1 is modified to achieve this.
  • Result in a new List – A new list3 contains elements from list1 and list2. Also, list1 and list2 remain unmodified.

Further, Kotlin has MutableList and List (read-only) two list types.

In this tutorial, we’ll look at all these scenarios and discuss combining two lists.

3. When the First List Is a MutableList

First, let’s look at the MutableList case. Let’s say list1 is a MutableList:

val list1 = mutableListOf("A", "B", "C")
val list2 = listOf("D", "E")

*The addAll() function allows us to append all of list2‘s elements to list1*:

list1.addAll(list2)
assertEquals(listOf("A", "B", "C", "D", "E"), list1)

Alternatively, we can use the “plus assign” operator (+=) to achieve it:

val list1 = mutableListOf("A", "B", "C")
val list2 = listOf("D", "E")

list1 += list2
assertEquals(listOf("A", "B", "C", "D", "E"), list1)

Under the hood, the ‘*+=*‘ operator overloading calls addAll() internally:

public inline operator fun <T> MutableCollection<in T>.plusAssign(elements: Iterable<T>) {
    this.addAll(elements)
}

Like ‘*+=“, the ‘+*‘ operator also allows us to combine two lists. The difference is that ‘*+*‘ joins two lists into a new List object:

val list1 = mutableListOf("A", "B", "C")
val list2 = listOf("D", "E")

val result = list1 + list2
assertEquals(listOf("A", "B", "C", "D", "E"), result)
assertEquals(listOf("A", "B", "C"), list1)

As shown in the code, result contains expected elements, and list1 remains unchanged. As we’ll be using the ‘*+*‘ operator later on, it’s worth understanding why it joins two lists into a new List object:

public operator fun <T> Collection<T>.plus(elements: Iterable<T>): List<T> {
    if (elements is Collection) {
        val result = ArrayList<T>(this.size + elements.size)
        result.addAll(this)
        result.addAll(elements)
        return result
    } else {
        val result = ArrayList<T>(this)
        result.addAll(elements)
        return result
    }
}

The implementation is straightforward. When we do list1 + list2, an ArrayList is created to hold elements from both input lists. Therefore, the result we get is the new ArrayList object.

It’s also worth mentioning that the new list object returned from ‘*+*‘ is always read-only, although ArrayList and list1 are mutable.

4. When the First List Is a Read-Only List

Next, let’s move to the read-only List case:

val list1 = listOf("I", "II", "III")
val list2 = listOf("IV", "V")

Now, both list1 and list2 are read-only lists. We’ve seen the ‘*+*‘ operator can join a mutable list and a read-only list. It works for read-only lists, too:

val result = list1 + list2
assertEquals(listOf("I", "II", "III", "IV", "V"), result)
assertEquals(listOf("I", "II", "III"), list1)

As the test shows, we got a new list object containing desired elements.

It isn’t difficult to understand that we cannot perform in-place joining since list1 is read-only. But let’s see what happens if we do ‘list1 += list2‘:

//plusAssign requires 'var'
var list1 = listOf("I", "II", "III")
val list2 = listOf("IV", "V")
val list1Origin = list1

list1 += list2
assertEquals(listOf("I", "II", "III", "IV", "V"), list1)
assertNotSame(list1Origin, list1)

When we run the test, it passes. What a surprise! How can list2‘s elements be appended to read-only list1? Next, let’s figure out what’s happened.

Since list1 is read-only,  ‘list1 += list2‘ here doesn’t call MutableCollection.plusAssign(). Instead, it performs list1 = list1 + list2. It first joins list1 and list2 into a new list object and reassigns the result to list1. Therefore, we declared list1 with var instead of val. This also explains why list1 and list1Origin are different objects in the test above.

5. Conclusion

In this article, we’ve explored different methods for joining two lists in Kotlin. Also, we’ve addressed various practical scenarios where these methods come in handy.

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