Kotlin collections offer convenient ways for different operations with data. They also implement an interface and thus can be used to operate with different types of data. For instance, you can write a function that can print each element of both Set and List. Let's figure out how it works.
Iterable
We have already figured out that there are two universal interfaces in Kotlin: Iterable and MutableIterable. They provide methods for working with sequences of elements.
Classes that inherit from Iterable have a set of common methods: iterator(), forEach(), contains(), count(), drop(), filter(), etc. MutableIterable, unlike Iterable, also supports removing elements during iterations. For example, remove() and removeAll(). It's interesting that they both contain the method drop(), which drops a certain element. But unlike remove() from MutableIterable, which really removes a certain element in a certain object, drop() returns a new object.
Notice that you can use one iterator only once per iteration over List (or Set). If you want to do it one more time, you must create a new iterator. But you can also use iteration with the for loop or forEach().
Collection interfaces
And now, let's consider how inheritance works in Kotlin's collections. In the diagram below, you can see relations between different classes. There are two iterable interfaces, which we have considered above. Interfaces Collection<T> and MutableCollection<T> inherit from them. In their turn, List and Set inherit from Collection<T>, and MutableList and MutableSet — from MutableCollection<T>.
Collection<T> is the root for a hierarchy of immutable collections — List and Set. It provides methods like size, get(), find(), filter(), count(), etc. MutableCollections<T> is the root for MutableList and MutableSet, and it supports adding and removing elements with the help of the following methods: add(), addAll(), remove(), removeAll(), retainAll(), and drop().
Collection and MutableCollection inherit from Iterable and MutableIterable. So, these interfaces support all the methods available to their parents.
However, maps don't represent Collection or Iterable interfaces, though we also call them collections. It means that they have methods different from other collections. And it is logical because they contain key-value pairs — as opposed to single elements in Set or List, so we need to separate methods for them. Unfortunately, we can't use Map with Collection interfaces.
Collection
Let's see how we can use these interfaces. For instance, we want to print all the collection elements. In this case, we can use a for loop, iterator(), or the forEach() method:
// All realizations of the function printAll do the same thing.
fun printAll(strings: Collection<String>) {
for(str in strings) print("$str ")
}
fun printAll(songs: Collection<String>) {
songs.forEach { print("$it ") }
}
fun printAll(songs: Collection<String>) {
val songsIterator = songs.iterator() // We create iterator, which will help us go through the List
while (songsIterator.hasNext()) { // hasNext() checks if our iterator contains next element
print("${songsIterator.next()} ") // next() moves the pointer to the next element of iterator
}
}
val listOfSongs = listOf("Creep", "Idioteque", "Street Spirit", "Paranoid Android") // We can also use setOf()
printAll(listOfSongs) // Creep Idioteque Street Spirit Paranoid Android
Cool! We are now able to work with both types of immutable collections Set and List.
You can use one iterator only once per iteration over List (or Set). If you want to do it one more time, you must create a new iterator().
There are some common methods for all collections.
count()returns the number of elements that meet the conditions.drop()returns a newListof elements withoutnfirst elements.containsAll()checks if the collection contains all elements from another collection and returnstrueorfalse.
Some examples:
fun countElements(strings: Collection<String>) = strings.count { it.matches("\\w+".toRegex()) }
fun dropElements(songs: Collection<String>) = songs.drop(2).toSet()
fun compareCollections(old: Collection<String>, new: Collection<String>) = old.containsAll(new)
val setOfSongs = setOf("Creep", "Idioteque", "Street Spirit", "Paranoid Android")
val listOfSongs = listOf("Creep", "Idioteque", "Street Spirit", "Paranoid Android")
println(countElements(setOfSongs)) // output: 2
println(dropElements(listOfSongs)) // output: [Street Spirit, Paranoid Android]
println(compareCollections(listOfSongs, setOfSongs)) // output: true
joinToString()returns the collection as a string with a specific delimiter.find()returns the first element which fits the pattern.filter()returns aListof elements that meet the conditions.minus()returns the collection without the element or elements specified in the condition.random()returns a random element of the collection.
Some examples:
fun convertToString(strings: Collection<String>) = strings.joinToString(" | ")
fun findElement(strings: Collection<String>) = strings.find { it.contains("I") }
fun filterElements(strings: Collection<String>) = strings.filter { it.contains("t") }
fun returnRandomElement(strings: Collection<String>) = strings.random()
fun decreaseCollection(strings: Collection<String>) = strings.minus("Creep")
// minus could have a collection as parameter
val listOfSongs = listOf("Creep", "Idioteque", "Street Spirit", "Paranoid Android")
val setOfSongs = setOf("Creep", "Idioteque", "Street Spirit", "Paranoid Android")
println(convertToString(listOfSongs)) // output: Creep | Idioteque | Street Spirit | Paranoid Android
println(findElement(setOfSongs)) // output: Idioteque
println(filterElements(listOfSongs)) // output: [Idioteque, Street Spirit]
println(returnRandomElement(setOfSongs)) // output: Street Spirit
println(decreaseCollection(setOfSongs)) // output: [Idioteque, Street Spirit, Paranoid Android]
Notice that drop() or minus() don’t change the original collection; instead, they create a new collection, so they also work with Collection<T>, an immutable type.
MutableCollection
Now, let's consider the use of MutableCollection. It can use all the methods from Collection, but it also has some specific operations.
Notice that the specific MutableCollection methods, unlike drop() or minus(), change the original collection. So a function with these methods will return a boolean value — true if the operation was successful and false if the operation failed. You have already studied these methods, and they work the same way with MutableCollection as a function argument.
addAll()adds all elements from one collection to another.add()adds one element to a collection.clear()removes all elements from a collection.remove()removes the first occurrence of a certain element from a collection.
See the examples below:
fun addCollection(old: MutableCollection<String>, new: Collection<String>) {
old.addAll(new)
}
fun addNewElement(old: MutableCollection<String>) {
old.add("Spectre")
}
fun clearCollection(old: MutableCollection<String>) {
old.clear()
}
fun removeElement(old: MutableCollection<String>): Boolean {
return old.remove("Creep")
}
val oldSongs = mutableSetOf("Creep", "Street Spirit")
val newSongs = listOf("Creep", "Street Spirit", "Paranoid Android")
clearCollection(oldSongs)
println(oldSongs) // []
addCollection(oldSongs, newSongs)
println(oldSongs) // [Creep, Street Spirit, Paranoid Android]
addNewElement(oldSongs)
println(oldSongs) // [Creep, Street Spirit, Paranoid Android, Spectre]
removeElement(oldSongs)
println(oldSongs) // [Street Spirit, Paranoid Android, Spectre]
println(removeElement(oldSongs)) // false because this collection doesn't contain "Creep"
The retainAll() method leaves the unique elements in the collection — only those that are also contained in the specified collection.
fun retainAllFromCollection(old: MutableCollection<String>, new: Collection<String>) {
old.retainAll(new)
}
val oldSongs = mutableSetOf("Creep", "Street Spirit", "Paranoid Android")
val newSongs = listOf("Spectre", "Street Spirit")
retainAllFromCollection(oldSongs, newSongs)
println(oldSongs) // [Street Spirit]
Thus, you can use Collection and MutableCollection interfaces to perform any universal operations with their inheritors and don't need to think about their types.
Function and collections
Considering collections as interfaces and keeping in mind the inheritance of data types, you can create functions for processing different data. And it isn't only about the Collection type — you can create functions with parameters of a certain type, List or Set, with the parent type of unit within them.
fun processNumbers(list: List<Number>) {
list.forEach { print("$it ") }
}
val numbers1 = listOf(0, 12, 10)
val numbers2 = listOf(0.0, 12.0, 10.0)
val numbers3 = listOf(423324534536356, 4L, 56L)
processNumbers(numbers1) // 0 12 10
processNumbers(numbers2) // 0.0 12.0 10.0
processNumbers(numbers3) // 423324534536356 4 56
In these cases, however, you have a limited set of methods — only the common methods which are possible to apply to the parent data type. For example, if you try to increase each element of a list by 1, the compiler will find a problem:
fun processNumbers(list: List<Number>) {
list.forEach { print(it + 1) }
}
The compiler’s response is: "Unresolved reference. None of the following candidates is applicable because of receiver type mismatch…"
It happens because the abstract class Number doesn't support any computing operations — they are implemented only in certain classes, like Int or Double. But Number does support different converting operations, like toDouble(), toInt(), or toChar():
fun processNumbers(list: List<Number>) {
list.forEach { print("${it.toChar()} ") }
}
val numbers1 = listOf(41, 42, 43)
processNumbers(numbers1) // ) * +Conclusion
In this topic, we have learned that collections are also interfaces. You can use them for universal operations with different types of data. We've also seen that even though we call Map a collection, this data type doesn't inherit from Collection.
Now, you can write universal functions for processing different collection types, like Set or List, MutableSet or MutableList. No need to care about their type: the important thing is that they inherit from Collection or MutableCollection.
Also, keep in mind that Set doesn’t inherit from MutableCollection, likewise MutableList doesn’t inherit from Collection.