Kotlin's Collections in a Nutshell

Romman SabbirRomman Sabbir
10 min read

[Background Image by JOHN TOWNER]

Kotlin, known for its conciseness, expressiveness, and interoperability with Java, provides powerful tools for working with collections.

What is Collections in Kotlin?

The Collections class in Kotlin is like a toolbox full of useful tools for working with groups of items, like lists or sets. Just like a toolbox helps us organize and manage our tools, the Collections class helps us to organize and manage our data.

Imagine we have a bunch of books scattered around our room. The Collections class helps us neatly arrange them on our bookshelf, sort them by title or author, find a specific book quickly, or even remove some books we don't need anymore.

In programming terms, the Collections class provides methods for tasks like sorting a list, finding the minimum or maximum value, shuffling elements randomly, searching for specific items, and much more. It's like having a set of handy functions to make working with collections of data easier and more efficient in our Kotlin code.

Overview of Kotlin Collections

Before diving deep into the Collections class, let's briefly recap Kotlin's collections hierarchy:

  1. Immutable Collections: These collections, once created, cannot be modified. They provide a read-only view of the data. Examples include List, Set, and Map.

  2. Mutable Collections: These collections can be modified after creation. Examples include MutableList, MutableSet, and MutableMap.

Now, let's focus on the Collections class.

Functionality

The Collections class serves as a utility class for common operations on collections. It provides methods for creating instances of various collection types, as well as utility functions for working with collections.

1. Factory Methods

  • emptyList(), emptySet(), emptyMap(): These methods return empty instances of List, Set, and Map, respectively.

  • listOf(), setOf(), mapOf(): These methods create immutable instances of List, Set, and Map containing the specified elements.

  • mutableListOf(), mutableSetOf(), mutableMapOf(): These methods create mutable instances of List, Set, and Map containing the specified elements.

2. Utility Functions

  • reverse(): Reverses the order of elements in a list.

  • shuffle(): Shuffles the elements in a list randomly.

  • sort(): Sorts the elements in a list in natural order or using a custom comparator.

  • binarySearch(): Searches for an element in a sorted list using binary search.

3. Type Conversion

  • toMutableList(), toMutableSet(), toMutableMap(): Converts an immutable collection to a mutable one.

Common Examples

  1. Sorting a List:

     val list = mutableListOf(3, 1, 4, 1, 5, 9, 2, 6)
     val sortedList = Collections.sort(list)
     println(sortedList) // Output: [1, 1, 2, 3, 4, 5, 6, 9]
    
  2. Finding Minimum and Maximum:

     val list = listOf(3, 1, 4, 1, 5, 9, 2, 6)
     val min = Collections.min(list)
     val max = Collections.max(list)
     println("Min: $min, Max: $max") // Output: Min: 1, Max: 9
    
  3. Reversing a List:

     val list = mutableListOf(1, 2, 3, 4, 5)
     Collections.reverse(list)
     println(list) // Output: [5, 4, 3, 2, 1]
    
  4. Shuffling a List:

     val list = mutableListOf(1, 2, 3, 4, 5)
     Collections.shuffle(list)
     println(list) // Output: [3, 2, 1, 5, 4] (Randomly shuffled)
    
  5. Binary Search:

     val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
     val index = Collections.binarySearch(list, 5)
     println("Index of 5: $index") // Output: Index of 5: 4
    
  6. Creating Immutable Collections:

     val immutableList = Collections.singletonList("Hello")
     println(immutableList) // Output: [Hello]
    
  7. Finding Frequency of Elements:

     val list = listOf(1, 2, 3, 1, 2, 1, 3, 4, 5)
     val frequencyMap = HashMap<Int, Int>()
     for (item in list) {
         frequencyMap[item] = Collections.frequency(list, item)
     }
     println(frequencyMap) // Output: {1=3, 2=2, 3=2, 4=1, 5=1}
    
  8. Swapping Elements:

     val list = mutableListOf(1, 2, 3, 4, 5)
     Collections.swap(list, 0, 4)
     println(list) // Output: [5, 2, 3, 4, 1]
    
  9. Checking for Equality of Two Lists:

     val list1 = listOf(1, 2, 3, 4, 5)
     val list2 = listOf(5, 4, 3, 2, 1)
     val isEqual = Collections.disjoint(list1, list2)
     println("Are lists equal: ${!isEqual}") // Output: Are lists equal: true
    
  10. Filling a List with Specified Value:

    val list = mutableListOf<Int>()
    Collections.fill(list, 10)
    println(list) // Output: [10, 10, 10, 10, 10] (List filled with 10)
    
  11. Creating Synchronized Collections:

    val synchronizedList = Collections.synchronizedList(mutableListOf(1, 2, 3))
    
  12. Finding Sublist:

    val list = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
    val subList = list.subList(2, 5)
    println(subList) // Output: [3, 4, 5]
    

