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 inlibraries/stdlib/src/kotlin/collections/Collections.kt, eliminating per-call allocation overhead.- The EmptyList object implements
List<Nothing>,Serializable, andRandomAccess, providing immutable, thread-safe behavior. listOf()without arguments delegates toemptyList(), whilemutableListOf()always creates a newArrayListinstance.- Identity comparisons (
===) between any twoemptyList()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →