Kotlin - Collections

Kotlin Collections

Collections are fundamental to programming. Kotlin provides rich collection types (List, Set, Map) with both mutable and immutable variants, plus powerful operations for data processing.

Collection Types Overview

Immutable vs Mutable Collections

✅ Immutable (Read-Only) - Preferred

val list = listOf(1, 2, 3)
val set = setOf("a", "b", "c")
val map = mapOf("key" to "value")

// Cannot modify:
// list.add(4)  // ❌ No such method

⚠️ Mutable (Changeable) - When Needed

val list = mutableListOf(1, 2, 3)
val set = mutableSetOf("a", "b", "c") 
val map = mutableMapOf("key" to "value")

// Can modify:
list.add(4)           // ✅ Allowed
set.remove("a")       // ✅ Allowed
map["new"] = "value"  // ✅ Allowed
Best Practice: Start with immutable collections and only use mutable ones when you need to modify the collection after creation.

Lists - Ordered Collections

Creating Lists

// Immutable lists
val fruits = listOf("apple", "banana", "cherry")
val numbers = listOf(1, 2, 3, 4, 5)
val mixedList = listOf("text", 42, true)  // Any type
val emptyList = emptyList<String>()

// Mutable lists
val mutableFruits = mutableListOf("apple", "banana")
val growingList = mutableListOf<String>()

// ArrayList (specific implementation)
val arrayList = arrayListOf(1, 2, 3)

List Operations

val numbers = listOf(1, 2, 3, 4, 5)

// Accessing elements
println(numbers[0])          // 1 (first element)
println(numbers.first())     // 1
println(numbers.last())      // 5
println(numbers.get(2))      // 3

// Safe access
println(numbers.getOrNull(10))  // null (instead of exception)
println(numbers.getOrElse(10) { -1 })  // -1 (default value)

// Properties
println(numbers.size)        // 5
println(numbers.isEmpty())   // false
println(numbers.isNotEmpty()) // true

// Checking contents
println(3 in numbers)        // true
println(numbers.contains(6)) // false

Mutable List Operations

val fruits = mutableListOf("apple", "banana")

// Adding elements
fruits.add("cherry")                 // [apple, banana, cherry]
fruits.add(0, "orange")             // [orange, apple, banana, cherry]
fruits += "grape"                   // [orange, apple, banana, cherry, grape]
fruits.addAll(listOf("kiwi", "mango"))

// Removing elements
fruits.remove("apple")              // Remove by value
fruits.removeAt(0)                  // Remove by index
fruits -= "banana"                  // Remove using -= operator
fruits.clear()                      // Remove all elements

println(fruits) // []

Sets - Unique Collections

Creating Sets

// Immutable sets (no duplicates)
val colors = setOf("red", "green", "blue", "red")  // Only 3 items
val uniqueNumbers = setOf(1, 2, 3, 2, 1)          // Only 3 items
val emptySet = emptySet<String>()

// Mutable sets
val mutableColors = mutableSetOf("red", "green")
val hashSet = hashSetOf(1, 2, 3)

println(colors)        // [red, green, blue]
println(colors.size)   // 3 (duplicates removed)

Set Operations

val set1 = setOf(1, 2, 3, 4)
val set2 = setOf(3, 4, 5, 6)

// Basic operations
println(3 in set1)           // true
println(set1.contains(5))    // false

// Set mathematics
println(set1 union set2)     // [1, 2, 3, 4, 5, 6] (all elements)
println(set1 intersect set2) // [3, 4] (common elements)  
println(set1 subtract set2)  // [1, 2] (elements only in set1)

// Mutable set operations
val mutableSet = mutableSetOf(1, 2, 3)
mutableSet.add(4)           // true (added)
mutableSet.add(2)           // false (already exists)
mutableSet.remove(1)        // true (removed)
println(mutableSet)         // [2, 3, 4]

Maps - Key-Value Pairs

Creating Maps

// Immutable maps
val ages = mapOf("Alice" to 30, "Bob" to 25, "Charlie" to 35)
val grades = mapOf(
    "Alice" to 'A',
    "Bob" to 'B', 
    "Charlie" to 'A'
)
val emptyMap = emptyMap<String, Int>()

// Mutable maps
val mutableAges = mutableMapOf("Alice" to 30, "Bob" to 25)
val hashMap = hashMapOf("key1" to "value1", "key2" to "value2")

Map Operations

val phoneBook = mapOf(
    "Alice" to "555-1234",
    "Bob" to "555-5678",
    "Charlie" to "555-9012"
)

// Accessing values
println(phoneBook["Alice"])           // 555-1234
println(phoneBook.get("Alice"))       // 555-1234
println(phoneBook["David"])           // null (key doesn't exist)
println(phoneBook.getValue("Alice"))  // 555-1234 (throws exception if not found)

