Lists

Lists in Kotlin

1. Introduction

Lists

Lists are one of the most commonly used data structures in programming. They provide an ordered collection of elements, making it easy to store and retrieve data by maintaining a sequence. Kotlin takes the concept of lists further with a concise and expressive syntax, immutability by default, and extensive support for functional programming.

2. Overview of Lists

Definition and Purpose

A list is an ordered collection of elements, where each element can be accessed by its position (index). Lists in Kotlin are part of the kotlin.collections package and come in two primary flavors:

  • List<T>: A read-only list.
  • MutableList<T>: A list that supports modification operations such as adding, removing, or updating elements.

Key Characteristics

  • Ordering: Lists maintain the order of elements.
  • Duplication: Lists allow duplicate elements.
  • Index-Based Access: Elements can be accessed via zero-based indices.

Common Scenarios

  • Maintaining an ordered collection of items (e.g., usernames, product IDs).
  • Storing data that requires frequent retrieval by index.
  • Iterating over a sequence of elements.

3. Kotlin Implementations

Types of Lists

1. Immutable List (List<T>)

An immutable list is read-only and cannot be modified after its creation.

val fruits = listOf("Apple", "Banana", "Cherry")

Here you can run this code in Kotlin Playground:

2. Mutable List (MutableList<T>)

A mutable list supports adding, removing, or updating elements.

val fruits = mutableListOf("Apple", "Banana", "Cherry")
fruits.add("Dragonfruit")
fruits.remove("Banana")

Here you can run this code in Kotlin Playground:

Features Specific to Kotlin Lists

  • Null Safety: Kotlin lists can hold null elements if declared accordingly.

    val nullableList: List<String?> = listOf("Apple", null, "Cherry")
    
  • Extension Functions: Lists in Kotlin come with a rich set of extension functions for filtering, mapping, sorting, and more.


4. How to Use Lists

Creating and Initializing Lists

Immutable Lists

listOf() creates a read-only list, which means its elements cannot be modified once created.

val list = listOf(1, 2, 3, 4, 5)
println(list)  // Output: [1, 2, 3, 4, 5]

Here is Kotlin Playground to run the code:

In this example, list is a read-only list containing 5 integers.

You can also create read-only lists of other types, such as String or custom objects:

val stringList = listOf("Kotlin", "Java", "Python")
println(stringList)  // Output: [Kotlin, Java, Python]

data class Student(val name: String, val grade: Int)
val studentList = listOf(Student("Alice", 90), Student("Bob", 85))
println(studentList)  // Output: [Student(name=Alice, grade=90), Student(name=Bob, grade=85)]

Here is Kotlin Playground to run the code:

Mutable Lists

mutableListOf() creates a mutable list, meaning the list can be modified (i.e., you can add, remove, or change elements).

val mutableList = mutableListOf(1, 2, 3)
mutableList.add(4)  // Adding an element
println(mutableList)  // Output: [1, 2, 3, 4]

Here, mutableList is created with 3 elements, and we added one more element (4).

Try this code in Kotlin Playground:

A mutable list can also start as an empty list and have elements added later:

val emptyMutableList = mutableListOf<String>()
emptyMutableList.add("Hello")
emptyMutableList.add("World")
emptyMutableList.add("This")
emptyMutableList.add("is")
emptyMutableList.add("Kotlin")
println(emptyMutableList)  // Output: [Hello, World, This, is, Kotlin]

Here is Kotlin Playground to run the code:

You can create a mutable list of custom objects as well:

val studentMutableList = mutableListOf<Student>()
studentMutableList.add(Student("Charlie", 88))
println(studentMutableList)  // Output: [Student(name=Charlie, grade=88)]

Empty Lists

val emptyList = emptyList<String>()
val emptyMutableList = mutableListOf<String>()

Accessing Elements

To access the elements of a list, you use the index of the element, which starts at 0. This is the same as arrays.

val list = listOf(10, 20, 30, 40, 50)
println(list[0])  // Output: 10 (first element)
println(list[4])  // Output: 50 (last element)

Lists also provide the get() method to access an element by index:

println(list.get(2))  // Output: 30

Updating Elements (MutableList Only)

cities[1] = "Paris"

Adding and Removing Elements (MutableList Only)

  • Read-only lists (List<T>) do not support modification; they are immutable. To modify the list, you need to use mutable lists (MutableList<T>).
val mutableList = mutableListOf(10, 20, 30)
mutableList[0] = 100  // Modifying an element
mutableList.add(40)   // Adding an element
mutableList.remove(20)  // Removing an element
println(mutableList)  // Output: [100, 30, 40]

In this example, we modified, added, and removed elements from the mutable list.

Here is Kotlin Playground to run the code:

Note on MutableList Methods:

  • add(): Adds a new element to the list.
  • remove(): Removes the first occurrence of the specified element.
  • removeAt(): Removes the element at the specified index.
  • clear(): Clears the list, removing all elements.

Checking if an Element Exists

You can check if an element exists in a list using the contains() function:

println(list.contains(30))  // Output: true
println(list.contains(60))  // Output: false

Iterating Over a List

Using for Loop

for (city in cities) {
    println(city)
}

Using forEach

cities.forEach { println(it) }

Here is Kotlin Playground to run the code:


5. List Extensions

1. sum()

The sum() function calculates the sum of all numeric elements in the list (available for List<Int>, List<Double>, etc.).