These are just a few examples of the functionality provided by the Collections class in Kotlin. It offers a wide range of utility methods for working with collections efficiently.

Remember, Kotlin also provides extension functions and other methods on collection types directly, so we might not always need to use Collections class directly.

Practical Usages

  1. E-commerce Platform: In an e-commerce platform, we may need to work with collections of products, orders, and customers. We can use Kotlin's Collections class for tasks like sorting products by price, finding the most popular products based on order frequency, or creating a list of recommended products for a specific customer based on their purchase history.

  2. Social Media Analytics Tool: When building a social media analytics tool, we might collect data such as likes, comments, and shares for posts. We can use Kotlin's Collections class to analyze this data, such as finding the most engaging posts, identifying trends based on hashtags, or clustering users based on their interactions with content.

  3. Inventory Management System: In an inventory management system for a warehouse or retail store, we'll work with collections of items, orders, and shipments. Kotlin's Collections class can help with tasks like sorting inventory by category or quantity, finding items that need restocking, or optimizing routes for order fulfillment based on the location of items in the warehouse.

  4. Travel Booking Platform: When developing a travel booking platform, we'll deal with collections of flights, hotels, and itineraries. Kotlin's Collections class can be useful for tasks like filtering available flights based on departure time or price, sorting hotels by rating or proximity to attractions, or generating personalized travel packages for users based on their preferences.

  5. Healthcare Management System: In a healthcare management system, we'll handle collections of patient records, appointments, and medical procedures. Kotlin's Collections class can assist with tasks like organizing patient data by demographics or medical history, scheduling appointments based on availability of doctors and facilities, or analyzing trends in patient diagnoses and treatments.

These examples illustrate how Kotlin's Collections class can be applied in various real-life projects to efficiently manage and process data.

Managing Threads Effectively

Kotlin's coroutines are a powerful feature for asynchronous programming and managing concurrency. When combined with collections functionality, they provide an efficient way to process data concurrently while managing threads effectively. Here's how we can use coroutines with collections for thread management:

  1. Parallel Processing withmap:

     import kotlinx.coroutines.*
    
     suspend fun processData(item: Int): String {
         // Simulate some processing delay
         delay(1000)
         return "Processed: $item"
     }
    
     suspend fun main() {
         val list = listOf(1, 2, 3, 4, 5)
         val processedData = list.map { item ->
             // Launch a coroutine to process each item concurrently
             async {
                 processData(item)
             }
         }.awaitAll() // Wait for all coroutines to complete
    
         println(processedData)
     }
    
  2. Concurrency withforEach:

     import kotlinx.coroutines.*
    
     suspend fun processData(item: Int): String {
         // Simulate some processing delay
         delay(1000)
         return "Processed: $item"
     }
    
     suspend fun main() {
         val list = listOf(1, 2, 3, 4, 5)
         val deferredList = mutableListOf<Deferred<String>>()
         list.forEach { item ->
             // Launch a coroutine to process each item concurrently
             val deferred = withContext(Dispatchers.Default) {
                 processData(item)
             }
             deferredList.add(deferred)
         }
    
         val processedData = deferredList.awaitAll() // Wait for all coroutines to complete
         println(processedData)
     }
    
  3. Batch Processing withchunked:

     import kotlinx.coroutines.*
    
     suspend fun processDataBatch(batch: List<Int>): List<String> {
         // Simulate some processing delay
         delay(1000)
         return batch.map { item -> "Processed: $item" }
     }
    
     suspend fun main() {
         val list = (1..10).toList()
         val batchSize = 3
         val processedData = list.chunked(batchSize).map { batch ->
             // Launch a coroutine to process each batch concurrently
             withContext(Dispatchers.Default) {
                 processDataBatch(batch)
             }
         }.flatMap { it.await() } // Wait for all coroutines to complete and flatten the results
    
         println(processedData)
     }
    
  4. Filtering Concurrently:

     import kotlinx.coroutines.*
    
     suspend fun checkCondition(item: Int): Boolean {
         // Simulate some condition checking delay
         delay(1000)
         return item % 2 == 0
     }
    
     suspend fun main() {
         val list = (1..10).toList()
         val filteredData = list.map { item ->
             // Launch a coroutine to check condition concurrently
             async {
                 item to checkCondition(item)
             }
         }.awaitAll().filter { it.second }.map { it.first } // Wait for all coroutines and filter results
    
         println(filteredData)
     }
    
  5. Concurrent Data Transformation:

     import kotlinx.coroutines.*
    
     suspend fun transformData(item: Int): String {
         // Simulate some transformation delay
         delay(1000)
         return "Transformed: $item"
     }
    
     suspend fun main() {
         val list = (1..10).toList()
         val transformedData = list.map { item ->
             // Launch a coroutine to transform data concurrently
             withContext(Dispatchers.Default) {
                 transformData(item)
             }
         }.map { it.await() } // Wait for all coroutines and collect results
    
         println(transformedData)
     }
    
  6. Concurrent Data Processing withzip:

     import kotlinx.coroutines.*
    
     suspend fun processData(item1: Int, item2: Int): Int {
         // Simulate some processing delay
         delay(1000)
         return item1 + item2
     }
    
     suspend fun main() {
         val list1 = listOf(1, 2, 3, 4, 5)
         val list2 = listOf(6, 7, 8, 9, 10)
         val processedData = list1.zip(list2).map { (item1, item2) ->
             // Launch a coroutine to process data concurrently
             async {
                 processData(item1, item2)
             }
         }.map { it.await() } // Wait for all coroutines and collect results
    
         println(processedData)
     }
    