// Safe access
println(phoneBook.getOrDefault("David", "Unknown"))  // Unknown
println(phoneBook.getOrElse("David") { "Not found" }) // Not found

// Properties and checks
println(phoneBook.size)               // 3
println(phoneBook.isEmpty())          // false
println("Alice" in phoneBook)         // true
println(phoneBook.containsKey("David")) // false
println(phoneBook.containsValue("555-1234")) // true

// Getting keys and values
println(phoneBook.keys)    // [Alice, Bob, Charlie]
println(phoneBook.values)  // [555-1234, 555-5678, 555-9012]

Mutable Map Operations

val inventory = mutableMapOf(
    "apples" to 10,
    "bananas" to 5,
    "oranges" to 8
)

// Adding/updating entries
inventory["grapes"] = 12         // Add new entry
inventory.put("kiwis", 6)        // Alternative way to add
inventory["apples"] = 15         // Update existing value
inventory += "mangoes" to 4      // Using += operator

// Removing entries
inventory.remove("bananas")      // Remove by key
inventory -= "oranges"           // Using -= operator

// Batch operations
inventory.putAll(mapOf("berries" to 20, "cherries" to 15))
inventory.clear()                // Remove all entries

println(inventory)  // {}

Collection Operations

Filtering

val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

// Filter elements
val evenNumbers = numbers.filter { it % 2 == 0 }
val oddNumbers = numbers.filterNot { it % 2 == 0 }
val greaterThanFive = numbers.filter { it > 5 }

println(evenNumbers)      // [2, 4, 6, 8, 10]
println(oddNumbers)       // [1, 3, 5, 7, 9]
println(greaterThanFive)  // [6, 7, 8, 9, 10]

// Filter by type
val mixedList = listOf("hello", 42, "world", 3.14, true)
val strings = mixedList.filterIsInstance<String>()
val numbers2 = mixedList.filterIsInstance<Number>()

println(strings)  // [hello, world]
println(numbers2) // [42, 3.14]

Transformation (Map)

val words = listOf("hello", "world", "kotlin")

// Transform elements
val lengths = words.map { it.length }
val upperCase = words.map { it.uppercase() }
val withIndex = words.mapIndexed { index, word -> "$index: $word" }

println(lengths)    // [5, 5, 6]
println(upperCase)  // [HELLO, WORLD, KOTLIN]
println(withIndex)  // [0: hello, 1: world, 2: kotlin]

// Transform and flatten
val sentences = listOf("hello world", "kotlin programming")
val allWords = sentences.flatMap { it.split(" ") }
println(allWords)   // [hello, world, kotlin, programming]

Aggregation

val scores = listOf(85, 92, 78, 96, 88)

// Basic aggregations
println(scores.sum())        // 439
println(scores.average())    // 87.8
println(scores.min())        // 78
println(scores.max())        // 96
println(scores.count())      // 5

// Conditional aggregations
println(scores.count { it > 90 })    // 2
println(scores.sumOf { it * 2 })     // 878 (sum of doubled values)

// Reduce operations
val product = scores.reduce { acc, score -> acc * score }
val concatenated = listOf("A", "B", "C").reduce { acc, letter -> acc + letter }

println(product)      // Very large number (multiplication of all scores)
println(concatenated) // ABC

Searching and Finding

val students = listOf("Alice", "Bob", "Charlie", "Diana", "Eve")

// Find operations
val firstLongName = students.find { it.length > 5 }        // Charlie
val lastLongName = students.findLast { it.length > 5 }     // Charlie
val anyShortName = students.any { it.length < 4 }          // true (Bob, Eve)
val allLongNames = students.all { it.length > 2 }          // true
val noneEmpty = students.none { it.isEmpty() }             // true

// First/last with conditions
val firstA = students.first { it.startsWith("A") }         // Alice
val lastE = students.last { it.endsWith("e") }             // Charlie
val firstD = students.firstOrNull { it.startsWith("D") }   // Diana
val firstZ = students.firstOrNull { it.startsWith("Z") }   // null

println("First long name: $firstLongName")
println("Any short names: $anyShortName")

Collection Conversions

val numbers = listOf(1, 2, 3, 2, 1, 4, 3)

// Convert between collection types
val numberSet = numbers.toSet()           // Remove duplicates: [1, 2, 3, 4]
val backToList = numberSet.toList()       // Convert back to list
val mutableCopy = numbers.toMutableList() // Create mutable copy

// Convert to Map
val indexed = numbers.withIndex().associate { it.index to it.value }
val grouped = numbers.groupBy { it % 2 }  // Group by even/odd

