Kotlin Empty List: Why `emptyList()` Returns a Singleton and How It Differs from Other Initializations

emptyList() in Kotlin returns a single, shared instance of an immutable list that is reused across the entire program, eliminating memory allocation overhead on every call.

When working with collections in the JetBrains/kotlin standard library, choosing the right initialization method impacts both performance and memory usage. Unlike other list factories that instantiate new objects, the kotlin empty list function leverages a singleton pattern defined in libraries/stdlib/src/kotlin/collections/Collections.kt. Understanding this implementation detail helps developers write more efficient code, particularly when using empty lists as default arguments or sentinel values.

The Singleton Implementation of EmptyList

The emptyList() function does not allocate new memory. Instead, it returns a reference to a single object declaration that exists for the lifetime of the application:

public fun <T> emptyList(): List<T> = EmptyList

This implementation appears at line 75 of libraries/stdlib/src/kotlin/collections/Collections.kt. The EmptyList object itself is defined at line 25 as an internal singleton implementing List<Nothing>:

internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
    override val size: Int get() = 0
    override fun isEmpty(): Boolean = true
    override fun get(index: Int): Nothing = 
        throw IndexOutOfBoundsException("Empty list doesn't contain element at index $index.")
    // … other overrides …
}

Because EmptyList is a Kotlin object, the JVM creates exactly one instance at class-initialization time. Every call to emptyList() returns this same instance, making identity comparisons (===) return true regardless of the call site or generic type parameter.

How kotlin empty list Differs from Other List Factories

The key differentiator of kotlin empty list is that it avoids allocation overhead entirely, while other factory functions create new backing storage:

emptyList() returns the single EmptyList object with zero allocation. This makes it ideal for default parameter values and high-frequency operations where you need a read-only empty list.

listOf() with no arguments delegates internally to emptyList(), returning the same singleton. However, when provided with elements, listOf(vararg) creates a new ArrayList or optimized specialized list.

mutableListOf() and arrayListOf() always allocate a new ArrayList instance, even when initialized with no elements. These return mutable collections with independent backing arrays.

listOf(element) returns a tiny, optimized read-only list that stores the single element directly, but this is still a new allocation rather than a singleton.

Practical Code Examples

The following examples demonstrate the singleton behavior and identity equality of emptyList():

// Example 1 – Reusing the same instance across types
val a: List<Int> = emptyList()
val b: List<String> = emptyList()
println(a === b)          // true – both reference the same EmptyList object

When used as a default parameter, emptyList() avoids creating new objects on every function call:

// Example 2 – Zero-allocation default parameter
fun <T> filterNonNull(items: List<T?> = emptyList()): List<T> =
    items.filterNotNull()

println(filterNonNull())  // prints [] without allocating a new list

The compiler optimizes listOf() without arguments to use the same singleton, but mutable variants create distinct instances:

// Example 3 – Comparison with listOf()
val c = listOf<Int>()    // compiled to a call to emptyList()
println(c === emptyList()) // true

// Example 4 – Mutable counterpart creates new objects
val mutable = mutableListOf<Int>()
println(mutable === emptyList()) // false – mutableListOf creates a new ArrayList

Serialization and Equality Guarantees

The EmptyList object implements Serializable and RandomAccess, ensuring it can be safely written to object streams while preserving its singleton nature upon deserialization. The serialVersionUID guarantees compatibility across Kotlin versions.

Equality comparisons treat any empty list as equal regardless of implementation:

val empty1 = emptyList<String>()
val empty2 = listOf<String>()
val empty3 = mutableListOf<String>()

println(empty1 == empty2)  // true – structural equality
println(empty1 == empty3)  // true – structural equality
println(empty1 === empty2) // true – referential equality (same singleton)

The toString() implementation yields "[]", providing consistent string representation across all empty list types.

Summary

  • emptyList() returns a pre-created singleton instance defined in libraries/stdlib/src/kotlin/collections/Collections.kt, eliminating per-call allocation overhead.
  • The EmptyList object implements List<Nothing>, Serializable, and RandomAccess, providing immutable, thread-safe behavior.
  • listOf() without arguments delegates to emptyList(), while mutableListOf() always creates a new ArrayList instance.
  • Identity comparisons (===) between any two emptyList() calls return true, confirming the singleton pattern.
  • The implementation supports safe serialization and maintains structural equality with any other empty list implementation.

Frequently Asked Questions

Is emptyList() the same as listOf() with no arguments?

Yes. When you call listOf() without arguments, the compiler generates a call to emptyList(), returning the same singleton instance. Both expressions result in identical bytecode and reference the shared EmptyList object.

Can I modify a list created with emptyList()?

No. The EmptyList object is immutable, and any attempt to modify it—such as calling add(), remove(), or set()—will result in an UnsupportedOperationException. For mutable empty lists, use mutableListOf() instead.

Why does emptyList() return List<Nothing> instead of List<T>?

The EmptyList object implements List<Nothing> because it contains no elements, and Nothing is the bottom type in Kotlin's type hierarchy. The generic function emptyList<T>() casts this single instance to List<T> safely at the call site, allowing type inference to work while maintaining the singleton pattern.

Is emptyList() thread-safe?

Yes. Because emptyList() returns an immutable singleton object with no mutable state, it is inherently thread-safe. Multiple threads can safely reference the same EmptyList instance without synchronization concerns, making it ideal for concurrent applications and default parameter values.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →