# Immutable Arrays Immutable Arrays are a safer and more efficient alternative to read-only lists while maintaining the same look and feel. They combine the safety of true immutability with hundreds of optimizations resulting in significant performance and memory improvements. Immutable arrays are inline classes that compile to regular arrays in the generated bytecode while eliminating mutating capabilities and replacing common operations with highly-optimized versions. If you find this library useful, please consider giving it a [![GitHub stars](https://img.shields.io/github/stars/daniel-rusu/pods4k?label=Star)](https://github.com/daniel-rusu/pods4k) on [GitHub](https://github.com/daniel-rusu/pods4k) and sharing it with others. * [Installation](#installation) * [Key Benefits](#key-benefits) * [Benchmarks](#benchmarks) * [Usage](#usage) * [Benefits vs Alternatives](#benefits-vs-alternatives) * [Memory Layout](#memory-layout) * [Caveats](#caveats) ## Installation is available from Maven Central. See [dependency instructions](../README.md#installation) for more details. ```kotlin repositories { mavenCentral() } dependencies { implementation("com.danrusu.pods4k:pods4k:") } ``` ## Key Benefits * **Clean & Familiar** * List-like syntax ensures easy adoption with clean code. * **True Immutability** * Unlike read-only lists, Immutable Arrays cannot be mutated through casting. * **Fast** * 2 to 8 times faster than lists for many common operations, with some even faster! * Immutability allows skipping work and many operations are bypassed entirely when inferring identical results. * **Memory Efficient** * Over 5X memory reduction versus lists for many operations across hundreds of scenarios! * `people.map { it.weightKg }` returns a primitive array when `weightKg` is a primitive type. * Immutability allows operations to retain zero extra memory in hundreds of scenarios! * `people.filter { it.isEmployed() }` returns same instance when everyone is employed. * **Type Safety** * Accidental mutation attempts are prevented at compile time. Ideal for memory-constrained environments, performance-critical workloads, or simply for ensuring data integrity. Immutable Arrays are great for Android and backend JVM applications. ## Benchmarks Here is a sneak peek of a few types of operations from the [Benchmarks page](BENCHMARKS.md): ### Copy operations Operations that copy ranges of values have significantly higher performance than lists and even regular arrays. The smaller data types are split into a separate chart to avoid skewing the axis since their performance is too high: ![Memory Layout of immutable arrays](./resources/benchmarks/take.png) ### Transformation Operations Transformations are much faster than lists and even regular arrays: ![Memory Layout of immutable arrays](./resources/benchmarks/map.png) ### Condition Operations Operations that inspect the data are much faster than lists when dealing with one of the 8 base types: ![Memory Layout of immutable arrays](./resources/benchmarks/any.png) ### Benchmark Summary It seems impossible that Immutable Arrays outperform regular arrays for dozens of operations since these are like-for-like comparisons (eg. `ImmutableFloatArray` vs.`FloatArray`). That's because Immutable Arrays replace array operations with highly optimized versions that maintain immutability. See the [Benchmarks page](BENCHMARKS.md) for detailed explanations and more benchmarks with even higher performance! Unlike lists or regular arrays, working with Immutable Arrays makes it common to operate on primitives without needing to think about it. The following automatically switches from an `ImmutableArray` to an `ImmutableFloatArray`: ```kotlin // This operation uses 5X less memory versus lists or regular arrays! val weightsInKilograms = people.map { it.weightKg } // ImmutableFloatArray since weightKg is a Float // ... // Extra-fast while looking the same as regular list code val babyIsPresent = weightsInKilograms.any { it < 5.0f } ``` ## Usage Usages look the same as regular lists after construction: ```kotlin val people = immutableArrayOf(dan, jill, bobby) people[0] // dan // Normal iteration with loops, forEach, asSequence, etc. for (person in people) { sendMarketingEmail(person) } // All the typical transformations and conditions val adults = people.filter { it.age >= 18 } val adultAges = adults.map { it.age } val adultsSortedByName = adults.sortedBy { it.name } val containsRetirees = adults.any { it.isRetired() } // etc. ```
Creating Immutable Arrays #### Regular Creation ```kotlin // Empty Arrays emptyImmutableArray() // generic ImmutableArray emptyImmutableBooleanArray() // primitive ImmutableBooleanArray // ... // From Values immutableArrayOf("Bob", "Jane") // ImmutableArray immutableArrayOf(1, 2, 3) // primitive int array immutableArrayOf(1, 2, 3) // generic array with boxed integers // Generated Elements ImmutableArray(size = 3) { it.toString() } // ["0", "1", "2"] ImmutableIntArray(size = 5) { it * it } // [0, 1, 4, 9, 16] // From Existing Collections listOfStrings.toImmutableArray() // ImmutableArray listOfIntegers.toImmutableArray() // primitive ImmutableIntArray listOfIntegers.toImmutableArray() // generic ImmutableArray // similarly with conversions from regular arrays or other iterables like Set, etc. ``` #### With Build Functions We can use the build functions when we don't know the resulting size in advance. Build functions are more efficient than accumulating the values in a collection and converting it to an immutable array. ```kotlin // Creates generic ImmutableArray val adults = buildImmutableArray { for (person in people) { if (person.age >= 18) add(person) } } // Creates primitive ImmutableIntArray val favoriteNumbers = buildImmutableIntArray { people.forEach { addAll(it.favoriteNumbers) } } ``` #### With Builders We can use immutable-array builders when accumulating values in more complex scenarios. Builders are more efficient than accumulating the values in a collection and converting that into an immutable array. ```kotlin fun getTopStocks(): ImmutableArray { val topStocksBuilder = ImmutableArray.Builder() addTrendingStocks(topStocksBuilder) addFastestGrowingStocks(topStocksBuilder) return topStocksBuilder.build() } // primitive variants also have builders e.g. ImmutableBooleanArray.Builder() ```
Accessing Elements #### By Position ```kotlin val names = immutableArrayOf("Dan", "Bob", "Jill") names[0] // "Dan" val (first, _, third) = names // first = "Dan", third = "Jill" // Special access methods names.single() // similarly with singleOrNull() names.first() // similarly with firstOrNUll() names.last() // similarly with lastOrNull() ``` #### By Condition ```kotlin val numbers = immutableArrayOf(1, 4, 5, 6) val firstEvenNumber = numbers.first { it % 2 == 0 } // 4 val lastOddNumber = numbers.last { it % 2 == 1 } // 5 // similarly with firstOrNull { condition } and lastOrNull { condition } numbers.single { it % 3 == 0 } // 6 // similarly with singleOrNull ```
Iterating Elements ```kotlin val names = immutableArrayOf("Dan", "Bob", "Jill") // For-Loop for (name in names) { println(name) } // ForEach names.forEach { println(it) } names.forEachIndexed { index, name -> println("$index: name") } // Sequence names.asSequence() .filter { /* ... */ } .forEach { /* ... */ } // Iterator names.asIterable() val iterator = names.iterator() while (iterator.hasNext()) { //... } ```
Conditions #### Element Conditions ```kotlin val names = immutableArrayOf("Dan", "Bobby", "Jill") "Jill" in names // true names.contains("Joe") // false names.isEmpty() // false names.all { it.isNotEmpty() } // true names.any { it.startsWith("B") } // true names.none { it.length > 10 } // true // etc. ``` #### Array Equality Conditions Regular equality work as expected. Kotlin prevents checking referential equality using `===` because Immutable Arrays are inline classes that get erased at compile time. They compile to regular arrays in the generated bytecode so they don't have the traditional object identity since the wrapper object is erased. We can use `referencesSameArrayAs` to check whether two variables reference the same array instance: ```kotlin val names = immutableArrayOf("Dan", "Jill") val sameNames = immutableArrayOf("Dan", "Jill") // true since they contain identical values names == sameNames // regular equality // false since they were created separately names.referencesSameArrayAs(sameNames) // referential equality of the array // Immutability allows us to safely share instances behind the scenes names.take(100).referencesSameArrayAs(names) // true names.filter { it.isNotEmpty() }.referencesSameArrayAs(names) // true // etc. ```
Transformations ```kotlin val names = immutableArrayOf("Dan", "Bobby", "Jill") names.map { it.length } // [3, 5, 4] names.filter { it.length <= 4 } // ["Dan", "Jill"] names.take(2) // ["Dan", "Bobby"] names.sorted() // ["Bobby", "Dan", "Jill"] names.partition { it.length % 2 == 0 } // Pair(["Jill"], ["Dan", "Bobby"]) // etc. ```
### Interop with List APIs You can pass an Immutable Array to methods that expect lists, collections, or iterables in several ways: ```kotlin val people = immutableArrayOf(dan, bob, jill) // copy to standalone read-only List notifyPeople(people.toList()) // create an immutable List wrapper backed by the same array without copying the elements notifyPeople(people.asList()) // create an Iterable wrapper without copying the elements notifyPeople(people.asIterable()) ```
Adding Immutable Arrays to an existing codebase Updating an existing codebase to use Immutable Arrays can be done in chunks rather than attempting to replace all usages at once. This can be tackled at the class, package, or module level and the boundaries that interact with other parts of the application would expose Immutable Arrays using `toList`, `asList`, or `asIterable`. As other parts of the application get updated to work with Immutable Arrays, the boundary layers can be updated to operate on Immutable Arrays directly for optimal efficiency.
Choosing between toList(), asList(), and asIterable() When working with reference types, such as `ImmutableArray`, prefer `asList()` as that allows the flexibility of random access and is extremely efficient since the generated wrapper is backed by the same backing array without copying the elements. When working with primitive variants, such as `ImmutableFloatArray`, exposing these to list APIs will auto-box the elements since lists operate on generic types: * If random access isn't needed, use `asIterable()` to avoid copying the backing array and nudge usages towards one-time access patterns with the iterator. * Otherwise, if random access is needed and the number of accesses won't exceed the list size, use `asList()`. * For everything else, use `toList()` as this auto-boxes all the values upfront and avoids additional auto-boxing when the same elements are accessed multiple times. Using `asList()` or `asIterable()` only auto-boxes the elements that are accessed. For example, wrapping a 1,000-element array by calling `asList()` and then performing 3 element accesses will only perform 3 auto-boxing operations. Using `asList()` or `asIterable()` has a secondary benefit since they only auto-box values lazily when accessed. This reduces the pressure on the garbage collector for use cases that aggregate values without retaining the elements, such as when keeping track of the sum to compute the average. This usage pattern of creating temporary objects that are immediately discarded is very efficient with modern garbage collectors as the collection process only copies objects that are still accessible. So having 1 or 1,000 discarded objects has no performance impact on the collection step. However, if we auto-box all elements up-front to pass to some utility, any collections that occur before that utility completes would be forced to copy all these objects around since they're all still accessible.
Why Immutable Arrays don't implement the List interface There are several reasons why Immutable Arrays shouldn't implement the `List` interface: 1. If the 8 primitive variants implemented the List interface, (eg.`ImmutableFloatArray` implemented `List`), we can't override the generic `List` methods and use primitive return types as that changes the method signature. Although Kotlin hides the distinction between a `Float` wrapper and a `float` primitive (using Java terms), this is enforced at the bytecode level. This would auto-box the elements every time they're accessed and dramatically affect the memory and performance of the library. 2. As a quick test, we made `ImmutableArray` implement the `List` interface and found that the `List` extension functions from the Kotlin standard library overshadowed the optimized versions from this library. This significantly affects the memory and performance of this library and also break the immutability guarantees since the Kotlin standard library functions generate read-only lists that can be mutated through casting. 3. The `List` interface contains methods with `List` return types that we wouldn't want users to use. Using these would affect the memory and performance profile but most importantly, this would make usages accidentally cross over into the list world where the immutability guarantees no longer exist.Throwing an `OperationNotSupportedException` would break the `List` contract breaking downstream usages in unpredictable ways.
## Benefits vs Alternatives | Feature | Immutable Arrays | Regular Arrays | Read-only Lists | Unmodifiable Lists | Java Immutable Lists | |--------------------------|------------------|------------------------|--------------------------------------|-----------------------------|---------------------------| | True Immutability | ✅ | ❌ | ❌
Casting enables mutation | ❌
Mutable backing list | ✅ | | Memory Efficiency | ✅✅ | ✅ | ❌ | ❌ | ❌ | | Performance | ✅✅ | ✅ | ❌ | ❌ | ❌ | | Compile-time Safety | ✅ | ❌
Can be mutated | ✅ / ❌
Casting enables mutation | ❌
Throws exceptions | ❌
Throws exceptions | | Proper equals & hashCode | ✅ | ❌ | ✅ | ✅ | ✅ | | Meaningful toString() | ✅ | ❌ | ✅ | ✅ | ✅ | ### Benefits over regular arrays
Meaningful toString() Unlike regular arrays, calling `toString()` on immutable arrays produces a pretty representation of the data: ```kotlin println(immutableArrayOf("Dan", "Bob")) // [Dan, Bob] Nice! println(arrayOf("Dan", "Bob")) // [Ljava.lang.String;@7d4991ad Yuck! ```
Efficient sharing of encapsulated data Regular arrays can have their elements reassigned making them a poor choice for encapsulated data that needs to be shared. this forces us to duplicate the contents before sharing so that callers can't mutate the encapsulated array. Note that calling `asList()` to wrap generic arrays is not safe as casting that to an ArrayList exposes a backdoor to mutating the shared underlying array. Duplicating the array negatively affects performance and adds extra pressure on the garbage collector. Immutable arrays can be safely shared resulting in cleaner and more efficient code.
Efficient operations Regular arrays are usually chosen for memory or performance reasons, however these benefits are negated when performing dozens of common operations since these generate lists auto-boxing the values: ```kotlin val weights = doubleArrayOf(1.5, 3.0, 10.2, 15.7, 2.0) val largeWeights = weights.filter { it > 10.0 } // Oops, this creates a List auto-boxing each value! ``` Unlike regular arrays, most of the common operations on immutable arrays have specializations resulting in the most optimal representation so that clean code is efficient by default: ```kotlin val people = immutableArrayOf( Person(name = "Dan", age = 3), Person(name = "Bob", age = 4), ) // ImmutableArray // Since the age field is a non-nullable Int, Mapping the ages uses an // efficient ImmutableIntArray storing primitive int values val ages = people.map { it.age } ``` Here's a non-exhaustive list of operations that take advantage of primitives resulting in significant memory and performance improvements over regular arrays: * drop * dropLast * dropLastWhile * dropWhile * filter * filterIndexed * filterNot * filterNotNull * flatMap * flatMapIndexed * flatten * map * mapNotNull * mapIndexed * mapIndexedNotNull * partition * sorted * sortedBy * sortedByDescending * sortedDescending * sortedWith * take * takeLast * takeLastWhile * takeWhile * etc.
Avoids equality & hashCode defects Unlike regular arrays, Immutable arrays have proper equals & hashCode implementations allowing us to check equality: ```kotlin arrayOf("Dan", "Bob") == arrayOf("Dan", "Bob") // false! // Yes, this condition will be true when immutable arrays have equal contents immutableArrayOf("Dan", "Bob") == immutableArrayOf("Dan", "Bob") // true ``` Since we can compare lists directly, developers occasionally attempt to do the same with regular arrays. Even worse, defects can sneak in without obvious usages of these broken behaviors: ```kotlin data class Order(val id: Long, private val products: Array) val rejectedOrders = mutableSetOf() // Oops, attempting to add Orders to a hashSet will make use of the auto-generated // equals & hashCode methods from the Order data class which will in turn rely on // the defective equals & hashCode implementation of regular arrays ``` Swapping `Array` with `ImmutableArray` will fix this defect scenario.

