1. Why do you need Set if you already have List?
If List is like “a queue of people with numbered tickets”, then Set is like “a guest list at the entrance”, where only one thing matters: either the person is on the list, or they are not. In programming, this model shows up all the time: “allowed commands”, “unique tags”, “values already processed”, “don’t add again”. Set helps express this idea directly, rather than via hacks like “I’ll check the list before adding”.
From Kotlin’s point of view, Set<T> stores unique elements: the same value cannot exist inside the set “in two copies”. People also often say that order in a set is not part of the meaning. The documentation phrases it like this: Set stores unique elements, and their order is “usually undefined”.
Imagine you are building a small console app that stores expense records (we started this in the previous lecture using lists). At some point you will almost certainly want to:
- store the list of expenses (duplicates are allowed there: you can buy coffee 200 times);
- but also keep a unique set of categories that have already occurred (to show suggestions or validate input).
This is exactly a classic place for Set.
2. Set and MutableSet: reading vs. modifying
When you first see Set and MutableSet, it’s very tempting to think: “Ah, Set is like val, and MutableSet is like var”. And this is where Kotlin gives a sly smile.
Set<T> is a read-only interface: size, presence checks, iteration.
MutableSet<T> is a mutable interface: adding, removing.
So the difference is not whether you can change the variable, but which operations are available through the type. This style is the same as with List/MutableList.
One more important point: you almost always want to store a mutable set in a val. Then you won’t accidentally “rebind” the variable to another set, but you can still honestly mutate the contents via add/remove. This idea already came up in the overview of Kotlin collections: val protects the reference, but does not forbid write operations on a mutable collection.
Creating sets: setOf, mutableSetOf, empty sets
When you are just starting out, the most convenient way to create sets is with the standard library factory functions. They read like “create me a list/set right now”, without extra noise. And it fits nicely with the familiar style of listOf(...) and mutableListOf(...), which we already used for lists.
setOf(...) and “collapsing” duplicates
fun main() {
val tags = setOf("kotlin", "kotlin", "jvm")
println(tags) // [kotlin, jvm]
println(tags.size) // 2
}
What matters here is not the order in which the set prints, but the fact that the duplicate "kotlin" does not create a second element. Set is about uniqueness, not “how many times it occurred”.
Empty set: sometimes you need an explicit type
As with lists, an empty collection often needs a hint for the compiler: “what type of elements are you expecting here?”.
fun main() {
val usedCategories = mutableSetOf<String>()
usedCategories.add("food")
println(usedCategories) // [food]
}
If you try to write just mutableSetOf() without <String>, Kotlin may honestly answer: “I don’t know what should go in here.” And it will be right.
3. Basic set operations
Uniqueness as a contract: add() and the returned Boolean
In a list, add() almost always means “the element definitely got added (to the end)”. In a set, it’s more interesting: add(x) means “try to add, but if such an element already exists — change nothing”. That’s why the operation returns Boolean: whether a new element was actually added.
This is very handy when you want to “notify only once” or “avoid duplicating an action”.
fun main() {
val seen = mutableSetOf<Int>()
println(seen.add(10)) // true
println(seen.add(10)) // false
println(seen) // [10]
}
The second attempt did not add “another ten” — it is simply not needed in terms of the set’s meaning.
Membership checks: contains() and the in operator
The most “natural” operation for Set is the question: “Is the element here?”. In Kotlin you can do this in two equally correct ways: via contains(x) and via x in set. The second option usually reads nicer, especially in conditions.
And yes, this is one reason why sets are loved for “allowed/forbidden” checks: the code becomes almost conversational.
fun main() {
val allowedCommands = setOf("add", "list", "remove")
val cmd = "list"
println(cmd in allowedCommands) // true
}
At this point it’s helpful to keep this mental model: Set is a “set of allowed values”. It’s not about order, not about indexes, and definitely not about “remove element #3”.
Removal: remove() and what it returns
Removal in MutableSet is similar to remove(value) on a list, but it’s more natural in meaning: a set has no indexes, so removal is always “by value”. Just like add, it returns Boolean: whether anything was removed.
fun main() {
val usedCategories = mutableSetOf("food", "transport")
println(usedCategories.remove("food")) // true
println(usedCategories.remove("food")) // false
println(usedCategories) // [transport]
}
The second time there’s nothing to remove — "food" is already absent.
4. Order, equality, and “remove duplicates”
Can you rely on order in Set?
Intuitively you want to think: “Well, it prints like "[a, b, c]", so there is an order”. And Kotlin often does make it look like the order is stable. But it’s important to separate “how it happens to be implemented today” from “what the data structure means”.
Formally, Set is a set: unique elements, and order is “not the main point at all”.
At the same time, in the Kotlin standard library the default implementation of MutableSet is LinkedHashSet, and it preserves insertion order, so first() and last() can be predictable specifically with this implementation.
A practical beginner rule sounds like this: treat Set as a container where order is not part of the contract. If your logic breaks when elements are rearranged, you most likely need a different structure (usually a list).
“Remove duplicates”: toSet() as a quick uniqueness filter
One of the most common applied scenarios: you already have a list (or array) with duplicates, and you need a set of unique values. For example, the user entered categories inconsistently, and you want to get a “list of all unique categories”.
That’s what toSet() is for. It creates a new collection and “collapses” duplicates. Importantly, copy/convert functions like toSet() take a snapshot at the moment of the call: later changes to the source collection do not “magically” show up in the result.
fun main() {
val words = listOf("a", "b", "a", "c")
val uniqueWords = words.toSet()
println(uniqueWords) // [a, b, c]
println(uniqueWords.size) // 3
}
The same works for arrays: you can turn an array into a Set and see duplicates disappear, while converting to a List will keep them.
Set equality: elements matter more than order
For lists, equality depends on order: [1, 2] is not equal to [2, 1]. For sets, the logic is different: if the elements are the same, the sets are equal, even if you created them “in a different order”. This matches the mathematical meaning of a set and is useful in real life when you compare sets of permissions/tags/features.
In the Kotlin documentation this is shown with an example: sets are equal if they contain the same elements.
fun main() {
val a = setOf(1, 2, 3, 4)
val b = setOf(4, 3, 2, 1)
println(a == b) // true
}
5. Example: expense tracking and unique categories
Now we’ll continue our mini project of the day: “console expense tracking”, without classes and without complex architecture — just to build hands-on skill with collections. In the previous lecture, we stored expense records in a MutableList<Triple<String, String, Int>>, where the fields are category, description, and amount.
Now we’ll add one more structure: a MutableSet<String> for unique categories that have already occurred. This gives us two useful things: a fast answer to the question “was this category already used?” and the ability to get a “category list without duplicates” at any moment, without manually iterating through the whole list.
Mini data diagram
Let’s draw a very simple diagram so your brain doesn’t try to keep everything in memory (memory is expensive, especially on Monday morning):
flowchart LR
Records["records: MutableList⟨Triple⟨Category, Title, Amount⟩⟩"]
Categories["knownCategories: MutableSet⟨Category⟩"]
Records -->|"added a record"| Categories
The idea: each new record can “throw” a new category into the set. If the category already existed, the set simply won’t change.
Code: add a record and update the category set
Here we intentionally write small functions and short examples so the code reads like a textbook rather than “serious production” (it will catch up to you later).
fun addExpense(
records: MutableList<Triple<String, String, Int>>,
knownCategories: MutableSet<String>,
category: String,
title: String,
amount: Int
) {
records.add(Triple(category, title, amount))
knownCategories.add(category)
}
Notice: knownCategories.add(category) doesn’t break anything even if the category already existed. That’s the beauty of a set: it protects us from duplicates by contract.
Checking “new category or not” via add()
Now let’s use the fact that add returns Boolean to show the user (or ourselves during debugging) a helpful message: whether the category is new or not.
fun addCategoryIfNew(knownCategories: MutableSet<String>, category: String) {
val added = knownCategories.add(category)
if (added) {
println("New category: $category") // for example: New category: food
} else {
println("Category already existed: $category") // for example: Category already existed: food
}
}
A list cannot do this “out of the box”: with a list you would have to call contains separately, then add, and it’s easy to make a mistake (or simply forget).
Full mini example: records and categories
Let’s put together a small main that demonstrates the whole idea. It doesn’t claim to be a full UI — it’s a learning “sandbox” where we manually verify that the data structures behave as promised.
fun main() {
val records = mutableListOf<Triple<String, String, Int>>()
val knownCategories = mutableSetOf<String>()
addExpense(records, knownCategories, "food", "coffee", 300)
addExpense(records, knownCategories, "transport", "taxi", 1200)
addExpense(records, knownCategories, "food", "lunch", 550)
println(records.size) // 3
println(knownCategories) // [food, transport] (order is not part of the contract)
println("food" in knownCategories) // true
}
This makes it very clear: there are three records (two of them in the "food" category), but only two categories — because a set stores unique values.
6. Set and null: yes, you can — but be careful
Sometimes data arrives “dirty”: the category failed to parse, the user entered an empty string, and you haven’t cleaned it up yet. Since we already covered null-safety, it’s useful to know: Set can store null, but as a unique element — at most one null in the set. The documentation explicitly says that null in a Set is also unique: a second null will not appear.
This is not a call to store null everywhere you can (we are not collecting problems), but it’s useful knowledge for debugging and transitional states.
fun main() {
val categories: MutableSet<String?> = mutableSetOf()
categories.add(null)
categories.add(null)
println(categories.size) // 1
println(categories) // [null]
}
7. Common mistakes when working with Set and MutableSet
Mistake #1: expecting a set to store duplicates “like a list”.
Beginners often add the same value to a MutableSet and are surprised: “Why doesn’t the size grow?”. Because Set, by definition, stores unique elements. If you need to store “every user input”, even if it repeats, that’s a list task. And if you need to store “which values have occurred at all” — that’s a set task.
Mistake #2: writing logic that depends on the order of elements in a Set.
Sometimes everything looks as if there is an order, and your hand reaches to do “I’ll take the first element and treat it as the main one”. Kotlin often uses an implementation that preserves insertion order, but that is not what Set is about. Once you start relying on order, your code becomes fragile: the implementation changes, the data source changes, and the behavior unexpectedly drifts.
Mistake #3: trying to access a Set by index.
After List, it’s very tempting to write something like mySet[0]. But a set has no indexes as part of its model. That’s a signal you chose the wrong container for the job. If you need position — use List. If you need uniqueness and “present/absent” checks — use Set.
Mistake #4: forgetting that add/remove return Boolean, and losing useful information.
Many people just call add(x) and ignore the result, even though it’s an almost free hint about whether a change actually happened. When you validate input or maintain a list of “already seen values”, that Boolean helps you write simpler, clearer code: “if it was added for the first time — notify/do the action; otherwise — skip”.
Mistake #5: creating an empty mutableSetOf() without a type and getting a compilation error.
An empty collection gives the compiler no hints, so sometimes you need to explicitly specify the type: mutableSetOf<String>(). That’s normal and doesn’t make you a “worse programmer”. If anything, it’s the opposite: you explicitly fix the contract, and Kotlin stops guessing what you meant.
GO TO FULL VERSION