skip to content

Unsigned Types

UInt and friends give you unsigned arithmetic as inline value classes wrapping the signed types, written with a u suffix. It is a niche corner, but it shows up when you handle binary protocols, and the interop and arithmetic limitations are the interesting part.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

How do you convert between signed and unsigned types in Kotlin, and what are the bit-level gotchas?

level: middleimportance: must knowfreq 28%

answer

  1. No implicit conversion — call toUInt()/toInt()/toUByte()
  2. Same width = reinterpret bits, e.g. (-1).toUInt()==4294967295
  3. Widening signed sign-extends; unsigned zero-extends
  4. Byte gotcha: use b.toUByte().toUInt() not b.toUInt()
  5. Java interop crosses at the underlying int/long

basics

~10 s

Kotlin never converts numbers implicitly, so you call explicit functions like toUInt(), toInt(), toULong(), or toUByte(). Converting reinterprets the bits, which can flip a negative signed number into a large unsigned one.

solid answer

~40 s

Kotlin has no implicit widening, so all conversions are explicit calls. Each numeric type offers `toUByte()`, `toUShort()`, `toUInt()`, `toULong()` and the reverse `toByte()`, `toInt()`, `toLong()`. Two distinct semantics matter: converting a *signed* value to its same-width unsigned counterpart (e.g. `Int.toUInt()`) **reinterprets the bits** — `(-1).toInt().toUInt()` becomes `4294967295`. Converting across widths follows the usual truncate/sign-extend rules of the source type. There are also `Int.toUByte()` style narrowing conversions that truncate. Be careful with `Byte.toUByte()` vs `Byte.toUInt()`: a negative byte sign-extends when going through Int. For correct unsigned widening of a byte read from a stream, use `byte.toUByte().toUInt()` rather than `byte.toUInt()`. Unsigned types interop with the JDK via helpers like `java.lang.Integer.toUnsignedLong`.

code

kotlin · 7 lines
kotlin
val negByte: Byte = -1
println(negByte.toUInt())            // 4294967295 (sign-extended)
println(negByte.toUByte().toUInt())  // 255 (zero-extended, correct)

val u: UInt = 0xFFFF_FFFFu
println(u.toInt())                   // -1 (same bits)
println(u.toLong())                  // 4294967295 (zero-extended)

go deeper

for a junior

Knows conversions are explicit toX() calls and that unsigned can't be implicit.

for a middle

Distinguishes same-width reinterpretation from cross-width sign/zero extension and truncation.

for a senior

Diagnoses the Byte sign-extension bug and prescribes toUByte().toUInt() for protocol parsing.

for a principal

Establishes conversion conventions at module boundaries (where to convert, what JDK helpers to use) to keep unsigned semantics correct across interop.

## Explicit conversions only Kotlin deliberately has **no implicit widening or narrowing** between numeric types — including signed↔unsigned. You must call an explicit conversion function. The available ones on every numeric type include: - to unsigned: `toUByte()`, `toUShort()`, `toUInt()`, `toULong()` - back to signed: `toByte()`, `toShort()`, `toInt()`, `toLong()` - to floating: `toFloat()`, `toDouble()` ## Two kinds of conversion semantics **1. Same-width reinterpretation.** Converting a signed value to the *same-width* unsigned type keeps the bits and only changes interpretation: ```kotlin val s = -1 // Int, bits 0xFFFFFFFF val u = s.toUInt() // 4294967295u (same bits) println(u.toInt()) // -1 (reinterpret back) ``` **2. Cross-width conversion** follows the **source** type's rules: widening a *signed* source **sign-extends**, widening an *unsigned* source **zero-extends**, and narrowing **truncates**. ## The classic byte gotcha This is the most common real bug. A `Byte` read from a stream can be negative (range −128..127). Watch the difference: ```kotlin val b: Byte = -1 // 0xFF println(b.toUInt()) // 4294967295 (sign-extended through Int!) println(b.toUByte().toUInt()) // 255 (zero-extended, what you usually want) ``` Going `Byte.toUInt()` first sign-extends the byte to a 32-bit Int, *then* reinterprets. Going `Byte.toUByte()` first reinterprets the 8 bits as `0..255`, then `toUInt()` zero-extends. For protocol/byte-buffer code, **`toUByte().toUInt()`** is the safe idiom. ## Floating-point and parsing Unsigned types also support `toFloat()`/`toDouble()`. To parse text you can use `"255".toUInt()` / `toUIntOrNull()` style extensions. The standard library and `java.lang.Integer`/`Long` provide `toUnsignedString`, `divideUnsigned`, `remainderUnsigned`, and `compareUnsigned` used internally. ## Java interop Because Java lacks unsigned types, crossing the boundary you typically pass the underlying `int`/`long` and use JDK unsigned helpers, or convert at the edge with `.toInt()`/`.toUInt()`. ## Summary - No implicit conversions — always `toX()`. - Same-width signed↔unsigned = bit reinterpretation. - Cross-width follows the *source* type (sign-extend signed, zero-extend unsigned, truncate narrowing). - For bytes, prefer `toUByte().toUInt()` to avoid accidental sign extension.

  • Why does (-1).toUInt() print 4294967295?
    Int -1 is the bit pattern 0xFFFFFFFF. toUInt() keeps the exact 32 bits and reinterprets them as unsigned, which is the maximum UInt value.
  • You read a Byte 0x80 and want its unsigned value 128. What do you write?
    byte.toUByte().toUInt() (or .toInt() and(0xFF)). Plain byte.toInt()/toUInt() sign-extends to a negative/huge value because Byte 0x80 is -128.

saying these in an interview costs you the question

  • Expecting implicit signed->unsigned conversion to compile
  • Using byte.toUInt() for protocol parsing and getting sign extension
  • Thinking (-1).toUInt() throws or clamps instead of reinterpreting
  • Believing widening an unsigned value sign-extends

context

open as a page

What are Kotlin's unsigned integer types, and how do you write unsigned literals?

level: juniorimportance: should knowfreq 35%

basics

~20 s

Kotlin has four unsigned integer types: UByte, UShort, UInt, and ULong. They only hold zero and positive numbers. You write unsigned literals by adding the suffix u (or uL for ULong), like 42u or 0xFFu.

open as a page

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%

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.

open as a page

How does arithmetic, comparison, and overflow behave for Kotlin unsigned types, and where do mixed signed/unsigned operations fail?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Unsigned arithmetic wraps around silently on overflow, never throwing. Division and comparison use unsigned rules, so big values aren't treated as negative. You cannot freely mix signed and unsigned in one expression; you must convert first.

open as a page

When should a team adopt Kotlin unsigned types in a public API, and what are the trade-offs versus signed Int with manual masking?

level: principalimportance: nice to knowfreq 12%

basics

~20 s

Use unsigned types when a value is naturally non-negative and may exceed the signed maximum, like bit masks or binary protocol fields. Avoid them in public APIs shared with Java, because interop and tooling get awkward; signed Int with masking is often safer there.

open as a page