Sets
A Set is a collection that stores unique elements with no duplicates, unlike a List which happily holds repeated values. Kotlin sets are built on the same Collection hierarchy as lists and maps, but uniqueness is enforced automatically using each element’s equals() and hashCode(). Sets are the tool of choice whenever you care about membership (“is this item present?”) or need to de-duplicate data, and Kotlin gives you several flavors depending on whether you need insertion order, sorted order, or raw speed.
Overview: What Is a Set?
In Kotlin, Set<T> is a read-only interface (part of kotlin.collections) that guarantees no two elements are equal according to equals(). Its mutable counterpart, MutableSet<T>, extends it with add(), remove(), and clear(). This mirrors the same read-only vs. mutable split you see with List and Map: a plain Set reference cannot be modified through that reference, even if the underlying object is actually mutable underneath.
Kotlin doesn’t have its own set implementation from scratch — on the JVM, setOf() and mutableSetOf() are backed by java.util.LinkedHashSet, which preserves insertion order. hashSetOf() is backed by java.util.HashSet, which offers no ordering guarantee at all (its iteration order depends on hash codes and internal bucket layout, and can change between runs or JDK versions). sortedSetOf() is backed by java.util.TreeSet, which keeps elements in ascending sorted order (using natural ordering via Comparable, or a supplied Comparator).
Under the hood, adding an element to a hash-based set works by computing hashCode() to pick a bucket, then comparing candidates in that bucket with equals() to check whether the element is already present. If an equal element already exists, add() is a no-op and returns false; otherwise the element is inserted and add() returns true. This is exactly why equals() and hashCode() must be consistent — if two objects are equals()-equal but report different hash codes, a hash-based set can end up storing both as if they were distinct. Kotlin’s data class generates both methods correctly and consistently for you, which is why data classes and sets pair so naturally (see Common Mistakes below for what goes wrong with a plain class).
Syntax
The general forms for creating sets:
val a: Set<Int> = setOf(1, 2, 3)
val b: MutableSet<Int> = mutableSetOf(1, 2, 3)
val c: HashSet<String> = hashSetOf("x", "y")
val d: LinkedHashSet<String> = linkedSetOf("x", "y")
val e: Set<Int> = sortedSetOf(3, 1, 2)
println(e)
| Function | Backing type | Iteration order | Mutable? |
|---|---|---|---|
setOf(...) |
LinkedHashSet | Insertion order | No (read-only view) |
mutableSetOf(...) |
LinkedHashSet | Insertion order | Yes |
hashSetOf(...) |
HashSet | Unspecified | Yes |
linkedSetOf(...) |
LinkedHashSet | Insertion order | Yes |
sortedSetOf(...) |
TreeSet | Sorted (ascending) | Yes |
Every one of these also has an empty-argument form (e.g. mutableSetOf<String>()) and every set type implements Iterable<T>, so all the familiar collection functions — map, filter, forEach, count, any, all, and so on — work on sets exactly as they do on lists.
Examples
Example 1: Duplicates Are Silently Ignored
fun main() {
val numbers = mutableSetOf(1, 2, 3)
numbers.add(2)
numbers.add(4)
println(numbers)
println("Size: ${numbers.size}")
}
Output:
[1, 2, 3, 4]
Size: 4
Adding 2 again does nothing — it’s already present, so the set’s size stays put — while adding 4 appends a genuinely new element. Because mutableSetOf is backed by a LinkedHashSet, the printed order matches insertion order even though sets are conceptually unordered collections of unique values.
Example 2: Set Operations (Union, Intersect, Subtract)
fun main() {
val setA = setOf(1, 2, 3, 4)
val setB = setOf(3, 4, 5, 6)
val unionSet = setA union setB
val intersectSet = setA intersect setB
val differenceSet = setA subtract setB
println("Union: $unionSet")
println("Intersect: $intersectSet")
println("Difference: $differenceSet")
}
Output:
Union: [1, 2, 3, 4, 5, 6]
Intersect: [3, 4]
Difference: [1, 2]
These three infix functions come straight from mathematical set theory. union combines both sets; intersect keeps only elements present in both; subtract removes from the left set anything found in the right set. All three return a new Set and leave setA and setB untouched.
Example 3: De-duplicating a List of Data Objects
data class User(val id: Int, val name: String)
fun main() {
val users = listOf(
User(1, "Alice"),
User(2, "Bob"),
User(1, "Alice"),
User(3, "Cara")
)
val uniqueUsers = users.toSet()
println("Original size: ${users.size}")
println("Unique size: ${uniqueUsers.size}")
uniqueUsers.forEach { println(it) }
}
Output:
Original size: 4
Unique size: 3
User(id=1, name=Alice)
User(id=2, name=Bob)
User(id=3, name=Cara)
Calling toSet() on a List is one of the most common uses of sets: turning a list that may contain duplicates into a collection of distinct values. Because User is a data class, its generated equals()/hashCode() compare by field values, so the second User(1, "Alice") is correctly recognized as a duplicate of the first and dropped. Order is preserved based on first occurrence, since toSet() also builds on LinkedHashSet.
How Sets Work Step by Step
When you call add(element) on a hash-based mutable set:
- Kotlin calls
element.hashCode()to determine which internal bucket the element belongs to. - It scans the existing elements already in that bucket, comparing each with
element.equals(existing). - If an equal element is found,
add()returnsfalseand nothing changes. - If no equal element is found, the new element is inserted into the bucket, the set’s size increases, and
add()returnstrue.
A LinkedHashSet does exactly the same hashing and equality work, but additionally threads every entry through a doubly linked list that records insertion order — that’s the extra bookkeeping that makes iteration order predictable. A TreeSet (behind sortedSetOf) skips hashing entirely and instead walks a balanced binary tree, comparing elements with compareTo() to decide where each one belongs; this is also why every element in a sorted set must implement Comparable, or you must supply a Comparator.
Common Mistakes
1. Using a plain class instead of a data class
A regular class inherits the default equals() from Any, which checks reference identity, not field values. Two separately-constructed instances with identical fields are treated as different elements.
class Point(val x: Int, val y: Int)
fun main() {
val points = mutableSetOf<Point>()
points.add(Point(1, 1))
points.add(Point(1, 1))
println(points.size)
}
Output:
2
The fix is to make it a data class, which generates a value-based equals()/hashCode() pair automatically:
data class Point(val x: Int, val y: Int)
fun main() {
val points = mutableSetOf<Point>()
points.add(Point(1, 1))
points.add(Point(1, 1))
println(points.size)
}
Output:
1
2. Calling add() on a read-only Set
setOf() returns type Set<T>, which has no add() method — this fails at compile time, not runtime:
val numbers = setOf(1, 2, 3)
numbers.add(4)
Output:
Compilation error: Unresolved reference 'add'. setOf() returns a read-only Set; use mutableSetOf() if the set needs to change after creation.
3. Trying to reassign a val set
val fixes the reference, not the contents. Beginners sometimes try to “reset” a set by reassigning it, which won’t compile:
val allowedRoles = mutableSetOf("admin", "editor")
allowedRoles = mutableSetOf("admin")
Output:
Compilation error: Val cannot be reassigned.
Since the set itself is mutable, the correct fix is to mutate its contents in place rather than replace the reference:
fun main() {
val allowedRoles = mutableSetOf("admin", "editor")
allowedRoles.clear()
allowedRoles.add("admin")
println(allowedRoles)
}
Output:
[admin]
Best Practices
- Default to
setOf()for read-only sets andmutableSetOf()only when you genuinely need to add or remove elements later. - Use
data classfor any type you plan to store in a set (or use as a map key) so uniqueness works correctly out of the box. - Reach for
hashSetOf()when you only care about fast membership checks and don’t need any particular iteration order. - Reach for
linkedSetOf()(or the defaultsetOf/mutableSetOf) when insertion order matters for display or logging. - Reach for
sortedSetOf()when you need elements automatically kept in sorted order, and make sure the element type isComparableor supply aComparator. - Use
list.toSet()as the idiomatic way to de-duplicate a list; uselist.distinct()when you want the result back as aListinstead. - Prefer
in(which callscontains()) for membership checks —if (x in mySet)reads more naturally thanif (mySet.contains(x)). - Never rely on
HashSetiteration order in tests or output formatting — it is not guaranteed and can differ across runs.
Practice Exercises
- Write a program that reads a list of words (you can hardcode a
List<String>with repeats) and prints only the distinct words, in the order they first appeared. - Given two sets of student names who took Math and Science respectively, use
intersectto print students who took both subjects, andsubtractto print students who took only Math. - Define a
data class Coordinate(val x: Int, val y: Int), add several coordinates — including duplicates — to amutableSetOf<Coordinate>(), and print the final size. Then remove thedatakeyword and predict (then explain) how the size changes.
Summary
- A
Setstores unique elements; duplicates (byequals()) are silently ignored on insertion. setOf()/mutableSetOf()andlinkedSetOf()preserve insertion order viaLinkedHashSet;hashSetOf()gives no ordering guarantee;sortedSetOf()keeps ascending sorted order viaTreeSet.- Uniqueness relies on consistent
equals()/hashCode()— usedata classfor elements you plan to store in sets. union,intersect, andsubtractimplement standard set-theory operations without mutating the originals.valprevents reassigning the set reference, not mutating a mutable set’s contents.list.toSet()is the idiomatic way to de-duplicate a list while preserving first-occurrence order.
