Skip to Content
Course content

168: Avoiding Boxing with Primitive Arrays

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 amplitudes property from Array<Float> to the appropriate primitive array type.
  • Update the calculatePeak function to operate on the primitive array.
  • Update the amplitudes initialization 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()}")
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.