Kotlin Data Types Explained
This guide belongs to our main Kotlin Fundamentals series.
If you have ever written Java, C++, or C#, you are likely accustomed to a sharp division between primitive data types (int, boolean, double) and reference object types (Integer, Boolean, Double).
In Kotlin, that artificial divide disappears completely.
To the developer, every variable in Kotlin behaves like an object with built-in methods and properties. Yet behind the scenes, the Kotlin compiler optimizes your code to run with the blazing performance of raw primitive types whenever possible.
Understanding Kotlin data types isn’t just about learning syntax—it is about knowing how types behave in memory, how type casting works, and how Kotlin’s unified type hierarchy prevents runtime errors.
In this comprehensive guide to Kotlin data types, you will learn:
- How Kotlin unifies primitive types and wrapper objects into a single type hierarchy.
- The exact range, size, and syntax rules for numbers, text, and logical data types.
- How specialized primitive arrays prevent memory overhead.
- The roles of Kotlin’s special top and bottom types:
Any,Unit, andNothing.
Let’s start by looking at how Kotlin handles type representation under the hood.
Everything is an Object: Kotlin’s Unified Type System
In legacy languages like Java, there is a strict divide between primitive types (like int, double, boolean) and reference types (like Integer, Double, Boolean). Primitives store raw values efficiently in memory, while reference types are full objects that live on the heap and support method calls.
Kotlin eliminates this dual system. In Kotlin source code, everything is an object. You can invoke functions and access properties on any variable, including numbers and booleans.
// Calling methods directly on raw numeric literals
val numberString = 42.toString()
val isEvenNumber = 10.rem(2) == 0
val absoluteValue = (-15).absoluteValue
Code language: Kotlin (kotlin)
How Kotlin Optimizes Memory Under the Hood
Even though every type looks like an object in code, Kotlin does not sacrifice execution speed or balloon memory usage. The Kotlin compiler (kotlinc) automatically optimizes types down to raw JVM primitives whenever possible.
How a type is compiled to JVM bytecode depends primarily on nullability and generics:
- Non-Nullable Types (
Int,Double,Boolean): Represented as raw JVM primitives (int,double,boolean) in bytecode. No object creation overhead or heap allocation occurs. - Nullable Types (
Int?,Double?,Boolean?): Must be boxed into JVM object wrappers (java.lang.Integer,java.lang.Double) because primitive types on the JVM cannot holdnull. - Generic Collections (
List<Int>): Generic parameters require boxed object wrappers on the JVM due to type erasure.
// Compiles to JVM primitive 'int' (fast, lightweight memory footprint)
val age: Int = 25
// Compiles to JVM boxed 'java.lang.Integer' (holds object reference to support null)
val nullableAge: Int? = null
// Compiles to java.util.List<java.lang.Integer> (boxed objects inside generics)
val scores: List<Int> = listOf(90, 85, 95)
Code language: Kotlin (kotlin)
Automatic Primitive Boxing Comparison
| Kotlin Source Type | Nullable? | Compiled JVM Bytecode Type | Memory Overhead |
Int | No | int (Primitive) | Minimum (4 bytes) |
Int? | Yes | java.lang.Integer (Boxed Object) | Higher (Object header + reference) |
Double | No | double (Primitive) | Minimum (8 bytes) |
Double? | Yes | java.lang.Double (Boxed Object) | Higher (Object header + reference) |
Boolean | No | boolean (Primitive) | Minimum (1 byte) |
Boolean? | Yes | java.lang.Boolean (Boxed Object) | Higher (Object header + reference) |
Performance Takeaway: Avoid making numeric variables nullable (
Int?,Double?) unless your application domain explicitly requires representing missing state (null). Keeping primitive types non-nullable allowskotlincto generate ultra-fast primitive bytecode.
Numeric Data Types in Detail
Kotlin provides a rich set of built-in numeric types that represent integers, floating-point numbers, and unsigned integers.
Unlike languages like C++ or Java, Kotlin does not perform implicit widening type conversions. For instance, you cannot assign an Int directly to a Long variable without an explicit conversion call.
Integer Types
Kotlin offers four standard signed integer types, differing by memory allocation and bit size:
| Data Type | Size (Bits) | Min Value | Max Value | Typical Use Case |
Byte | 8 | $-128$ | $127$ | Low-level buffer streams, raw binary processing. |
Short | 16 | $-32,768$ | $32,767$ | Memory-constrained game data or microcontrollers. |
Int | 32 | $\approx -2.14 \times 10^9$ | $\approx 2.14 \times 10^9$ | Default type for whole numbers and loop counters. |
Long | 64 | $\approx -9.22 \times 10^{18}$ | $\approx 9.22 \times 10^{18}$ | Database primary keys, timestamps, large financial values. |
val defaultInteger = 42 // Inferred automatically as Int
val explicitLong = 42L // Long literal specified using 'L' suffix
val explicitByte: Byte = 10 // Explicitly typed as Byte
Code language: Kotlin (kotlin)
Floating-Point Types
For decimal values, Kotlin supports standard IEEE 754 floating-point representations:
| Data Type | Size (Bits) | Significant Bits | Precision | Suffix |
Float | 32 | 24 | 6-7 decimal digits | f or F |
Double | 64 | 53 | 15-16 decimal digits | None (Default) |
val piDouble = 3.141592653589793 // Inferred as Double (64-bit precision)
val piFloat = 3.1415927f // Explicit Float literal using 'f' suffix
Code language: Kotlin (kotlin)
Precision Tip: Use
Doubleby default for decimal calculations. Only reach forFloatin memory-critical applications like real-time 3D graphics rendering or embedded sensor processing.
Explicit Conversions (No Implicit Widening)
To prevent subtle truncation bugs, Kotlin requires explicit calls to convert between numeric types—even when converting a smaller type to a larger type:
val smallInt: Int = 100
// COMPILE ERROR: Int cannot be directly assigned to Long
// val bigLong: Long = smallInt
// CORRECT: Explicit conversion using .toLong()
val bigLong: Long = smallInt.toLong()
Code language: Kotlin (kotlin)
Every numeric type provides helper functions for explicit conversions:
.toByte(),.toShort(),.toInt(),.toLong().toFloat(),.toDouble().toChar()
Literals, Formatting, and Unsigned Integers
Kotlin includes built-in syntax features to make numeric literals cleaner and easier to read in source code:
1. Visual Underscores in Literals
You can place underscores inside large numbers to improve visual readability without affecting the underlying numeric value:
val creditCardNumber = 1234_5678_9012_3456L
val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E
Code language: Kotlin (kotlin)
2. Hexadecimal and Binary Literals
Represent non-decimal number formats using standard prefix notations:
val hexValue = 0x0F // Hexadecimal (15 in base-10)
val binaryValue = 0b00001010 // Binary (10 in base-10)
Code language: Kotlin (kotlin)
3. Unsigned Integer Types
When working with low-level bitwise operations, cryptographics, or binary protocols where negative values make no sense, Kotlin offers explicit Unsigned Integer Types (UByte, UShort, UInt, ULong):
val unsignedInt: UInt = 4000000000u // 'u' or 'U' suffix denotes unsigned literal
val unsignedByte: UByte = 255u // Range: 0 to 255
Code language: Kotlin (kotlin)
Textual and Logical Types: Char, Boolean, and String
Beyond raw numbers, Kotlin provides first-class, highly expressive types for handling individual characters, truth values, and textual data.
1. Char: Representing Characters
In Kotlin, the Char type represents a single 16-bit Unicode character enclosed in single quotes ('A').
Unlike Java or C++, a Char in Kotlin cannot be treated directly as a number:
val letter: Char = 'A'
// COMPILE ERROR: Character literals cannot be assigned direct numeric values
// val invalidChar: Char = 65
// CORRECT: Convert explicitly from an Int to a Char
val asciiChar: Char = 65.toChar() // Result: 'A'
Code language: Kotlin (kotlin)
Special Escape Sequences
Special characters inside single or double quotes are escaped using a backslash (\):
\n(Newline),\t(Tab),\b(Backspace),\r(Carriage Return)\'(Single Quote),\"(Double Quote),\\(Backslash),\$(Dollar Sign)
2. Boolean: Logical Truth Values
The Boolean type represents logical values with two possible states: true or false. When non-nullable, it compiles down to the JVM primitive boolean (1 byte).
Kotlin supports standard short-circuit logical operations:
val isEmailVerified = true
val isAccountActive = false
val canLogin = isEmailVerified && isAccountActive // Logical AND (false)
val requiresAttention = !isEmailVerified || !isAccountActive // Logical OR / NOT (true)
Code language: Kotlin (kotlin)
Short-Circuiting: In
a && b, ifaisfalse,bis never evaluated. Ina || b, ifaistrue,bis skipped.
3. String: Working with Text
Strings in Kotlin are immutable sequences of characters enclosed in double quotes ("..."). Because strings are immutable, operations that modify a string return a brand-new String instance.
String Templates (Interpolation)
Kotlin renders string concatenation obsolete through String Templates. Prefix a variable name with $ to embed its value directly inside a string. For complex expressions or function calls, enclose the expression in curly braces ${}:
val firstName = "Billy"
val age = 28
// Interpolating simple variables
val greeting = "Hello, my name is $firstName and I am $age years old."
// Interpolating complex expressions
val calculationMessage = "In 5 years, $firstName will be ${age + 5} years old."
val uppercaseName = "Uppercase: ${firstName.uppercase()}"
Code language: Kotlin (kotlin)
Raw Multiline Strings (""")
When working with JSON payloads, SQL queries, or multiline prose, standard strings require heavy escaping (\n, \").
Kotlin solves this with Raw Multiline Strings enclosed in triple quotes ("""..."""). Raw strings retain line breaks and formatting without requiring escape characters:
// Raw multiline string with automatic margin stripping
val jsonPayload = """
{
"id": 101,
"name": "$firstName",
"role": "Developer"
}
""".trimIndent()
Code language: Kotlin (kotlin)
Handling Formatting Margins
Use .trimIndent() to strip common leading whitespace across all lines, or .trimMargin() with a pipe symbol (|) to control precise left alignment:
val formattedQuery = """
|SELECT * FROM users
|WHERE age >= 18
|ORDER BY created_at DESC
""".trimMargin("|")
Code language: Kotlin (kotlin)
Arrays vs. Specialized Primitive Arrays
Arrays in Kotlin represent fixed-size sequential collections of elements. However, how you instantiate an array can have a massive impact on your application’s memory footprint and CPU performance.
To prevent unneeded boxing overhead, Kotlin offers two distinct categories of arrays: generic arrays (Array<T>) and specialized primitive arrays (like IntArray, ByteArray, and DoubleArray).
Generic Arrays (Array<T>)
When you create an array using Array<T> or the arrayOf() factory function, Kotlin creates a generic object array.
If T is a primitive type (like Int or Double), the JVM boxes every element into an object wrapper (java.lang.Integer[] or java.lang.Double[]):
Kotlin
// Compiles to java.lang.Integer[] (Array of object references)
val genericArray: Array<Int> = arrayOf(1, 2, 3, 4, 5)
Code language: Kotlin (kotlin)
In memory, genericArray does not store the raw integer numbers side by side. Instead, it holds an array of 64-bit object references, with each reference pointing to a separate Integer object allocated elsewhere on the heap.
Specialized Primitive Arrays
To avoid the performance penalty of object boxing and memory pointer chasing, Kotlin provides dedicated array classes for every primitive type:

// Compiles directly to Java raw primitive int[]
val primitiveArray: IntArray = intArrayOf(1, 2, 3, 4, 5)
Code language: Kotlin (kotlin)
Memory Layout & Performance Breakdown
| Kotlin Type | Compiled JVM Bytecode Type | Memory Structure | Boxing Overhead |
Array<Int> | java.lang.Integer[] | Array of references $\rightarrow$ Heap objects | High (Pointer overhead + Object headers) |
IntArray | int[] | Continuous contiguous memory block | Zero (Raw primitive bytes) |
Array<Double> | java.lang.Double[] | Array of references $\rightarrow$ Heap objects | High |
DoubleArray | double[] | Continuous contiguous memory block | Zero |
Array<Boolean> | java.lang.Boolean[] | Array of references $\rightarrow$ Heap objects | High |
BooleanArray | boolean[] | Continuous contiguous memory block | Zero |
Why Primitive Arrays Are Significantly Faster:
- Cache Locality: Because an
IntArraystores raw primitive values in a contiguous block of memory, CPU hardware caches prefetch adjacent elements seamlessly during iteration. - Zero Garbage Collection Pressure:
IntArraycreates a single array object on the heap. In contrast, anArray<Int>with 10,000 elements creates 10,001 heap objects (1 array container + 10,000 boxedIntegerinstances), placing heavy pressure on the JVM Garbage Collector.
Available Primitive Array Types & Factory Functions
Kotlin includes specialized primitive arrays for all built-in numeric and logical types:
val bytes: ByteArray = byteArrayOf(0x10, 0x20)
val shorts: ShortArray = shortArrayOf(10, 20)
val ints: IntArray = intArrayOf(100, 200, 300)
val longs: LongArray = longArrayOf(1000L, 2000L)
val floats: FloatArray = floatArrayOf(1.0f, 2.0f)
val doubles: DoubleArray = doubleArrayOf(1.5, 2.5)
val booleans: BooleanArray = booleanArrayOf(true, false)
val chars: CharArray = charArrayOf('K', 't')
Code language: Kotlin (kotlin)
You can also construct primitive arrays with a fixed size and an initializer function:
// Creates an IntArray of size 5 initialized with squared index values: [0, 1, 4, 9, 16]
val squares = IntArray(5) { index -> index * index }
Code language: Kotlin (kotlin)
Converting Between Array Types
If an API requires a generic Array<Int> but you are holding an IntArray, you can convert between them using explicit mapping functions:
val primitiveInts = intArrayOf(1, 2, 3)
// Convert IntArray (int[]) to Array<Int> (Integer[])
val boxedArray: Array<Int> = primitiveInts.toTypedArray()
// Convert Array<Int> (Integer[]) back to IntArray (int[])
val backToPrimitive: IntArray = boxedArray.toIntArray()
Code language: Kotlin (kotlin)
Performance Rule of Thumb: Always default to specialized primitive arrays (
IntArray,DoubleArray,ByteArray) when working with primitive collections. Reserve genericArray<T>for custom object types (e.g.,Array<User>) or when interfacing with generic Java APIs.
Special Structural Types: Any, Unit, and Nothing
Kotlin’s type hierarchy is crowned and anchored by three special structural types: Any, Unit, and Nothing.
Understanding how these three types interact gives you full visibility into Kotlin’s underlying type system and makes function contracts much cleaner.
1. Any: The Universal Top Type
In Kotlin, Any is the supertype of all non-nullable classes, including numeric primitives, strings, and custom objects. It sits at the absolute top of the non-nullable type hierarchy.
val number: Any = 42 // Int is a subtype of Any
val text: Any = "Hello" // String is a subtype of Any
val user: Any = User("Billy") // Custom class is a subtype of Any
Code language: Kotlin (kotlin)
Any vs. Java’s Object:
Anymaps directly tojava.lang.Objectin JVM bytecode.- Unlike Java’s
Object,Anyonly defines three core methods:equals(),hashCode(), andtoString(). Low-level synchronization methods likewait()andnotify()are omitted fromAny. - To hold
null, you must useAny?—the true root supertype of every possible value in Kotlin.
2. Unit: The Expressive Equivalent of void
In languages like C++ or Java, functions that do not return a result use the void keyword. In Kotlin, such functions return Unit.
The key difference is that while void represents the total absence of a value, Unit is a real, singleton object:
// Implicitly returns Unit
fun logMessage(message: String) {
println("LOG: $message")
}
// Explicitly declared Unit return type (identical to above)
fun logMessageExplicit(message: String): Unit {
println("LOG: $message")
return Unit // Optional: Unit can be returned explicitly
}
Code language: Kotlin (kotlin)
Why Unit Superior to void:
Because Unit is a real singleton object, generic higher-order functions do not need special edge-case handling for functions with no return value. A generic callback like Function<T> simply uses Unit as T:
// Clean functional contract: no specialVoidCallback required
val onClickListener: () -> Unit = { println("Button clicked!") }
Code language: Kotlin (kotlin)
3. Nothing: The Universal Bottom Type
If Any? sits at the top of Kotlin’s type hierarchy, Nothing sits at the absolute bottom. Nothing is a subtype of every type in Kotlin—even custom or primitive types.
Nothing represents a value that never exists. It signals to the compiler that an expression or function will never complete normally (it either throws an exception or enters an infinite loop):
// Function that always throws an exception returns Nothing
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
// TODO() built-in function also returns Nothing
fun processData(): String {
TODO("Implement data processing pipeline") // Compiles fine because Nothing is a subtype of String!
}
Code language: Kotlin (kotlin)
How Nothing Empowers Null-Safety Checks
Because Nothing is a subtype of every type, you can use throw expressions on the right side of the Elvis operator (?:) without breaking type safety:
fun processUser(inputName: String?) {
// If inputName is null, fail() returns Nothing.
// The compiler infers 'name' as non-nullable String!
val name: String = inputName ?: fail("Name required")
println("Processing $name...")
}
Code language: Kotlin (kotlin)
Summary Matrix: Any vs Unit vs Nothing
| Type | Position in Type Hierarchy | JVM Equivalent | What It Represents |
Any | Top of non-nullable hierarchy | java.lang.Object | Any non-null value or object instance. |
Any? | Universal top type | java.lang.Object | Any value, object instance, or null. |
Unit | Standard object type | void (or Unit instance) | A function that finishes normally with no return data. |
Nothing | Bottom type (subtype of all) | Void / Exception throw | An operation that never completes or returns a value. |
Conclusion & Actionable Takeaways
Kotlin’s unified type system strikes the ideal balance between developer productivity and execution efficiency. By treating everything as an object in source code while optimizing down to raw primitives in JVM bytecode, Kotlin eliminates the clunky dualities of legacy languages without sacrificing speed or memory performance.
Understanding how kotlinc represents types in bytecode—and leveraging specialized structures like IntArray, string templates, and Nothing—allows you to write clean, defensive, and high-performance applications.
Master Type System Cheat Sheet
| Type Category | Kotlin Types | Compiled JVM Bytecode Type | Key Characteristics |
| Numeric | Byte, Short, Int, Long, Float, Double | Primitive (int, double) or Boxed (Integer) | Primitive when non-null; requires explicit widening calls (e.g., .toLong()). |
| Textual | Char, String | char, java.lang.String | Char cannot be assigned integer literals directly; String supports raw """ blocks. |
| Logical | Boolean | boolean or java.lang.Boolean | Evaluates truth values with standard short-circuiting logic (&&, ` |
| Primitive Arrays | IntArray, ByteArray, DoubleArray | int[], byte[], double[] | Contiguous primitive memory blocks with zero object-boxing overhead. |
| Structural | Any, Unit, Nothing | Object, void / Unit, Void | Any is top type; Unit is singleton void; Nothing marks non-returning operations. |
Actionable Next Steps for Developers
Ready to optimize how data types are used across your codebase? Here is your quick-start checklist:
- Eliminate Unnecessary Primitive Nullability: Audit your core domain models and data pipelines. Change
Int?orDouble?to non-nullableIntorDoublewhenever possible to prevent the JVM from allocating boxed object wrappers on the heap. - Swap Generic Arrays for Primitive Arrays: Search your codebase for arrays of numbers or booleans (e.g.,
Array<Int>). Replace them with specialized arrays (IntArray,ByteArray,FloatArray) to drastically reduce garbage collection pressure and improve memory cache locality. - Clean Up String Formatting: Replace verbose string concatenations (
+) with clean String Templates ("$var"or"${expr}"). Convert multi-line JSON payloads, SQL queries, or regex patterns into triple-quoted raw strings ("""...""") with.trimIndent(). - Leverage
Nothingfor Fail-Fast Guard Functions: UseNothingas the return type for custom error-handling or validation functions. This allows the Kotlin compiler to automatically infer non-null state on the left side of Elvis operators (?:).
Which Kotlin data type feature surprised you the most? Are you using specialized primitive arrays in your projects, or do raw strings steal the show? Let us know in the comments below!