# Operations on Collections
An essential aspect of functional languages is the ability to easily perform batch operations on collections of objects.
Most functional languages provide powerful support for working with collections, and Kotlin is no exception. You’ve already seen map()
, filter()
, any()
and forEach()
. This atom shows additional operations available for List
s and other collections.
We start by looking at various ways to manufacture List
s. Here, we initialize List
s using lambdas:
// OperationsOnCollections/CreatingLists.kt
import atomictest.eq
fun main() {
// The lambda argument is the element index:
val list1 = List(10) { it }
list1 eq "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
// A list of a single value:
val list2 = List(10) { 0 }
list2 eq "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]"
// A list of letters:
val list3 = List(10) { 'a' + it }
list3 eq "[a, b, c, d, e, f, g, h, i, j]"
// Cycle through a sequence:
val list4 = List(10) { list3[it % 3] }
list4 eq "[a, b, c, a, b, c, a, b, c, a]"
}
This version of the List
constructor has two parameters: the size of the List
and a lambda that initializes each List
element (the element index is passed in as the it
argument). Remember that if a lambda is the last argument, it can be separated from the argument list.
MutableList
s can be initialized in the same way. Here we see the initialization lambda both inside the argument list (mutableList1
) and separated from the argument list (mutableList2
):
// OperationsOnCollections/ListInit.kt
import atomictest.eq
fun main() {
val mutableList1 =
MutableList(5, { 10 * (it + 1) })
mutableList1 eq "[10, 20, 30, 40, 50]"
val mutableList2 =
MutableList(5) { 10 * (it + 1) }
mutableList2 eq "[10, 20, 30, 40, 50]"
}
Note that List()
and MutableList()
are not constructors, but functions. Their names intentionally begin with an upper-case letter to make them look like constructors.
Many collection functions take a predicate and test it against the elements of a collection, some of which we’ve already seen:
filter()
produces a list containing all elements matching the given predicate.any()
returnstrue
if at least one element matches the predicate.all()
checks whether all elements match the predicate.none()
checks that no elements match the predicate.find()
andfirstOrNull()
both return the first element matching the predicate, ornull
if no such element was found.lastOrNull()
returns the last element matching the predicate, ornull
.count()
returns the number of elements matching the predicate.
Here are simple examples for each function:
// OperationsOnCollections/Predicates.kt
import atomictest.eq
fun main() {
val list = listOf(-3, -1, 5, 7, 10)
list.filter { it > 0 } eq listOf(5, 7, 10)
list.count { it > 0 } eq 3
list.find { it > 0 } eq 5
list.firstOrNull { it > 0 } eq 5
list.lastOrNull { it < 0 } eq -1
list.any { it > 0 } eq true
list.any { it != 0 } eq true
list.all { it > 0 } eq false
list.all { it != 0 } eq true
list.none { it > 0 } eq false
list.none { it == 0 } eq true
}
filter()
and count()
apply the predicate against each element, while any()
or find()
stop when the first matching result is found. For example, if the first element satisfies the predicate, any()
returns true
right away, while find()
returns the first matching element. The only time all the elements are processed is if the list contains no elements matching the given predicate.
filter()
returns a group of elements satisfying the given predicate. Sometimes you may be interested in the remaining group—the elements that don’t satisfy the predicate. filterNot()
produces this remaining group, but partition()
can be more useful because it simultaneously produces both lists:
// OperationsOnCollections/Partition.kt
import atomictest.eq
fun main() {
val list = listOf(-3, -1, 5, 7, 10)
val isPositive = { i: Int -> i > 0 }
list.filter(isPositive) eq "[5, 7, 10]"
list.filterNot(isPositive) eq "[-3, -1]"
val (pos, neg) = list.partition { it > 0 }
pos eq "[5, 7, 10]"
neg eq "[-3, -1]"
}
partition()
produces a Pair
object containing List
s. Using [Destructuring Declarations](javascript:void(0)), you can assign the elements of the Pair
to a parenthesized group of var
s or val
s. Destructuring means defining multiple var
s or val
s and initializing them simultaneously, from the expression on the right side of the assignment. Here, destructuring is used with a custom function:
// OperationsOnCollections/PairOfLists.kt
package operationsoncollections
import atomictest.eq
fun createPair() = Pair(1, "one")
fun main() {
val (i, s) = createPair()
i eq 1
s eq "one"
}
filterNotNull()
produces a new List
with the null
s removed:
// OperationsOnCollections/FilterNotNull.kt
import atomictest.eq
fun main() {
val list = listOf(1, 2, null)
list.filterNotNull() eq "[1, 2]"
}
In [Lists](javascript:void(0)), we saw functions such as sum()
or sorted()
applied to a list of comparable elements. These functions can’t be called on lists of non-summable or non-comparable elements, but they have counterparts named sumBy()
and sortedBy()
. You pass a function (often a lambda) as an argument, which specifies the attribute to use for the operation:
// OperationsOnCollections/ByOperations.kt
package operationsoncollections
import atomictest.eq
data class Product(
val description: String,
val price: Double
)
fun main() {
val products = listOf(
Product("bread", 2.0),
Product("wine", 5.0)
)
products.sumByDouble { it.price } eq 7.0
products.sortedByDescending { it.price } eq
"[Product(description=wine, price=5.0)," +
" Product(description=bread, price=2.0)]"
products.minByOrNull { it.price } eq
Product("bread", 2.0)
}
Note that we have two functions sumBy()
and sumByDouble()
to sum integer and double values, respectively. sorted()
and sortedBy()
sort the collection in ascending order, while sortedDescending()
and sortedByDescending()
sort the collection in descending order.
minByOrNull
returns a minimum value based on a given criteria or null
if the list is empty.
take()
and drop()
produce or remove (respectively) the first element, while takeLast()
and dropLast()
produce or remove the last element. These have counterparts that accept a predicate specifying the elements to take or drop:
// OperationsOnCollections/TakeOrDrop.kt
import atomictest.eq
fun main() {
val list = listOf('a', 'b', 'c', 'X', 'Z')
list.takeLast(3) eq "[c, X, Z]"
list.takeLastWhile { it.isUpperCase() } eq
"[X, Z]"
list.drop(1) eq "[b, c, X, Z]"
list.dropWhile { it.isLowerCase() } eq
"[X, Z]"
}
Operations like those you’ve seen for List
s are also available for Set
s:
// OperationsOnCollections/SetOperations.kt
import atomictest.eq
fun main() {
val set = setOf("a", "ab", "ac")
set.maxByOrNull { it.length }?.length eq 2
set.filter {
it.contains('b')
} eq listOf("ab")
set.map { it.length } eq listOf(1, 2, 2)
}
maxByOrNull()
returns null
if a collection is empty, so its result is nullable.
Note that filter()
and map()
, when applied to a Set
, return their results in a List
.
Exercises and solutions can be found at www.AtomicKotlin.com.