### Benefits over read-only lists
Casting doesn't introduce backdoor for mutation Read-only lists appear to be immutable at first, but they can be cast into a `MutableList` and modified: ```kotlin val values = listOf(1, 2, 3) (values as MutableList)[0] = 100 // backdoor to mutation println(values) // [100, 2, 3] ``` Immutable arrays don't have this backdoor: ```kotlin val values = immutableArrayOf(1, 2, 3) values[0] = 100 // Compiler error: No set method providing array access @Suppress("CAST_NEVER_SUCCEEDS") (values as IntArray)[0] = 100 // ClassCastException: ImmutableIntArray cannot be cast to [I ```
More memory efficient Read-only lists use between 3.5 to 32 times more memory than immutable arrays when storing or performing operations that produce primitive values (E.g. `people.map { it.age }`). See the **Memory Impacts** section in [Memory Layout](#memory-layout) for details. Read-only lists also have 17% unused capacity on average when the resulting capacity isn't known in advance. There's also the small memory overhead of the `ArrayList` class whereas variables of immutable array types point directly at the backing array in the generated bytecode.
Higher performance Most operations are significantly faster on immutable arrays compared to lists. Operations on immutable arrays have been optimized to reduce memory footprint, improve cache locality, and reduce the number of memory hops.

### Benefits over unmodifiable lists
Safer and more robust Calling `Collections.unmodifiableList(myMutableList)` doesn't copy the elements into a new immutable list but rather creates a view that wraps the original collection. Although the view won't allow mutation, the underlying collection that the view references can continue to be mutated. This introduces a category of defects where a view is shared and intended to be processed right away but the underlying list is modified again before the view is processed. This can happen when the view is shared and then a separate thread mutates the underlying list. Another scenario is when the handling logic gets updated to delay the processing to a later time such as by adding it to some worker queue. Immutable arrays don't have this problem as they can never be mutated.
No mutation exceptions at runtime Unmodifiable lists implement the Java `List` interface and override mutating methods to throw exceptions. Although mutation is prevented at the view level, bad usages result in runtime exceptions affecting the user experience. Attempting to mutate an immutable array won't even compile preventing this category of defects altogether.
More memory efficient Unmodifiable lists have the same memory drawbacks as read-only lists (see [Benefits over read-only lists](#benefits-over-read-only-lists)) along with a tiny extra overhead from the wrapper.
Higher performance Unmodifiable lists have similar performance drawbacks as read-only lists (see [Benefits over read-only lists](#benefits-over-read-only-lists)) but slightly worse due to the extra layer of indirection caused by the view wrapper.

### Benefits over immutable lists from Java (such as Guava)
No mutation exceptions at runtime Immutable lists defined in Java implement the mutable Java `List` interface and override mutating methods to throw exceptions. Although this prevents mutation, bad usages result in runtime exceptions affecting the user experience. Attempting to mutate an immutable array won't even compile preventing this category of defects altogether.
More memory efficient Immutable lists use between 3.5 to 32 times more memory than immutable arrays when storing or performing operations that produce primitive values (E.g. `people.map { it.age }`). See the **Memory Impacts** section in [Memory Layout](#memory-layout) for details. There's also the small memory overhead of the immutable list class whereas variables of immutable array types point directly at the backing array in the generated bytecode.
Higher performance Immutable lists have the same performance drawbacks as read-only lists (see [Benefits over read-only lists](#benefits-over-read-only-lists)).
## Memory Layout Performing some operation that results in an `ImmutableIntArray` ends up with the following memory layout: ![Memory Layout of immutable arrays](./resources/immutable-array-memory-layout.drawio.png) Note that the `values` variable of type `ImmutableIntArray` actually references a regular primitive int array in the bytecode. Here is the same example but operating on a regular primitive array and ending up with a read-only list: ![Memory Layout of Read-only Lists](./resources/list-memory-layout.drawio.png) Classes that operate on generics, such as lists, can't store primitive types directly. Each primitive int gets auto-boxed into an `Integer` wrapper object and a pointer to that wrapper is passed to the resulting list. These wrapper objects are allocated in different regions of memory depending on availability and the garbage collector also periodically moves surviving objects around, so we can end up with the objects scattered throughout the heap. Primitive arrays benefit from faster CPU memory access due to their contiguous memory layout, while lists with scattered wrapper objects face higher memory latency due to the extra indirection.
Memory Impacts 1. Notice that the list contains 7 values but the backing array has a capacity of 10. When an `ArrayList` runs out of capacity, the backing array is replaced with a new array that's 1.5-times larger and the elements get copied over. On average, array lists end up with about 17% of unused capacity when the exact capacity isn't known ahead of time. 2. A primitive int uses just 4 bytes. However, an `Integer` wrapper object requires 16 bytes for the object header, plus 4 bytes for the int value, plus another 4 bytes of padding totaling 24 bytes. Enabling JVM pointer compression reduces this to 16 bytes per wrapper. 3. Lists don't store the wrappers directly but instead store pointers to each of these wrappers. So a list of integers uses 8 + 24 = 32 bytes to store each 4-byte int value! Enabling JVM pointer compression reduces this to 20 bytes per integer element but that's still 5X the memory of primitive int arrays! 4. The ratio becomes worse when storing smaller data types. E.g. A list of booleans uses 32X more memory than primitive boolean arrays without JVM pointer compression and still 20X more memory with JVM pointer compression enabled! The following table shows the per-element memory usage on a 64-bit JVM accounting for the size of the element pointer, wrapper object header, value, and padding in the wrapper object to account for memory alignment: | Type | Immutable Array
(bytes per element) | ArrayList
JVM compressed oops disabled
(bytes per element) | ArrayList
JVM compressed oops enabled
(bytes per element) | |---------|-----------------------------------------|--------------------------------------------------------------------|-------------------------------------------------------------------| | Boolean | **1** | 8 + (16 + 1 + 7) = **32** | 4 + (12 + 1 + 3) = **20** | | Byte | **1** | 8 + (16 + 1 + 7) = **32** | 4 + (12 + 1 + 3) = **20** | | Char | **2** | 8 + (16 + 2 + 6) = **32** | 4 + (12 + 2 + 2) = **20** | | Short | **2** | 8 + (16 + 2 + 6) = **32** | 4 + (12 + 2 + 2) = **20** | | Int | **4** | 8 + (16 + 4 + 4) = **32** | 4 + (12 + 4 + 0) = **20** | | Float | **4** | 8 + (16 + 4 + 4) = **32** | 4 + (12 + 4 + 0) = **20** | | Long | **8** | 8 + (16 + 8 + 0) = **32** | 4 + (12 + 8 + 4) = **28** | | Double | **8** | 8 + (16 + 8 + 0) = **32** | 4 + (12 + 8 + 4) = **28** |
## Caveats
Relies on experimental Kotlin features The following experimental features are used which could change in future Kotlin releases: * [Overload resolution by lambda return type](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-overload-resolution-by-lambda-return-type/) * This enables the hundreds of optimized specializations that make use of overloaded functions containing parameters with different lambda return types. Without this feature, these overloaded functions would result in a runtime signature clash on the JVM. * This feature was introduced in Kotlin 1.4 and is used extensively throughout the Kotlin standard library. * [Custom equals in value classes](https://youtrack.jetbrains.com/issue/KT-24874/Support-custom-equals-and-hashCode-for-value-classes) * This enables overriding the `equals` & `hashCode` methods for inline value classes. * This feature was added for the JVM IR backend (which handles both Android & regular backend JVM development) in Kotlin 1.9 but hasn't been announced yet because the other multiplatform backends were not ready. Since this isn't a Kotlin multiplatform library, the lack of support in the other backends won't affect us. * Vote and comment on this [YouTrack ticket](https://youtrack.jetbrains.com/issue/KT-24874) to raise the importance of this feature so that we can use it without the extra experimental opt-in.
Shallow immutability Immutable arrays prevent adding, removing, or replacing elements. However, the elements themselves could be mutable: ```kotlin val people = immutableArrayOf(Person("Bob"), Person("Jane")) people[0].spouse = Person("Jill") ```
Auto-boxing Immutable arrays are zero-cost abstractions that get eliminated at compile time. All variables, properties, function arguments, function receiver types, or return types that explicitly use the immutable array types get replaced at compile time to operate directly on the underlying array without any auto-boxing or wrapper object. The Kotlin compiler adds additional instructions everywhere the immutable array is interpreted as a generic type, or by a supertype like `Any` or `Any?`. In these scenarios, the immutable array is auto-boxed into a single tiny wrapper object which stores a reference to the actual array. Note that this auto-boxing is different from lists as lists auto-box each primitive element. Generic functions that are marked with the `inline` modifier, such as `with` from the Kotlin standard library, don't induce auto-boxing because the function is inlined into each call site replacing the generic with the actual type. When using reflection to traverse the object graph, reflective code will encounter the underlying array directly except for the auto-boxing scenarios, in which case it will encounter the tiny wrapper. Here are some examples to get a better idea of where auto-boxing occurs: ```kotlin // no auto-boxing. `names` references the underlying array directly val names = immutableArrayOf("Dan", "Bob") // no auto-boxing because `with` is an inline function so the generic parameter gets replaced at compile time with(names) { println(this.size) } // casting induces auto-boxing. This prevents any backdoor to the underlying array names as Any // auto-boxing since println accepts a variable of type Any println(names) // Avoid println auto-boxing by calling toString() explicitly but the benefit is negligible if it's not in a loop println(names.toString()) // no auto-boxing since we're not passing the immutable array itself // Even though we're explicitly specifying the ImmutableArray type as the generic type, the ArrayList // class itself isn't hardcoded to work with immutable arrays, so each immutable array must be auto-boxed val arrays = ArrayList>() arrays += names // auto-boxing due to generics // auto-boxing because the immutable array is used as a generic receiver names.genericExtensionFunction() fun T.genericExtensionFunction() { // ... } ``` The overhead of auto-boxing the entire array is identical to that of autoboxing a single primitive `Double` value. Since this is referring to the entire immutable array, the memory or performance overhead of this operation is negligible in most scenarios unless it's part of a tight inner loop. Normally auto-boxing can have a large memory or performance impact when auto-boxing many values like what happens with read-only lists. However, in this case the immutable array itself is auto-boxed into a single tiny wrapper without auto-boxing any of the elements. For optimal performance, we recommend explicitly using the immutable array types for everything that expects to work with immutable arrays as this avoids auto-boxing. Passing immutable arrays to generic inline functions as the generic type also avoids auto-boxing since the generic parameter is replaced at compile time.
No identity Immutable Arrays are zero-cost abstractions that get eliminated at compile time and get compiled to regular arrays in the generated bytecode. We can think of them as a kind of virtual quantum particle that comes in and out of existence (see Auto-boxing above). Since immutable arrays aren't persistent wrapper objects, attempting to use their identities directly isn't supported. Use `immutableArray1.referencesSameArrayAs(immutableArray2)` if you want to check whether two immutable arrays refer to the same array. Here are some invalid patterns that attempt to make use of their identities: Reference equality: ```kotlin // Note the `===` reference equality. // Regular structural equality using `==` is allowed and works as expected immutableArray1 === immutableArray2 // Compiler error: Identity equality is forbidden ``` Identity hashCode: ```kotlin val values = immutableArrayOf(1, 2, 3) val identityHashCode = System.identityHashCode(values) // Oops, identityHashCode accepts Any type instead of an immutable array type, so it's auto-boxed // and the identity hashCode of that temporary wrapper is returned which is meaningless ``` Synchronization: ```kotlin class Account(val accountHolders: ImmutableArray) { fun withdraw(amount: Money) { // Compiler warning: Synchronizing by ImmutableArray is forbidden synchronized(accountHolders) { // Oops, synchronized accepts Any type instead of an immutable array type, so it's // auto-boxed. We're meaninglessly synchronizing on that temporary wrapper balance -= amount } } } ```