skip to content

How are Kotlin's unsigned types implemented under the hood, and what does that mean for performance and the JVM signatures?

level: middleimportance: should knowfreq 30%

answer

  1. @JvmInline value class over the signed type
  2. JVM has no unsigned — same bits, reinterpreted
  3. No boxing except nullable/generic/Any
  4. UIntArray etc. avoid boxing in arrays
  5. Name mangling -> exposed as int, awkward Java interop

basics

~20 s

Each unsigned type is an inline value class that wraps the matching signed type. The JVM has no unsigned integers, so a UInt is really an Int at runtime, reinterpreted as unsigned. There's usually no boxing, so it's fast.

solid answer

~40 s

Unsigned types are declared as `value class` (inline value classes, formerly `inline class`) over their signed counterparts: UInt wraps Int, ULong wraps Long, UByte wraps Byte, UShort wraps Short. The JVM has no native unsigned integers, so at runtime a UInt is stored as a plain int with the same bit pattern, interpreted as unsigned by Kotlin's generated arithmetic. Because it's an inline value class, the wrapper is erased in most cases — no allocation, no boxing — so performance matches the signed type. Boxing happens only when an unsigned value is used as a nullable (`UInt?`), as a generic type argument, or stored in a collection of `Any`. A consequence is mangled JVM signatures: a function taking/returning UInt gets a name-mangled suffix and an `int` signature, which affects Java interop and reflection.

code

kotlin · 6 lines
kotlin
val u: UInt = (-1).toUInt()   // same bits as Int -1
println(u)                     // 4294967295
println(u.toInt())             // -1 (reinterpret back)

val arr = UIntArray(3) { it.toUInt() } // primitive-backed, no boxing
val boxed: List<UInt> = listOf(1u)     // here UInt IS boxed

go deeper

for a junior

Knows unsigned types wrap signed ones and the JVM lacks native unsigned ints.

for a middle

Explains inline value class erasure, when boxing happens, and that UIntArray avoids it.

for a senior

Connects mangled JVM signatures and reinterpreted bit patterns to real Java-interop and reflection consequences.

for a principal

Weighs the interop/tooling cost of exposing unsigned types across module/API boundaries and recommends keeping them internal.

## Built on inline value classes Kotlin's unsigned types are not a special primitive; each is a standard-library **inline value class** (declared with `value class`, the modern form of the old `inline class`). The declaration is essentially: ```kotlin @JvmInline value class UInt internal constructor(private val data: Int) ``` Mapping: - `UByte` wraps `Byte` - `UShort` wraps `Short` - `UInt` wraps `Int` - `ULong` wraps `Long` ## Why a wrapper at all? The **JVM has no unsigned integer types**. There is only signed `int`, `long`, etc. Kotlin reuses the same 32 bits but *reinterprets* them: the bit pattern `0xFFFF_FFFF` is `-1` as a signed Int and `4294967295` as a UInt. Operations like comparison, division, `toString`, and `%` are routed to helper functions (e.g. `Integer.compareUnsigned`, `Integer.divideUnsigned`, `Integer.toUnsignedString`) so they behave as unsigned. ## Inline = no boxing (usually) Because it is an **inline value class**, the compiler **erases the wrapper** at runtime in the common case: a `UInt` parameter or local is compiled to a raw `int`. This means: - **No heap allocation**, no boxing object — performance equals signed. - Arithmetic compiles to the same JVM instructions plus the occasional unsigned helper call. **Boxing does happen** when the value must become a real object reference: - nullable usage: `UInt?` - used as a generic type argument: `List<UInt>`, `T = UInt` - treated as `Any`/supertype - stored in non-specialized collections For primitive arrays without boxing, use the dedicated **`UIntArray`** (and `UByteArray`, `UShortArray`, `ULongArray`), which store the underlying primitives directly. ## JVM signature mangling To prevent JVM signature clashes (two functions that differ only by `Int` vs `UInt` would erase to the same `int` signature), the compiler **name-mangles** functions that expose inline value classes — appending a hash-like suffix to the method name. Practical effects: - The exposed JVM type is the underlying primitive (`int`), not a Kotlin type. - Java callers see mangled names and raw `int`, so direct Java interop is awkward. - Reflection / frameworks scanning method names may need to account for the mangling. ## Takeaways Unsigned types give you unsigned semantics with primitive-level performance, at the cost of clumsy Java interop and the usual inline-value-class boxing edge cases.

  • When does a UInt actually get boxed?
    When it must become an object reference: as a nullable UInt?, as a generic type argument like List<UInt>, when treated as Any, or stored in a non-specialized collection.
  • Why is calling a UInt-returning Kotlin function from Java awkward?
    The compiler mangles the method name to avoid signature clashes and exposes the raw int type, so Java sees a strange name and a plain int rather than UInt.

Same banknote, different reading: the JVM stores one bit pattern; 'unsigned' is just a different way of reading the same note's value.

saying these in an interview costs you the question

  • Claiming the JVM has native unsigned integer types
  • Saying every UInt is boxed/allocated like a wrapper object
  • Not knowing UIntArray exists and assuming List<UInt> is unboxed
  • Unaware of name mangling affecting Java interop

context