val list = listOf(1, 2, 3, 4, 5)
println(list.sum())  // Output: 15

2. shuffled()

The shuffled() function returns a new list with the elements randomly shuffled.

val list = listOf(1, 2, 3, 4, 5)
val shuffledList = list.shuffled()
println(shuffledList)  // Output: [3, 1, 5, 4, 2] (random order)

3. toSet()

Converts the list to a set, which removes duplicate elements.

val list = listOf(1, 2, 3, 2, 1)
val set = list.toSet()
println(set)  // Output: [1, 2, 3]

4. toMap()

Converts a list of pairs into a map, where the first element of each pair becomes the key, and the second element becomes the value.

val list = listOf("a" to 1, "b" to 2, "c" to 3)
val map = list.toMap()
println(map)  // Output: {a=1, b=2, c=3}

5. filter()

The filter() function creates a new list containing only the elements that satisfy a given condition.

val list = listOf(1, 2, 3, 4, 5)
val evenNumbers = list.filter { it % 2 == 0 }
println(evenNumbers)  // Output: [2, 4]

6. map()

The map() function transforms each element in the list and returns a new list with the transformed elements.

val list = listOf(1, 2, 3, 4, 5)
val doubledList = list.map { it * 2 }
println(doubledList)  // Output: [2, 4, 6, 8, 10]

7. first()

The first() function returns the first element of the list.

val list = listOf(1, 2, 3, 4, 5)
println(list.first())  // Output: 1

8. last()

The last() function returns the last element of the list.

val list = listOf(1, 2, 3, 4, 5)
println(list.last())  // Output: 5

An example of using these functions together in Kotlin Playground:


6. Specialized or Variant Types

Specialized Lists

Kotlin provides specialized implementations of lists tailored for different use cases. Two common types are ArrayList and LinkedList.

1. ArrayList

  • Description: Backed by a dynamically resizable array, ArrayList provides fast random access to elements and is part of the Kotlin collections framework.

  • Performance:

    • Access: O(1) for getting or setting elements by index.
    • Insert/Delete: O(n) for adding or removing elements at arbitrary positions (shifting required).
  • Use Case: Ideal when frequent random access is needed, such as in search operations or iteration through data.

  • Example:

    val arrayList = arrayListOf("Apple", "Banana", "Cherry")
    arrayList.add("Date")
    println(arrayList) // Output: [Apple, Banana, Cherry, Date]
    
  • Advantages:

    • Efficient for accessing elements by index.
    • Automatically resizes when the capacity is exceeded.
  • Limitations:

    • Costly for insertions or deletions in the middle of the list due to the need for shifting elements.

2. LinkedList

  • Description: A doubly linked list implementation, typically used via Java’s LinkedList class in Kotlin.

  • Performance:

    • Access: O(n) for retrieving elements by index.
    • Insert/Delete: O(1) for adding or removing elements at the beginning or end.
  • Use Case: Ideal for scenarios where frequent insertions or deletions occur, such as maintaining a queue or stack-like behavior.

  • Example:

    val linkedList = java.util.LinkedList<String>()
    linkedList.add("Kotlin")
    linkedList.addFirst("Java")
    linkedList.addLast("Python")
    println(linkedList) // Output: [Java, Kotlin, Python]
    
  • Advantages:

    • Efficient for operations at the head or tail of the list.
    • Suitable for use as a queue or stack.
  • Limitations:

    • Slower random access compared to ArrayList due to traversal requirements.
    • Higher memory overhead because of additional pointers for each node.

When to Use Each Type

  • ArrayList:

    • Use when fast random access and iteration are required.
    • Suitable for read-intensive applications where modifications are less frequent.
  • LinkedList:

    • Use when the data structure requires frequent insertions and deletions, especially at the ends.
    • Suitable for applications requiring queue or stack-like behavior.

7. Performance and Best Practices

When to Use Lists

  • When maintaining order is critical.
  • When duplicates are acceptable.
  • When you need to access elements by index.

Performance Considerations

  • Random Access: List provides O(1) access by index for array-backed implementations.
  • Adding/Removing Elements: Performance depends on the implementation (e.g., ArrayList vs LinkedList).

Best Practices

  1. Prefer Immutability: Use List wherever possible for safer and predictable code.
  2. Use Extension Functions: Simplify common tasks with functions like filter, map, and sorted.
  3. Be Specific with Nullability: Explicitly declare lists as nullable only if necessary.

8. Code Examples

Example 1: Filtering Elements

Example 2: Mapping Elements

Example 3: Sorting a Mutable List

Example 4: Checking for Element Existence

Example 5: Managing a To-Do List

Example 6: Grouping Students by Grade

Example 7: Pairing Products with Prices

Example 8: Splitting a Team into Groups


9. Similar Data Structures

List vs Array

  • Array: Fixed size, better for performance-critical operations with primitives.
  • List: Dynamic sizing, richer API for functional operations.

List vs Set

  • List: Allows duplicates and maintains order.
  • Set: Ensures uniqueness of elements but doesn’t guarantee order (unless using LinkedHashSet).

10. Conclusion

Kotlin’s list implementations are powerful and flexible, offering solutions for both immutable and mutable collections. By leveraging Kotlin’s functional programming features and extension functions, you can write concise, expressive, and efficient code. Understanding when to use List or MutableList ensures you can choose the right tool for the job.

Experiment with Kotlin lists and discover their potential to simplify your programming tasks!

11. Additional Resources & References

12. Me