These examples showcase how Kotlin coroutines can be used with collections to perform process collections concurrently, thereby optimizing performance various concurrent tasks such as filtering, transformation, and processing, and efficiently utilizing threads and improving performance.

Extensions Functions

The Kotlin standard library provides a variety of extension functions for working with collections. Here are some of them along with examples:

  1. filter(): Returns a list containing only elements matching the given predicate.

     val list = listOf(1, 2, 3, 4, 5)
     val filteredList = list.filter { it % 2 == 0 }
     println(filteredList) // Output: [2, 4]
    
  2. map(): Returns a list containing the results of applying the given transform function to each element in the original collection.

     val list = listOf(1, 2, 3, 4, 5)
     val squaredList = list.map { it * it }
     println(squaredList) // Output: [1, 4, 9, 16, 25]
    
  3. flatMap(): Returns a single list of all elements resulting from applying the given transform function to each element of the original collection.

     val nestedList = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))
     val flattenedList = nestedList.flatMap { it }
     println(flattenedList) // Output: [1, 2, 3, 4, 5, 6]
    
  4. distinct(): Returns a list containing only distinct elements from the original collection.

     val list = listOf(1, 2, 2, 3, 3, 3, 4, 4, 4, 4)
     val distinctList = list.distinct()
     println(distinctList) // Output: [1, 2, 3, 4]
    
  5. groupBy(): Groups elements of the original collection by the key returned by the given keySelector function.

     data class Person(val name: String, val age: Int)
    
     val people = listOf(
         Person("Alice", 20),
         Person("Bob", 30),
         Person("Alice", 25)
     )
    
     val groupedByAge = people.groupBy { it.age }
     println(groupedByAge) // Output: {20=[Person(name=Alice, age=20)], 30=[Person(name=Bob, age=30), Person(name=Alice, age=25)]}
    
  6. sortedBy(): Returns a list of all elements sorted according to natural sort order of the value returned by specified selector function.

     val people = listOf(
         Person("Alice", 20),
         Person("Bob", 30),
         Person("Charlie", 25)
     )
    
     val sortedByName = people.sortedBy { it.name }
     println(sortedByName) // Output: [Person(name=Alice, age=20), Person(name=Bob, age=30), Person(name=Charlie, age=25)]
    
  7. associateBy(): Returns a map containing key-value pairs provided by the given transform function applied to each element of the original collection.

     val people = listOf(
         Person("Alice", 20),
         Person("Bob", 30),
         Person("Charlie", 25)
     )
    
     val mapByName = people.associateBy { it.name }
     println(mapByName) // Output: {Alice=Person(name=Alice, age=20), Bob=Person(name=Bob, age=30), Charlie=Person(name=Charlie, age=25)}
    
  8. partition(): Splits the original collection into pair of lists, where first list contains elements for which the predicate yielded true, while the second list contains elements for which the predicate yielded false.

     val list = listOf(1, 2, 3, 4, 5)
     val (even, odd) = list.partition { it % 2 == 0 }
     println("Even: $even, Odd: $odd") // Output: Even: [2, 4], Odd: [1, 3, 5]
    

These are just a few examples of extension functions provided by the Kotlin standard library for working with collections. They make working with collections in Kotlin more concise and expressive.


That's it for today. Happy Coding...

0
Subscribe to my newsletter

Read articles from Romman Sabbir directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Romman Sabbir
Romman Sabbir

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.