1. Overview
List is a pretty common collection type in Kotlin.
In this tutorial, we’ll learn how to compare two lists in Kotlin.
2. Introduction to the Problem
A List is an ordered collection of elements. Further, a List can contain duplicate values.
Therefore, when we’re talking about comparing two lists, depending on the requirement, there are a couple of scenarios in which we consider two lists are equal:
- Two lists have the same size, contain the same elements, and are in the same order.
- Two lists have the same size and contain the same elements. However, we don’t care about the order of the elements.
An example may explain it quickly. Let’s say we have a target list:
private val targetList = listOf("one", "two", "three", "four", "five")
As the code above shows, we have five elements in targetList.
Then, let’s create some more lists of different cases:
private val listExactlySame = listOf("one", "two", "three", "four", "five")
private val listInDiffSizeButWithSameElements = listOf("one", "two", "three", "four", "five", "five", "five")
private val listInDiffSizeAndElements = listOf("one", "two", "three", "four", "five", "five", "five", "I am a new element")
private val listInDiffOrder = listOf("two", "one", "three", "five", "four")
private val listInDiffElement = listOf("ONE", "two", "three", "four", "FIVE")
Next, let’s build methods to cover the two comparison scenarios we mentioned above to determine if these lists are the “same” as our targetList.
For simplicity, we’ll use unit test assertions to verify whether our comparison methods work as expected.
3. Comparing Elements and the Order
*In Kotlin, we use structural equality (==) to evaluate if both values are the same or equal*. This is the same as the equals() method in Java.
Therefore, if list1 == list2 is true, both lists have the same size and contain the same elements in exactly the same order.
Next, let’s test it with our lists:
targetList.let {
assertThat(listExactlySame == it).isTrue
assertThat(listInDiffOrder == it).isFalse
assertThat(listInDiffSizeButWithSameElements == it).isFalse
assertThat(listInDiffSizeAndElements == it).isFalse
assertThat(listInDiffElement == it).isFalse
}
As the code above shows, if we compare two lists using structural equality, only listExactlySame and targetList should be equal. If we run the test, it passes.
So, == is the most straightforward way to compare two lists to see if they are equal.
4. Compare Elements Ignoring the Order
When we’re facing the “ignoring order” requirement, some of us may come up with this idea: list1.size() == list2.size() && list1.containsAll(list2) &&list2.containsAll(list1).
Indeed, this solves the problem. However, List‘s containsAll is an expensive method. As List‘s look-up costs O(N). Thus, given that both lists have the same size, list1.containsAll(list2)’s cost is O(N^2).
To get better performance, we can use Set to solve the problem. The lookup function on HashSet costs only O(1). Therefore, we can solve the problem by first converting both lists to HashSets and then checking the structural equality of the two sets. The total cost will be O(N).
Now that we understand what to do, the implementation won’t be a challenge at all:
fun <T> equalsIgnoreOrder(list1:List<T>, list2:List<T>) = list1.size == list2.size && list1.toSet() == list2.toSet()
It’s worth mentioning that Kotlin’s List.toSet() function returns a LinkedHashSet object, which is a sub-type of HashSet.
The implementation is pretty straightforward. However, to make the function call fluent, we can create an extension function on List:
fun <T> List<T>.equalsIgnoreOrder(other: List<T>) = this.size == other.size && this.toSet() == other.toSet()
In this way, the function call will look like list1.equalsIgnoreOrder(list2).
Moreover, as the extension function has only one parameter, we can add the infix notation to make the function call more easy-to-read:
infix fun <T> List<T>.equalsIgnoreOrder(other: List<T>) = this.size == other.size && this.toSet() == other.toSet()
if (list1 equalsIgnoreOrder list2) ....
Now, let’s test it using our example lists:
targetList.let {
assertThat(listExactlySame equalsIgnoreOrder it).isTrue
assertThat(listInDiffOrder equalsIgnoreOrder it).isTrue
assertThat(listInDiffSizeButWithSameElements equalsIgnoreOrder it).isFalse
assertThat(listInDiffSizeAndElements equalsIgnoreOrder it).isFalse
assertThat(listInDiffElement equalsIgnoreOrder it).isFalse
}
As the assertions above show if we ignore the order of the elements, listInDiffOrder and listExactlySame should be equal to our targetList.
The test passes when we execute it. Therefore, our equalsIgnoreOrder function works as expected.
5. Compare Lists by Property
Sometimes we might want to compare two lists by a property of elements in them. In this section, we’ll learn how to do such a comparison.
First, let’s start by initializing two lists, namely, names1 and names2:
val names1 = listOf("Ron", "Roby", "Peter")
val names2 = listOf("Bob", "John", "Betty")
Now, let’s see how we can zip the two lists and then use the all() function to compare against the length property:
val areEqual = names1.zip(names2).all { (n1, n2) -> n1.length == n2.length }
assertTrue(areEqual)
As expected, both the lists are equal in this case. That’s because the length of individual members at the same index is the same for the two lists.
Next, let’s take another scenario where the names in the lists names1 and names2 aren’t of equal length:
val names1 = listOf("John", "Hagrid", "Rickson")
val names2 = listOf("Abby", "Patrick", "Wren")
Lastly, let’s use the same approach to compare them:
val areEqual = names1.zip(names2).all { (n1, n2) -> n1.length == n2.length }
assertFalse(areEqual)
We got a false, as expected because lengths are unequal in this case.
6. Conclusion
In this article, we’ve explored how to compare two List objects, with or without checking the elements’ order, and by individual property.
Additionally, we’ve learned that using Kotlin’s extension function and infix notation can help write easy-to-read code.
As always, the complete source code used in the article can be found on GitHub.