Choosing the right data structure is one of the most impactful decisions you make as a developer. Use the wrong one and you'll write more code, hit performance bottlenecks, and introduce subtle bugs. Use the right one and your code becomes cleaner, faster, and more expressive.
Kotlin comes with a rich set of built-in data structures out of the box. In this guide you'll learn each one — Arrays, Lists, Maps, Sets, Stacks, Queues, and Trees — with practical Kotlin examples and real Android use cases.
Quick Reference — Which Data Structure to Use?
| Structure | Ordered? | Duplicates? | Key Access? | Best For |
|---|---|---|---|---|
| Array | ✅ Yes | ✅ Yes | Index | Fixed-size, fast index access |
| List | ✅ Yes | ✅ Yes | Index | Dynamic collections, RecyclerView data |
| Map | ⚠️ Depends | Values yes, keys no | Key | Lookup tables, caching, settings |
| Set | ❌ No | ❌ No | — | Unique items, deduplication |
| Stack | ✅ LIFO | ✅ Yes | Top only | Undo history, back stack, parsing |
| Queue | ✅ FIFO | ✅ Yes | Front only | Task queues, message passing |
| Tree | ✅ Hierarchical | ✅ Yes | — | Hierarchical data, file systems |
1. Arrays
Arrays store a fixed-size sequential collection of elements of the same type. Once created, the size cannot change. Use arrays when you know the exact number of elements upfront and need fast index-based access.
Android use case: Storing a fixed list of tab titles, pixel data for image processing, or predefined color palettes.
// Fixed-size integer array
val scores = IntArray(5)
scores[0] = 10; scores[1] = 20; scores[2] = 30
// Declare and initialize in one line
val colors = intArrayOf(0xFF0000, 0x00FF00, 0x0000FF)
// Array of strings
val tabTitles = arrayOf("Home", "Search", "Profile")
// Iterate
for (title in tabTitles) println(title)
// Access by index
val firstTab = tabTitles[0] // "Home"
2. Lists
Lists are dynamic — they grow and shrink as needed. Kotlin has two flavours: listOf() (immutable) and mutableListOf() (mutable). Lists are the most common data structure in Android development.
Android use case: Feeding data to a RecyclerView adapter, storing search results, managing a to-do list.
// Immutable list — cannot add/remove elements
val fruits = listOf("apple", "banana", "orange")
// Mutable list — can add/remove elements
val items = mutableListOf("Item 1", "Item 2")
items.add("Item 3") // Add to end
items.add(0, "Item 0") // Add at index
items.remove("Item 2") // Remove by value
items.removeAt(0) // Remove by index
// Common operations
val size = items.size
val first = items.first()
val last = items.last()
val filtered = items.filter { it.contains("1") }
val sorted = items.sorted()
// Iterating
items.forEach { println(it) }
items.forEachIndexed { index, value -> println("$index: $value") }
3. Maps
Maps store key-value pairs. Each key is unique but values can repeat. Use mapOf() for immutable and mutableMapOf() for mutable maps. For ordered maps, use LinkedHashMap.
Android use case: Caching API responses by ID, storing user preferences, mapping error codes to messages.
// Immutable map
val countryCode = mapOf("US" to "United States", "IN" to "India", "GB" to "UK")
// Mutable map
val userPrefs = mutableMapOf<String, Any>()
userPrefs["theme"] = "dark"
userPrefs["fontSize"] = 16
userPrefs["notifications"] = true
// Access — returns null if key not found
val theme = userPrefs["theme"] // "dark"
val missing = userPrefs["unknown"] // null
// Safe access with default
val fontSize = userPrefs.getOrDefault("fontSize", 14)
// Update and remove
userPrefs["theme"] = "light" // Update
userPrefs.remove("notifications") // Remove
// Iterate
userPrefs.forEach { (key, value) -> println("$key = $value") }
// Check key/value existence
val hasTheme = "theme" in userPrefs // true
val hasValue = userPrefs.containsValue(16) // true
4. Sets
Sets store unique elements only — duplicates are automatically ignored. They are unordered. Use setOf() for immutable and mutableSetOf() for mutable sets.
Android use case: Tracking which items a user has already seen, storing unique tags, deduplicating push notification IDs.
// Immutable set — duplicates ignored automatically
val uniqueIds = setOf(1, 2, 3, 2, 1) // Result: {1, 2, 3}
// Mutable set
val selectedItems = mutableSetOf<String>()
selectedItems.add("item_1")
selectedItems.add("item_2")
selectedItems.add("item_1") // Ignored — already exists
println(selectedItems.size) // 2
// Check membership — O(1) for HashSet
val isSelected = "item_1" in selectedItems // true
// Set operations
val set1 = setOf(1, 2, 3, 4)
val set2 = setOf(3, 4, 5, 6)
val union = set1 union set2 // {1, 2, 3, 4, 5, 6}
val intersection = set1 intersect set2 // {3, 4}
val difference = set1 subtract set2 // {1, 2}
5. Stacks (LIFO)
A Stack follows Last-In-First-Out (LIFO) — the last element added is the first one removed. Use ArrayDeque in Kotlin (preferred over the legacy Stack class).
Android use case: Implementing undo/redo functionality, managing a custom back stack, expression parsing.
// Use ArrayDeque as a Stack (LIFO)
val stack = ArrayDeque<String>()
// Push — add to top
stack.addLast("Action 1")
stack.addLast("Action 2")
stack.addLast("Action 3")
// Peek — view top without removing
val top = stack.last() // "Action 3"
// Pop — remove from top
val popped = stack.removeLast() // "Action 3"
println(stack) // [Action 1, Action 2]
// Check size
val isEmpty = stack.isEmpty()
// Android example: Undo history
val undoStack = ArrayDeque<() -> Unit>()
undoStack.addLast { /* undo delete */ }
undoStack.addLast { /* undo edit */ }
fun undo() {
if (undoStack.isNotEmpty()) undoStack.removeLast().invoke()
}
6. Queues (FIFO)
A Queue follows First-In-First-Out (FIFO) — the first element added is the first one removed. Use ArrayDeque or LinkedList in Kotlin.
Android use case: Task queues for background processing, message passing between threads, download queue management.
// Use ArrayDeque as a Queue (FIFO)
val queue = ArrayDeque<String>()
// Enqueue — add to back
queue.addLast("Task 1")
queue.addLast("Task 2")
queue.addLast("Task 3")
// Peek — view front without removing
val front = queue.first() // "Task 1"
// Dequeue — remove from front
val processed = queue.removeFirst() // "Task 1"
println(queue) // [Task 2, Task 3]
// Android example: Download queue
val downloadQueue = ArrayDeque<String>()
downloadQueue.addLast("image_1.jpg")
downloadQueue.addLast("image_2.jpg")
fun processNextDownload() {
if (downloadQueue.isNotEmpty()) {
val file = downloadQueue.removeFirst()
// Start downloading file
}
}
7. Trees
Trees are hierarchical data structures where each node has a parent and zero or more children. Kotlin doesn't have a built-in tree class — you define your own node structure. The most common is a Binary Tree.
Android use case: Representing folder/file structures, XML/JSON parsing trees, category hierarchies in e-commerce apps.
// Generic tree node
data class TreeNode<T>(
val value: T,
val children: MutableList<TreeNode<T>> = mutableListOf()
) {
fun addChild(child: TreeNode<T>) = children.add(child)
}
// Build a category tree
val root = TreeNode("Electronics")
val phones = TreeNode("Phones")
val laptops = TreeNode("Laptops")
phones.addChild(TreeNode("Android"))
phones.addChild(TreeNode("iPhone"))
laptops.addChild(TreeNode("MacBook"))
laptops.addChild(TreeNode("Windows"))
root.addChild(phones)
root.addChild(laptops)
// Depth-first traversal
fun <T> printTree(node: TreeNode<T>, depth: Int = 0) {
println("${" ".repeat(depth)}${node.value}")
node.children.forEach { printTree(it, depth + 1) }
}
printTree(root)
// Electronics
// Phones
// Android
// iPhone
// Laptops
// MacBook
// Windows
Best Practices
- Prefer immutable by default — use
listOf(),mapOf(),setOf()unless you specifically need to modify the collection. Immutable collections prevent accidental modifications and are safer in multi-threaded code. - Use MutableList over ArrayList —
mutableListOf()is the idiomatic Kotlin way. It returns anArrayListunder the hood but keeps your code Kotlin-style. - Use ArrayDeque over Stack and LinkedList —
ArrayDequeis the modern Kotlin recommendation for both stack and queue operations. The legacyjava.util.Stackis synchronized and slower. - Use Set for membership checks — checking
"item" in setis O(1) forHashSet, compared to O(n) for aList. If you frequently check "does this item exist", use a Set. - Use Map for O(1) lookups — if you find yourself iterating a list to find an item by ID, switch to a Map.
map[id]is instant;list.find { it.id == id }is linear.
Frequently Asked Questions
What is the difference between List and Array in Kotlin?
Arrays have a fixed size set at creation time and cannot grow or shrink. Lists are dynamic and can be resized. For most Android use cases, prefer List over Array. Use Array when you need primitive performance (IntArray, FloatArray) or interop with Java APIs.
What is the difference between List and MutableList in Kotlin?
List is read-only — you cannot add, remove, or modify elements after creation. MutableList allows add(), remove(), and set() operations. Always prefer the read-only List in function signatures and expose MutableList only where modification is needed.
Which data structure should I use for a RecyclerView adapter?
Use a List<YourModel> in your adapter. For efficient updates, use DiffUtil with ListAdapter which only redraws changed items. Store the data in a ViewModel as MutableStateFlow<List<YourModel>> and expose it as StateFlow.
Is HashMap faster than LinkedHashMap in Kotlin?
HashMap is slightly faster for lookups since it doesn't maintain insertion order. LinkedHashMap maintains insertion order at a small memory cost. Use HashMap when order doesn't matter, LinkedHashMap when you need predictable iteration order.
- Array — fixed size, fast index access, use for primitives
- List — dynamic, ordered, allows duplicates — most common in Android
- Map — key-value pairs, O(1) lookups, great for caching
- Set — unique elements only, O(1) membership check
- Stack (ArrayDeque) — LIFO, use for undo history and back stack
- Queue (ArrayDeque) — FIFO, use for task queues and message passing
- Tree — hierarchical, define custom node class
- Prefer immutable collections by default — use mutable only when needed