Kotlin Collection Filtering
Filtering is one of the most common operations performed on collections. Whether you're working with lists, sets, or maps, Kotlin provides a rich set of functions to filter collections efficiently and expressively.
Introduction to Collection Filtering
Filtering is the process of selecting elements from a collection that meet specific criteria. In Kotlin, filtering operations return a new collection containing only the elements that satisfy the given predicate (a condition that evaluates to true
or false
).
Kotlin's filtering capabilities are part of its functional programming features, making your code more concise and readable compared to traditional imperative approaches.
Basic Filtering with filter()
The most commonly used filtering function in Kotlin is filter()
. It creates a new collection containing only elements that satisfy the given predicate.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Filter even numbers
val evenNumbers = numbers.filter { it % 2 == 0 }
println("Original numbers: $numbers")
println("Even numbers: $evenNumbers")
}
Output:
Original numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Even numbers: [2, 4, 6, 8, 10]
In this example, filter { it % 2 == 0 }
creates a new list containing only even numbers from the original list.
Filtering with Negation using filterNot()
When you want to exclude elements that match a certain condition, you can use filterNot()
:
fun main() {
val fruits = listOf("apple", "banana", "cherry", "date", "elderberry")
// Filter fruits that don't start with 'a' or 'b'
val filteredFruits = fruits.filterNot { it.startsWith('a') || it.startsWith('b') }
println("All fruits: $fruits")
println("Filtered fruits: $filteredFruits")
}
Output:
All fruits: [apple, banana, cherry, date, elderberry]
Filtered fruits: [cherry, date, elderberry]
Filtering by Type with filterIsInstance()
When working with collections that contain different types of objects, you can filter by type using filterIsInstance()
:
fun main() {
val mixed = listOf(1, "two", 3.0, "four", 5, 6.0)
// Filter only String elements
val stringElements = mixed.filterIsInstance<String>()
// Filter only Int elements
val intElements = mixed.filterIsInstance<Int>()
println("Original collection: $mixed")
println("String elements: $stringElements")
println("Int elements: $intElements")
}
Output:
Original collection: [1, two, 3.0, four, 5, 6.0]
String elements: [two, four]
Int elements: [1, 5]
Filtering Non-Null Values with filterNotNull()
When dealing with collections that might contain null values, you can filter out those nulls:
fun main() {
val withNulls = listOf("apple", null, "banana", null, "cherry")
val nonNullValues = withNulls.filterNotNull()
println("With nulls: $withNulls")
println("Non-null values: $nonNullValues")
}
Output:
With nulls: [apple, null, banana, null, cherry]
Non-null values: [apple, banana, cherry]
Filtering Maps
Kotlin provides specialized filtering functions for maps, allowing you to filter based on keys, values, or both:
fun main() {
val scores = mapOf(
"Alice" to 92,
"Bob" to 76,
"Charlie" to 85,
"Diana" to 95,
"Edward" to 68
)
// Filter students with scores above 80
val highScorers = scores.filterValues { it > 80 }
// Filter students whose names start with 'A'
val aStudents = scores.filterKeys { it.startsWith('A') }
// Filter entries based on both key and value
val selectedStudents = scores.filter { (name, score) ->
name.length <= 5 && score >= 80
}
println("All scores: $scores")
println("High scorers: $highScorers")
println("Students with names starting with 'A': $aStudents")
println("Selected students (name <= 5 chars and score >= 80): $selectedStudents")
}
Output:
All scores: {Alice=92, Bob=76, Charlie=85, Diana=95, Edward=68}
High scorers: {Alice=92, Charlie=85, Diana=95}
Students with names starting with 'A': {Alice=92}
Selected students (name <= 5 chars and score >= 80): {Alice=92, Diana=95}
Partitioning Collections
Sometimes you want to divide a collection into two parts based on a condition. The partition()
function does exactly this:
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val (evenNumbers, oddNumbers) = numbers.partition { it % 2 == 0 }
println("Original numbers: $numbers")
println("Even numbers: $evenNumbers")
println("Odd numbers: $oddNumbers")
}
Output:
Original numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Even numbers: [2, 4, 6, 8, 10]
Odd numbers: [1, 3, 5, 7, 9]
Filtering with Indices using filterIndexed()
If you need access to the index of each element during filtering, use filterIndexed()
:
fun main() {
val fruits = listOf("apple", "banana", "cherry", "date", "elderberry")
// Keep elements at even indices
val evenIndexFruits = fruits.filterIndexed { index, _ -> index % 2 == 0 }
// Keep elements where index plus length is even
val specialFilter = fruits.filterIndexed { index, value -> (index + value.length) % 2 == 0 }
println("All fruits: $fruits")
println("Fruits at even indices: $evenIndexFruits")
println("Fruits where (index + length) is even: $specialFilter")
}
Output:
All fruits: [apple, banana, cherry, date, elderberry]
Fruits at even indices: [apple, cherry, elderberry]
Fruits where (index + length) is even: [apple, cherry, elderberry]
Real-World Example: Filtering Task List
Let's create a more practical example using a task management system:
data class Task(
val id: Int,
val title: String,
val isCompleted: Boolean,
val priority: Priority,
val dueDate: String? = null
)
enum class Priority { LOW, MEDIUM, HIGH }
fun main() {
val tasks = listOf(
Task(1, "Complete Kotlin assignment", false, Priority.HIGH, "2023-10-15"),
Task(2, "Buy groceries", false, Priority.MEDIUM, "2023-10-10"),
Task(3, "Pay utility bills", true, Priority.HIGH, "2023-10-05"),
Task(4, "Call mom", false, Priority.LOW),
Task(5, "Schedule dentist appointment", false, Priority.MEDIUM),
Task(6, "Clean the garage", true, Priority.LOW, "2023-10-20")
)
// Filter incomplete high-priority tasks
val urgentTasks = tasks.filter { it.priority == Priority.HIGH && !it.isCompleted }
// Filter tasks with due dates
val scheduledTasks = tasks.filter { it.dueDate != null }
// Filter completed tasks
val completedTasks = tasks.filter { it.isCompleted }
// Print the tasks
println("All tasks:")
tasks.forEach { println("- ${it.title} (${it.priority}, ${if (it.isCompleted) "Completed" else "Pending"})") }
println("\nUrgent pending tasks:")
urgentTasks.forEach { println("- ${it.title} (due: ${it.dueDate})") }
println("\nScheduled tasks:")
scheduledTasks.forEach { println("- ${it.title} (due: ${it.dueDate})") }
println("\nCompleted tasks:")
completedTasks.forEach { println("- ${it.title}") }
// Count tasks by priority
val tasksByPriority = tasks.groupingBy { it.priority }.eachCount()
println("\nTask count by priority: $tasksByPriority")
}
Output:
All tasks:
- Complete Kotlin assignment (HIGH, Pending)
- Buy groceries (MEDIUM, Pending)
- Pay utility bills (HIGH, Completed)
- Call mom (LOW, Pending)
- Schedule dentist appointment (MEDIUM, Pending)
- Clean the garage (LOW, Completed)
Urgent pending tasks:
- Complete Kotlin assignment (due: 2023-10-15)
Scheduled tasks:
- Complete Kotlin assignment (due: 2023-10-15)
- Buy groceries (due: 2023-10-10)
- Pay utility bills (due: 2023-10-05)
- Clean the garage (due: 2023-10-20)
Completed tasks:
- Pay utility bills
- Clean the garage
Task count by priority: {HIGH=2, MEDIUM=2, LOW=2}
Chaining Filters
One of the strengths of Kotlin's functional approach is the ability to chain operations:
fun main() {
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
val result = numbers
.filter { it > 5 } // Numbers greater than 5
.filter { it % 2 == 0 } // Even numbers
.filter { it <= 12 } // Numbers not exceeding 12
println("Original numbers: $numbers")
println("Result after chained filters: $result")
// More efficient approach using a single filter
val efficientResult = numbers.filter { it > 5 && it % 2 == 0 && it <= 12 }
println("Result with single filter: $efficientResult")
}
Output:
Original numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
Result after chained filters: [6, 8, 10, 12]
Result with single filter: [6, 8, 10, 12]
While both approaches yield the same result, using a single filter is generally more efficient as it avoids creating intermediate collections.
Performance Considerations
When filtering large collections, consider these performance tips:
- Use sequences for large collections: Convert to a sequence before filtering to process elements lazily
val result = numbers.asSequence()
.filter { it > 10 }
.filter { it % 2 == 0 }
.toList()
-
Combine multiple filter conditions into a single operation when possible
-
Consider using
filterTo()
to append filtered elements to an existing mutable collection:
val evenNumbers = mutableListOf<Int>()
numbers.filterTo(evenNumbers) { it % 2 == 0 }
Summary
Kotlin provides a rich set of filtering functions that make working with collections intuitive and expressive:
filter()
andfilterNot()
for basic inclusion/exclusion- Type-specific filters like
filterIsInstance()
andfilterNotNull()
- Map-specific filters:
filterKeys()
,filterValues()
, andfilter()
partition()
for splitting collectionsfilterIndexed()
for accessing indices during filtering
These functions follow functional programming principles, producing new collections rather than modifying existing ones, which helps write more predictable and maintainable code.
Exercises
- Create a list of integers from 1 to 20 and filter out all numbers that are not prime.
- Given a list of strings, filter out all strings that are not palindromes.
- Create a list of people (name, age, city) and filter those who are adults (18+) and live in a specific city.
- Given a map of employee names to salaries, filter employees who earn above average.
- Create a list of mixed data types and use
filterIsInstance
to extract only the numeric values.
Additional Resources
- Kotlin Collections Documentation
- Functional Programming in Kotlin
- Kotlin Sequences for processing large collections efficiently
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)