Data Types #

Every value in a program has a type — and the type determines what you can do with that value. In Java, there’s a fundamental difference between primitive types (int, double, boolean) and object types (Integer, Double, Boolean). Kotlin removes this distinction: all types are objects. But don’t worry about performance — the Kotlin compiler is smart enough to use JVM primitive representations behind the scenes when possible. The result: cleaner code without manual boxing/unboxing, with equivalent performance. This article covers all of Kotlin’s built-in data types in depth, including conversion methods, available operations, and special types that don’t exist in other languages.

Numeric Types #

Kotlin provides six numeric types: four for integers and two for decimal numbers.

Integers #

TypeSizeValue Range
Byte8 bits-128 to 127
Short16 bits-32,768 to 32,767
Int32 bits-2,147,483,648 to 2,147,483,647 (~2.1 billion)
Long64 bits-9.2 × 10¹⁸ to 9.2 × 10¹⁸

For most everyday needs, Int is sufficient. Use Long for values that exceed Int’s capacity — like Unix timestamps in milliseconds, very large database IDs, or byte counts of large files.

val age: Byte = 25
val year: Short = 2024
val cityPopulation: Int = 10_500_000
val timestampMs: Long = System.currentTimeMillis()

// L suffix for Long literals
val starDistanceMeters = 9_461_000_000_000_000L  // 1 light-year in meters

Kotlin supports underscores as digit separators for readability — the compiler ignores them completely:

val oneMillion = 1_000_000
val hexColor = 0xFF_33_AA
val binary = 0b1010_1100_1111_0000
val octalBytes = 255

Floating-Point Numbers #

TypeSizePrecisionApproximate Range
Float32 bits~6–7 decimal digits±3.4 × 10³⁸
Double64 bits~15–16 decimal digits±1.7 × 10³⁰⁸

Double is the default — a 3.14 literal without a suffix is inferred as Double. Use the f or F suffix for Float:

val temperature: Double = 36.6          // Double (default)
val tempFloat: Float = 36.6f            // Float — needs the f suffix

val pi = 3.141592653589793              // Double — full precision
val piFloat = 3.1415927f                // Float — truncated precision

Don’t use Float or Double for financial values (money). Floating-point representation can’t represent every decimal number exactly, so money calculations can produce small errors that accumulate. Use BigDecimal for money.

// ANTI-PATTERN: money calculations with Double
val price = 0.1 + 0.2
println(price)  // 0.30000000000000004 — not 0.3!

// CORRECT: use BigDecimal
import java.math.BigDecimal
val exactPrice = BigDecimal("0.1") + BigDecimal("0.2")
println(exactPrice)  // 0.3

Numeric Type Conversion #

Kotlin doesn’t perform implicit numeric conversion — unlike Java. You must always convert explicitly using the available conversion functions.

val intNumber: Int = 42

// ANTI-PATTERN: direct assignment between numeric types
val longNumber: Long = intNumber  // ✗ compilation error in Kotlin!

// CORRECT: explicit conversion
val longNumber: Long = intNumber.toLong()
val doubleNumber: Double = intNumber.toDouble()
val floatNumber: Float = intNumber.toFloat()
val byteNumber: Byte = intNumber.toByte()
val shortNumber: Short = intNumber.toShort()

The conversion functions available for all numeric types:

FunctionResult
.toByte()Byte
.toShort()Short
.toInt()Int
.toLong()Long
.toFloat()Float
.toDouble()Double
.toChar()Char
.toString()String

Converting Strings to Numbers #

val text = "42"
val number = text.toInt()          // 42
val decimal = "3.14".toDouble()    // 3.14

// Safe conversion — returns null on failure, not an exception
val valid = "123".toIntOrNull()      // 123
val invalid = "abc".toIntOrNull()    // null
val failed = "99.9".toIntOrNull()    // null (not an integer)

// Use Elvis for a default value
val value = "not a number".toIntOrNull() ?: 0  // 0

Overflow — Watch the Limits #

val max = Int.MAX_VALUE         // 2_147_483_647
val overflow = max + 1          // -2_147_483_648 — not an error, but wrong!

// CORRECT: use Long if the value can exceed Int.MAX_VALUE
val safeMax = Int.MAX_VALUE.toLong() + 1  // 2_147_483_648L

Char — The Character Type #

Char stores a single Unicode character. Written with single quotes.

val letter: Char = 'A'
val digitChar: Char = '7'
val symbol: Char = '@'
val unicode: Char = '\u0041'  // 'A' in Unicode escape notation

println(letter.code)           // 65 — ASCII/Unicode code value
println(letter.isLetter())     // true
println(letter.isUpperCase())  // true
println(letter.lowercaseChar()) // 'a'

Char in Kotlin is its own type — not a number. In Java, char can be used directly as an int. In Kotlin, conversion must be explicit:

val character = 'A'

// ANTI-PATTERN: treating Char as a number directly
val code: Int = character  // ✗ compilation error

// CORRECT: explicit conversion
val code: Int = character.code        // 65
val charCode: Char = 65.toChar()      // 'A'

Escape Characters #

val newline = '\n'     // new line
val tab = '\t'         // tab
val backslash = '\\'   // backslash
val singleQuote = '\'' // single quote
val unicode = '\u2764' // ❤ — a Unicode character

Boolean — The Logical Type #

Boolean has only two values: true and false. Used for conditions, flags, and control flow.

val active: Boolean = true
val isVerified = false   // type inference

// Logical operators
val logicalAnd = true && false   // false — AND
val logicalOr = true || false    // true — OR
val negation = !true             // false — NOT

// Short-circuit evaluation
// && stops evaluating if the left side is already false
// || stops evaluating if the left side is already true
val safe = list.isNotEmpty() && list[0] != null

Useful Boolean Extension Functions #

val condition = 10 > 5

// takeIf and takeUnless
val value = condition.takeIf { it }     // true if the condition is true, null if false

// Conversion to String
println(condition.toString())  // "true"

String — The Text Type #

String stores an immutable sequence of characters. Every operation on a string produces a new string, not modifying the existing one.

val greeting = "Hello, World!"
val empty = ""
val whitespace = "   "

println(greeting.length)         // 12
println(greeting.uppercase())    // HELLO, WORLD!
println(greeting.lowercase())    // hello, world!
println(greeting.reversed())     // !dlroW ,olleH
println(greeting.isEmpty())      // false
println(empty.isEmpty())         // true
println(whitespace.isBlank())    // true — isBlank() checks isEmpty() or only whitespace

String Templates #

val name = "Budi"
val age = 28

// Variable interpolation
val introduction = "My name is $name, age $age."

// Expression interpolation
val info = "Name length: ${name.length} characters"
val status = "Status: ${if (age >= 18) "Adult" else "Child"}"
val calculation = "Twice the age: ${age * 2}"

Raw Strings (Multiline) #

Raw strings use triple quotes and preserve all characters including newlines and tabs — without needing escapes.

val json = """
    {
        "name": "Budi Santoso",
        "age": 28,
        "city": "Jakarta"
    }
""".trimIndent()

val query = """
    SELECT u.name, u.email
    FROM pengguna u
    WHERE u.active = true
      AND u.age >= 18
    ORDER BY u.name
""".trimIndent()

println(json)

.trimIndent() removes the uniform leading indentation across all lines — the result is a string without excessive indentation.

Commonly Used String Operations #

val text = "  Kotlin Programming  "

// Cleaning
println(text.trim())           // "Kotlin Programming"
println(text.trimStart())      // "Kotlin Programming  "
println(text.trimEnd())        // "  Kotlin Programming"

// Checks
println(text.contains("Kotlin"))        // true
println(text.startsWith("  Kot"))       // true
println(text.endsWith("ing  "))         // true

// Manipulation
println(text.replace("Kotlin", "Swift")) // "  Swift Programming  "
println(text.trim().split(" "))          // [Kotlin, Programming]

val word = "kotlin"
println(word.capitalize())             // Kotlin (deprecated in recent versions)
println(word.replaceFirstChar { it.uppercase() })  // Kotlin (the modern way)

