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() returns false and nothing changes.
  • If no equal element is found, the new element is inserted into the bucket, the set’s size increases, and add() returns true.

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 and mutableSetOf() only when you genuinely need to add or remove elements later.
  • Use data class for 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 default setOf/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 is Comparable or supply a Comparator.
  • Use list.toSet() as the idiomatic way to de-duplicate a list; use list.distinct() when you want the result back as a List instead.
  • Prefer in (which calls contains()) for membership checks — if (x in mySet) reads more naturally than if (mySet.contains(x)).
  • Never rely on HashSet iteration 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 intersect to print students who took both subjects, and subtract to print students who took only Math.
  • Define a data class Coordinate(val x: Int, val y: Int), add several coordinates — including duplicates — to a mutableSetOf<Coordinate>(), and print the final size. Then remove the data keyword and predict (then explain) how the size changes.

Summary

  • A Set stores unique elements; duplicates (by equals()) are silently ignored on insertion.
  • setOf()/mutableSetOf() and linkedSetOf() preserve insertion order via LinkedHashSet; hashSetOf() gives no ordering guarantee; sortedSetOf() keeps ascending sorted order via TreeSet.
  • Uniqueness relies on consistent equals()/hashCode() — use data class for elements you plan to store in sets.
  • union, intersect, and subtract implement standard set-theory operations without mutating the originals.
  • val prevents 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.