How are Kotlin's unsigned types implemented under the hood, and what does that mean for performance and the JVM signatures?
answer
- @JvmInline value class over the signed type
- JVM has no unsigned — same bits, reinterpreted
- No boxing except nullable/generic/Any
- UIntArray etc. avoid boxing in arrays
- Name mangling -> exposed as int, awkward Java interop
basics
~20 sEach 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 sUnsigned 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 linesval 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 boxedgo deeper
Knows unsigned types wrap signed ones and the JVM lacks native unsigned ints.
Explains inline value class erasure, when boxing happens, and that UIntArray avoids it.
Connects mangled JVM signatures and reinterpreted bit patterns to real Java-interop and reflection consequences.
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