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 #
| Type | Size | Value Range |
|---|---|---|
Byte | 8 bits | -128 to 127 |
Short | 16 bits | -32,768 to 32,767 |
Int | 32 bits | -2,147,483,648 to 2,147,483,647 (~2.1 billion) |
Long | 64 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 #
| Type | Size | Precision | Approximate Range |
|---|---|---|---|
Float | 32 bits | ~6–7 decimal digits | ±3.4 × 10³⁸ |
Double | 64 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
FloatorDoublefor financial values (money). Floating-point representation can’t represent every decimal number exactly, so money calculations can produce small errors that accumulate. UseBigDecimalfor 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:
| Function | Result |
|---|---|
.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 --> DAny — 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 types —
Byte,Short,Int,Longfor integers;Float,Doublefor decimals.IntandDoubleare 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
BigDecimalfor money —FloatandDoublecan’t represent every decimal exactly; financial calculations must useBigDecimal.Charisn’t a number — unlike Java,Charin Kotlin is its own type. Convert to a number with.code, and back with.toChar().Stringis immutable — every string operation produces a new object. For repeated operations in loops, useStringBuilderorjoinToString().- Array vs List —
Arrayfor Java interop and performance-critical code;Listfor everyday Kotlin because it’s more expressive with full collection operation support.Anyis the supertype — all non-nullable types inherit fromAny.Any?also covers null values.Unitis a realvoid— the return type of functions without a meaningful value. Unlikevoid,Unitcan be used as a generic type argument.Nothingnever 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.