# 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
}
}
}
```