Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
168: Avoiding Boxing with Primitive Arrays
I once worked with a developer who was building a high-frequency telemetry processor for a fleet of drones. He had a buffer that stored roughly a million 64-bit timestamps as Array<Long>. On paper, the logic was flawless, but in production, the application was hitting massive GC (Garbage Collection) pauses that caused the system to drop packets. When we profiled the heap, we found that the JVM was spending a huge amount of time managing millions of java.lang.Long objects. He wasn't just storing numbers; he was storing millions of "boxes" that held numbers.
The Hidden Cost of Array<T>
In Kotlin, when you declare an Array<Int>, you aren't actually creating an array of primitive integers. Because Array<T> is a generic class, it can only hold objects. To make this work with numbers, the JVM uses "boxing." Every single Int is wrapped in an Integer object.
Think about the memory overhead. A primitive int takes up 4 bytes. A boxed Integer object, however, requires an object header (usually 12-16 bytes) plus the actual 4-byte value, and then there's the reference in the array itself. Suddenly, your "simple" list of numbers is consuming three or four times more memory than it should. For a few dozen elements, you'll never notice. For a million elements, you're asking for a performance nightmare.
// This is an array of objects (boxed)
val boxedNumbers: Array<Int> = arrayOf(1, 2, 3)
// This is a primitive array (unboxed)
val primitiveNumbers: IntArray = intArrayOf(1, 2, 3)
Switching to Specialized Primitive Arrays
To avoid this overhead, Kotlin provides specialized classes like IntArray, LongArray, DoubleArray, and so on. These map directly to the primitive arrays in Java (like int[] or double[]). When you use an IntArray, the values are stored contiguously in memory without any wrapper objects. This is significantly faster for the CPU to iterate over and creates zero pressure on the Garbage Collector.
I usually tell people to default to these primitive versions whenever they know the data type will remain constant. If you are dealing with coordinates, sensor readings, or large financial datasets, Array<Double> is almost always the wrong choice; DoubleArray is your friend here.
// Bad: Heavy memory usage and boxing
fun calculateSum(data: Array<Double>): Double {
return data.sum()
}
// Good: Direct primitive access, memory efficient
fun calculateSum(data: DoubleArray): Double {
return data.sum()
}
When Boxing is Actually Necessary
You might be wondering why Array<Int> even exists if it's so inefficient. The answer is generics. If you're writing a generic class—say, a DataBuffer<T> that needs to work for both String and Int—you can't use IntArray because IntArray doesn't inherit from a common generic array type.
Furthermore, if you need to store null values in your array, you have to use the boxed version. A primitive int can never be null; it's always at least 0. If your business logic requires a "missing" value (e.g., Array<Int?>), boxing is the price you pay for that flexibility. Just be mindful of the scale of your data before making that trade-off.
📋 Practical Task
Optimizing a High-Volume Signal Processor
You have been handed a piece of code from a legacy module that processes audio signal amplitudes. The current implementation uses Array<Float>, which is causing memory spikes during large file imports. Your task is to refactor the SignalProcessor class to eliminate boxing.
Requirements:
- Change the
amplitudesproperty fromArray<Float>to the appropriate primitive array type. - Update the
calculatePeakfunction to operate on the primitive array. - Update the
amplitudesinitialization to use the correct primitive array factory method.
class SignalProcessor(val amplitudes: Array<Float>) {
fun calculatePeak(): Float {
return amplitudes.maxOrNull() ?: 0f
}
}
fun main() {
// This is currently creating a boxed array of 1,000,000 Floats
val rawData = Array(1_000_000) { it.toFloat() * 0.1f }
val processor = SignalProcessor(rawData)
println("Peak: ${processor.calculatePeak()}")
}
There are no comments for now.