println(numberSet)   // [1, 2, 3, 4]
println(indexed)     // {0=1, 1=2, 2=3, 3=2, 4=1, 5=4, 6=3}
println(grouped)     // {1=[1, 1, 3, 3], 0=[2, 2, 4]}

// Array conversions
val array = numbers.toTypedArray()
val intArray = numbers.toIntArray()
val listFromArray = array.toList()

Sequences for Large Collections

For large collections or complex operations, sequences provide lazy evaluation:

val largeNumbers = (1..1_000_000).toList()

// Eager evaluation (processes all elements immediately)
val eagerResult = largeNumbers
    .filter { it % 2 == 0 }
    .map { it * it }
    .take(10)

// Lazy evaluation with sequences (processes only what's needed)
val lazyResult = largeNumbers.asSequence()
    .filter { it % 2 == 0 }
    .map { it * it }
    .take(10)
    .toList()  // Terminal operation triggers evaluation

println(lazyResult) // [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

// Infinite sequence
val infiniteSequence = generateSequence(1) { it + 1 }
val firstTenSquares = infiniteSequence
    .map { it * it }
    .take(10)
    .toList()

println(firstTenSquares) // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Real-World Examples

Student Grade Management

data class Student(val name: String, val grades: List<Int>)

val students = listOf(
    Student("Alice", listOf(85, 92, 78, 96)),
    Student("Bob", listOf(79, 83, 88, 85)),
    Student("Charlie", listOf(95, 89, 92, 94)),
    Student("Diana", listOf(67, 74, 82, 79))
)

// Calculate averages
val averages = students.associate { student ->
    student.name to student.grades.average()
}

// Find top performers
val topStudents = students.filter { student ->
    student.grades.average() >= 85.0
}

// Grade distribution
val allGrades = students.flatMap { it.grades }
val gradeDistribution = allGrades.groupBy { grade ->
    when (grade) {
        in 90..100 -> "A"
        in 80..89 -> "B"
        in 70..79 -> "C"
        in 60..69 -> "D"
        else -> "F"
    }
}

println("Averages: $averages")
println("Top students: ${topStudents.map { it.name }}")
println("Grade distribution: $gradeDistribution")

Inventory Management

data class Product(val id: String, val name: String, val price: Double, val stock: Int)

val inventory = listOf(
    Product("P001", "Laptop", 999.99, 5),
    Product("P002", "Mouse", 29.99, 50),
    Product("P003", "Keyboard", 79.99, 25),
    Product("P004", "Monitor", 199.99, 10),
    Product("P005", "Headphones", 149.99, 0)
)

// Products in stock
val inStock = inventory.filter { it.stock > 0 }

// Low stock alerts (less than 10 items)
val lowStock = inventory.filter { it.stock in 1..9 }

// Out of stock
val outOfStock = inventory.filter { it.stock == 0 }

// Total inventory value
val totalValue = inventory.sumOf { it.price * it.stock }

// Price ranges
val priceRanges = inventory.groupBy { product ->
    when {
        product.price < 50 -> "Budget"
        product.price < 200 -> "Mid-range"
        else -> "Premium"
    }
}

println("Low stock items: ${lowStock.map { it.name }}")
println("Total inventory value: $${"%.2f".format(totalValue)}")
println("Price ranges: ${priceRanges.mapValues { it.value.size }}")

Best Practices

Performance Tips

  • Use sequences for large collections or complex operations
  • Prefer immutable collections when possible
  • Use appropriate collection types (Set for uniqueness, Map for lookups)
  • Consider using any() instead of filter().isNotEmpty()
  • Chain operations efficiently to avoid intermediate collections
Architecture Note: Kotlin's collection library is based on Java collections but adds many functional programming features. The distinction between mutable and immutable collections helps prevent bugs and makes code more predictable.

Practice Exercises

  1. Create a program that manages a library's book collection using different collection types
  2. Build a simple shopping cart system with products, quantities, and total calculations
  3. Implement a word frequency counter that processes a list of sentences
  4. Create a student roster system that can filter and sort students by various criteria
  5. Design a inventory system that tracks products, categories, and stock levels

Quick Quiz

  1. What's the difference between listOf() and mutableListOf()?
  2. How does a Set differ from a List?
  3. When should you use sequences instead of regular collections?
  4. What's the difference between map() and flatMap()?
Show answers
  1. listOf() creates an immutable list that cannot be modified, mutableListOf() creates a mutable list that can be modified
  2. A Set automatically removes duplicates and doesn't maintain order, while a List allows duplicates and maintains insertion order
  3. Use sequences for large collections or when you need lazy evaluation to avoid processing all elements unnecessarily
  4. map() transforms each element into one result element, flatMap() transforms each element into a collection and flattens the results