// Substring
val language = "Kotlin Programming"
println(language.substring(0, 6))        // Kotlin
println(language.substringAfter(" "))    // Programming
println(language.substringBefore(" "))   // Kotlin

String Immutability and StringBuilder #

Because String is immutable, repeated operations that build a string (like in a loop) should use StringBuilder:

// ANTI-PATTERN: string concatenation in a loop — creates a new object every iteration
var result = ""
for (i in 1..1000) {
    result += "item-$i,"  // very inefficient for large loops
}

// CORRECT: use StringBuilder
val builder = StringBuilder()
for (i in 1..1000) {
    builder.append("item-$i,")
}
val result = builder.toString()

// Or use joinToString for collections
val result = (1..1000).joinToString(",") { "item-$it" }

Array #

An Array is a fixed-size data structure that stores elements of the same type. Its size can’t be changed after creation.

// Generic array
val fruits: Array<String> = arrayOf("Mangga", "Apel", "Jeruk")
val numbers: Array<Int> = arrayOf(1, 2, 3, 4, 5)

// Array with a size and default values
val zeros = Array(5) { 0 }              // [0, 0, 0, 0, 0]
val squares = Array(5) { i -> i * i }   // [0, 1, 4, 9, 16]

// Element access
println(fruits[0])      // Mangga
println(fruits.size)    // 3

fruits[1] = "Pisang"    // modify a value
println(fruits[1])      // Pisang

Primitive Arrays — More Efficient #

For numeric types, Kotlin provides primitive arrays that don’t box — their performance matches Java primitive arrays:

val byteArray = byteArrayOf(1, 2, 3)
val shortArray = shortArrayOf(10, 20, 30)
val intArray = intArrayOf(1, 2, 3, 4, 5)
val longArray = longArrayOf(1L, 2L, 3L)
val floatArray = floatArrayOf(1.0f, 2.5f, 3.7f)
val doubleArray = doubleArrayOf(1.0, 2.5, 3.14)
val booleanArray = booleanArrayOf(true, false, true)
val charArray = charArrayOf('K', 'o', 't', 'l', 'i', 'n')

Array vs List — When to Choose #

USE Array if:
  ✓ Working with Java APIs that require arrays
  ✓ Performance-critical and you need primitive arrays
  ✓ The size is fixed and won't change
  ✓ Interoperability with Java code

USE List if:
  ✓ The size may change (MutableList)
  ✓ You need collection operations: filter, map, sortedBy, etc.
  ✓ More expressive and idiomatic Kotlin code
  ✓ The majority of everyday use cases

Special Types: Any, Unit, Nothing #

These are three types at the top and bottom of Kotlin’s type hierarchy — and each has a unique role.

flowchart TD
    A["Any\n(supertype of all non-null types)"] --> B[String]
    A --> C[Int]
    A --> D[Boolean]
    A --> E[Custom classes...]
    F["Any?\n(supertype of all types including null)"] --> A
    F --> G[null]
    H["Nothing\n(subtype of all types — never has a value)"] --> B
    H --> C
    H --> D

Any — Supertype of All Types #

Any is the parent of all non-nullable types in Kotlin. Equivalent to Object in Java, but cleaner because it doesn’t have to deal with primitives.

val anything: Any = "Can hold anything"
val also: Any = 42
val or: Any = true
val even: Any = listOf(1, 2, 3)

// Any has three basic methods
println(anything.toString())       // string representation
println(anything.hashCode())       // hash code
println(anything.equals("test"))   // equality comparison

// Type check and smart cast
fun describe(value: Any): String {
    return when (value) {
        is String  -> "String with length ${value.length}"
        is Int     -> "Integer: ${value * 2}"
        is Boolean -> "Boolean: ${if (value) "true" else "false"}"
        is List<*> -> "List with ${value.size} elements"
        else       -> "Unknown type: ${value::class.simpleName}"
    }
}

println(describe("Kotlin"))   // String with length 6
println(describe(21))         // Integer: 42
println(describe(true))       // Boolean: true

Unit — The void Replacement #

Unit is the return type for functions that don’t return a meaningful value. It’s equivalent to void in Java, but Unit is a real type — it can be stored in a variable and used as a generic type argument.

// These two ways are identical — Unit can be written explicitly or omitted
fun printMessage(message: String): Unit {
    println(message)
}

fun printMessage2(message: String) {  // Unit is implied
    println(message)
}

// Unit is useful in generics
val action: () -> Unit = { println("Executed!") }
val list: List<() -> Unit> = listOf(
    { println("Action 1") },
    { println("Action 2") }
)

list.forEach { it() }

Nothing — The Type That Never Exists #

Nothing is a type that never has an instance. It’s a subtype of every other type, which means it can be used anywhere any type is expected. Used for functions that never return normally — always throwing an exception or running forever.

// A function that always throws an exception
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

// Useful for validation that ends execution
fun getValue(map: Map<String, Int>, key: String): Int {
    return map[key] ?: fail("Key '$key' not found")
    // The compiler knows: if map[key] is null, fail() never returns
    // So this expression always produces an Int — safe!
}

// A function that runs forever
fun loopForever(): Nothing {
    while (true) {
        Thread.sleep(1000)
        println("Still running...")
    }
}

Nothing is also used as the type of a throw expression:

val name: String = getNameFromDb()
    ?: throw IllegalStateException("Name must exist")
// The type of a throw expression is Nothing
// Nothing is a subtype of String
// So the whole expression is of type String ✓

Kotlin’s Type System: Non-Nullable and Nullable #

Every type in Kotlin comes in two versions: non-nullable (default) and nullable (with ?).

flowchart LR
    A["String\n(can't be null)"] -- "add ?" --> B["String?\n(can be null or String)"]
    C["Int\n(can't be null)"] -- "add ?" --> D["Int?\n(can be null or Int)"]
    E["Any\n(all non-null)"] -- "add ?" --> F["Any?\n(all types including null)"]
// Non-nullable — the compiler guarantees it's never null
var name: String = "Budi"
var age: Int = 25

// Nullable — must be handled before use
var address: String? = null
var optionalValue: Int? = null

// Type checking with is
fun processValue(value: Any?) {
    when {
        value == null        -> println("Null")
        value is String      -> println("String: $value")
        value is Int         -> println("Int: $value")
        value is Double      -> println("Double: $value")
        else                 -> println("Other type: ${value::class.simpleName}")
    }
}

Safe Type Casting #

val value: Any = "Kotlin"

// Safe cast — returns null on failure (not an exception)
val text: String? = value as? String    // "Kotlin"
val number: Int? = value as? Int        // null — not an Int

// Unsafe cast — exception on failure
val text2: String = value as String     // OK
// val number2: Int = value as Int      // ✗ ClassCastException!

Summary #

  • Six numeric typesByte, Short, Int, Long for integers; Float, Double for decimals. Int and Double are the defaults for each category.
  • No implicit conversion — Kotlin forces explicit conversion between numeric types with .toInt(), .toLong(), .toDouble(), etc. This prevents accidental bugs.
  • Use BigDecimal for moneyFloat and Double can’t represent every decimal exactly; financial calculations must use BigDecimal.
  • Char isn’t a number — unlike Java, Char in Kotlin is its own type. Convert to a number with .code, and back with .toChar().
  • String is immutable — every string operation produces a new object. For repeated operations in loops, use StringBuilder or joinToString().
  • Array vs ListArray for Java interop and performance-critical code; List for everyday Kotlin because it’s more expressive with full collection operation support.
  • Any is the supertype — all non-nullable types inherit from Any. Any? also covers null values.
  • Unit is a real void — the return type of functions without a meaningful value. Unlike void, Unit can be used as a generic type argument.
  • Nothing never exists — the type of functions that never return normally (always throwing or looping forever). Useful as a signal to the compiler that certain code branches are unreachable.
  • Every type comes in two versions — non-nullable (default, safe) and nullable (with ?, must be handled). Choose non-nullable as the default; add ? only if null is semantically valid.

← Previous: Constants   Next: Operators →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact