Kotlin Data Types Explained

Kotlin data types

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, and Nothing.

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 hold null.
  • 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 TypeNullable?Compiled JVM Bytecode TypeMemory Overhead
IntNoint (Primitive)Minimum (4 bytes)
Int?Yesjava.lang.Integer (Boxed Object)Higher (Object header + reference)
DoubleNodouble (Primitive)Minimum (8 bytes)
Double?Yesjava.lang.Double (Boxed Object)Higher (Object header + reference)
BooleanNoboolean (Primitive)Minimum (1 byte)
Boolean?Yesjava.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 allows kotlinc to 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 TypeSize (Bits)Min ValueMax ValueTypical Use Case
Byte8$-128$$127$Low-level buffer streams, raw binary processing.
Short16$-32,768$$32,767$Memory-constrained game data or microcontrollers.
Int32$\approx -2.14 \times 10^9$$\approx 2.14 \times 10^9$Default type for whole numbers and loop counters.
Long64$\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 TypeSize (Bits)Significant BitsPrecisionSuffix
Float32246-7 decimal digitsf or F
Double645315-16 decimal digitsNone (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 Double by default for decimal calculations. Only reach for Float in 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, if a is false, b is never evaluated. In a || b, if a is true, b is 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:

kotlin arrays
Primitive array contiguous memory vs object array pointer references. Source: HeapPulse
// 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 TypeCompiled JVM Bytecode TypeMemory StructureBoxing Overhead
Array<Int>java.lang.Integer[]Array of references $\rightarrow$ Heap objectsHigh (Pointer overhead + Object headers)
IntArrayint[]Continuous contiguous memory blockZero (Raw primitive bytes)
Array<Double>java.lang.Double[]Array of references $\rightarrow$ Heap objectsHigh
DoubleArraydouble[]Continuous contiguous memory blockZero
Array<Boolean>java.lang.Boolean[]Array of references $\rightarrow$ Heap objectsHigh
BooleanArrayboolean[]Continuous contiguous memory blockZero

Why Primitive Arrays Are Significantly Faster:

  1. Cache Locality: Because an IntArray stores raw primitive values in a contiguous block of memory, CPU hardware caches prefetch adjacent elements seamlessly during iteration.
  2. Zero Garbage Collection Pressure:IntArray creates a single array object on the heap. In contrast, an Array<Int> with 10,000 elements creates 10,001 heap objects (1 array container + 10,000 boxed Integer instances), 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 generic Array<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:

  • Any maps directly to java.lang.Object in JVM bytecode.
  • Unlike Java’s Object, Any only defines three core methods: equals(), hashCode(), and toString(). Low-level synchronization methods like wait() and notify() are omitted from Any.
  • To hold null, you must use Any?—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

TypePosition in Type HierarchyJVM EquivalentWhat It Represents
AnyTop of non-nullable hierarchyjava.lang.ObjectAny non-null value or object instance.
Any?Universal top typejava.lang.ObjectAny value, object instance, or null.
UnitStandard object typevoid (or Unit instance)A function that finishes normally with no return data.
NothingBottom type (subtype of all)Void / Exception throwAn 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 CategoryKotlin TypesCompiled JVM Bytecode TypeKey Characteristics
NumericByte, Short, Int, Long, Float, DoublePrimitive (int, double) or Boxed (Integer)Primitive when non-null; requires explicit widening calls (e.g., .toLong()).
TextualChar, Stringchar, java.lang.StringChar cannot be assigned integer literals directly; String supports raw """ blocks.
LogicalBooleanboolean or java.lang.BooleanEvaluates truth values with standard short-circuiting logic (&&, `
Primitive ArraysIntArray, ByteArray, DoubleArrayint[], byte[], double[]Contiguous primitive memory blocks with zero object-boxing overhead.
StructuralAny, Unit, NothingObject, void / Unit, VoidAny 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:

  1. Eliminate Unnecessary Primitive Nullability: Audit your core domain models and data pipelines. Change Int? or Double? to non-nullable Int or Double whenever possible to prevent the JVM from allocating boxed object wrappers on the heap.
  2. 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.
  3. 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().
  4. Leverage Nothing for Fail-Fast Guard Functions: Use Nothing as 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!

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *