1. Overview

In this tutorial, we’ll discuss collection aggregate operations in Kotlin.

2. Aggregate Operations in Kotlin

Aggregate operations in Kotlin are performed on collections of elements, such as arrays and lists, and return a single accumulated value based on the contents of the collection.

2.1. count(), sum(), and average()

We can use the count() function to find the number of elements in a collection:

val numbers = listOf(1, 15, 3, 8)
val count = numbers.count()

assertEquals(4, count)

Subsequently, to find the sum of all elements in a collection, we can use the sum() function:

val sum = numbers.sum()

assertEquals(27, sum)

Likewise, to find the average value of the elements in a collection, we can use the average() function:

val average = numbers.average()

assertEquals(6.75, average)

2.2. sumBy() and sumByDouble()

We can use the sumBy() function to find the sum of all values that are mapped by applying the selector function to each element in the collection. This function always returns an integer value. Despite that, we can also use this function with a list of Byte, Short, Long, and Float elements:

val sumBy = numbers.sumBy { it * 5 }

assertEquals(135, sumBy)

To operate with functions that return a double value, we can use the sumByDouble() function:

val sumByDouble = numbers.sumByDouble { it.toDouble() / 8 }

assertEquals(3.375, sumByDouble())

As of Kotlin 1.4, there is a new sumOf() method with different overloaded versions to achieve the same thing with a better API:

data class Employee(val name: String, val salary: Int, val age: UInt)

val employees = listOf(Employee("A", 3500, 23u), Employee("B", 2000, 30u))

assertEquals(5500, employees.sumOf { it.salary })
assertEquals(53u, employees.sumOf { it.age })

As shown above, we can use the same function to sum Int and UInt values and return Int and UInt summations, respectively. So, there’s no need to have functions with different names for each data type, just like sumBy() and sumByDouble().

To be more specific, we can use the sumOf() extension function with Int, Long, Double, UIntULong, BigInteger, and BigDecimal data types.

2.3. min() and max()

To find the largest element in a collection, we can use the max() function:

val maximum = numbers.max()

assertEquals(15, maximum)

Similarly, we can use the min() function to find the smallest element in a collection:

val minimum = numbers.min()

assertEquals(1, minimum)

Both functions return null if there are no elements in the collection.

As of Kotlin 1.4, these functions are deprecated. There are two new functions named maxOrNull() and minOrNull() that are consistent with the rest of the collections API and also are more explicit about what they may return.

In addition to these two, the max and min related functions described in the following sections are also deprecated in favor of new orNull alternatives.

2.4. maxBy() and minBy()

The maxBy() and minBy() functions convert the elements in the collection to a comparable type and compare them by the computed value.

To find the first element that yields the largest value from the given selector function, we can use the maxBy() function:

val maxBy = numbers.maxBy { it % 5 }

assertEquals(3, maxBy)

Furthermore, we can use the minBy() function to find the first element that yields the smallest value from the given selector function:

val minBy = numbers.minBy { it % 5 }

assertEquals(15, minBy)

Both of these functions return null when there are no elements in the collection.

2.5. maxWith() and minWith()

The maxWith() and minWith() functions compare the elements in the collection with each other and sort them by the return value of the comparator.

We can use maxWith() to find the first element that has the largest value according to the Comparator object:

val strings = listOf("Berlin", "Kolkata", "Prague", "Barcelona")
val maxWith = strings.maxWith(compareBy { it.length % 4 })

assertEquals("Kolkata", maxWith)

Similarly, to return the first element that has the smallest value according to the Comparator object, we can use the minWith() function:

val minWith = strings.minWith(compareBy { it.length % 4 })

assertEquals("Barcelona", minWith)

Both these functions return null if there are no elements in the collection.

3. Fold and Reduce Functions

The fold() and reduce() functions can be used to apply operations sequentially on a collection of elements and return the accumulated result. The two arguments that these functions require are the accumulated value and the element of the collection.

The difference between the two functions is simple. The fold() function takes an initial value, and the first invocation of the lambda will receive that initial value and the first element of the collection as parameters. In contrast, the reduce() function doesn’t take an initial value, and instead uses the first and second elements of the collection as parameters during the first invocation of the lambda.

We use fold() for cases where we need to define a default value for our operation. Alternatively, reduce() is useful where our operation depends only on values that are defined in the collection.

Another difference is that the reduce() function will throw an exception when performed on an empty collection. However, since the fold() function requires an initial value, it will be used as the default value in the case of an empty collection and, thus, will not throw an exception.

3.1. fold() and foldRight()

The fold() function takes an initial value for the accumulator. It then accumulates the value by applying the operation from left to right to the current accumulator value and each element of the collection:

val numbers = listOf(1, 15, 3, 8)
val result = numbers.fold(100) { total, it -> total - it }

assertEquals(73, result)

The first call to the lambda function will be with parameters 100 and 1.

The foldRight() function works similarly to the fold() function, but it traverses the elements in the collection from right to left. Moreover, the order of the operation arguments also changes where the element is used first and the accumulated value second:

val result = numbers.foldRight(100) { it, total -> total - it }

assertEquals(73, result)

In the above example, the first call to the lambda function will be with parameters 100 and 8.

3.2. foldIndexed() and foldRightIndexed()

The foldIndexed() function is useful when we want to apply operations based on the element indices. It accumulates the value starting with the initial value, and then applies the operation from left to right to the current accumulator value and every element with its index in the defined collection. In addition, the foldIndexed() function takes the element index as the operation argument along with the accumulated value and element of the collection:

val result = numbers.foldIndexed(100) { index, total, it ->
    if (index.minus(2) >= 0) total - it else total
}

assertEquals(89, result)

The first call to the lambda expression here will be with parameters 100 and 1. Since parameter 1 doesn’t satisfy the index condition in the lambda expression, the next best parameter 3 is called because it satisfies the index condition.

The foldRightIndexed() function works similarly to the foldIndexed() function while traversing the collection from right to left:

val result = numbers.foldRightIndexed(100) { index, it, total ->
    if (index.minus(2) >= 0) total - it else total
}

assertEquals(89, result)

In this case, the first call to the lambda expression will be with parameters 100 and 8. Since the parameter 8 satisfies the index condition in the lambda expression, it is used for the accumulated value.

3.3. reduce() and reduceRight()

The reduce() function is useful when we have a list of elements and we want to reduce it to a single value. This function accumulates value starting with the first element and then applies the operation from left to right to the current accumulator value and each element of the collection:

val result = numbers.reduce { total, it -> total - it }

assertEquals(-25, result)

The first call to the lambda here will be with parameters 1 and 15.

The reduceRight() function works similarly to the reduce() function, but it traverses the collection from right to left. Therefore, this function accumulates value starting with the last element:

val result = numbers.reduceRight() { it, total -> total - it }

assertEquals(-11, result)

In this case, the first call to the lambda will be with parameters 8 and 3.

3.4. reduceIndexed() and reduceRightIndexed()

The reduceIndexed() function accumulates value starting with the first element and then applies the operation from left to right to the current accumulator value and every element with its index in the defined collection:

val result = numbers.reduceIndexed { index, total, it ->
    if (index.minus(2) >= 0) total - it else total
}

assertEquals(-10, result)

The first call to the lambda expression here will be with parameters 1 and 15. Since parameter 15 doesn’t satisfy the index condition in the lambda expression, the next parameter 3 is called, as it satisfies the index condition.

The reduceRightIndexed() function works similarly to the reduceIndexed() function, but it traverses the collection from right to left:

val result = numbers.reduceRightIndexed { index, it, total ->
    if (index.minus(2) >= 0) total - it else total
}

assertEquals(5, result)

In this case, the first call to the lambda expression will be with parameters 8 and 3. Since parameter 3 satisfies the index condition in the lambda expression, it is used for the accumulated value.

3.5. Intermediate Results

So far, we’ve seen folding and reducing functions that apply some transformation to a series of values, one after another. When this chain ends, they return the final folded or reduced value.

As of Kotlin 1.4, we can also have access to all the intermediate results in addition to the final value. To be more specific, for folding, we can use runningFold():

val numbers = listOf(1, 2, 3, 4, 5)
assertEquals(listOf(0, 1, 3, 6, 10, 15), numbers.runningFold(0) {total, it -> total + it})

As shown above, this function folds the given list with zero as the initial value. During the operation, it keeps all the intermediate values and returns them.

The same is true for reducing with runningReduce():

assertEquals(listOf(1, 3, 6, 10, 15), numbers.runningReduce { total, it -> total + it })

4. Conclusion

In this article, we saw the various aggregate operations in Kotlin for working with collections. Refer to our Kotlin tutorials to learn more about Kotlin’s features.

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

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