Numbers #
Numbers are the foundation of almost every program — product prices, GPS coordinates, scientific calculation results, user statistics. Kotlin inherits the numeric type system from the JVM but wraps it in a cleaner, safer API. There are eight built-in numeric types, each with different capacities and trade-offs. Choosing the wrong type can produce silent overflow, precision loss in financial calculations, or poor performance. This article covers all Kotlin numeric types in depth: their capacities and limits, arithmetic operations and their behavior, BigDecimal for high precision, built-in mathematical functions, number formatting for display, and idiomatic patterns that make numeric code safer and more expressive.
Numeric Types and Their Capacities #
Kotlin has eight numeric types: four integers, two decimals, and two special types for characters and booleans.
flowchart TD
A["Kotlin Numeric Types"] --> B["Integers"]
A --> C["Floating Point"]
B --> D["Byte\n8-bit\n-128 to 127"]
B --> E["Short\n16-bit\n-32,768 to 32,767"]
B --> F["Int\n32-bit\n-2.1B to 2.1B"]
B --> G["Long\n64-bit\n-9.2 Quintillion to 9.2 Quintillion"]
C --> H["Float\n32-bit\n~7 decimal digits"]
C --> I["Double\n64-bit\n~15 decimal digits"]| Type | Size | Minimum Value | Maximum Value | Use cases |
|---|---|---|---|---|
Byte | 8-bit | -128 | 127 | Binary data, network protocols |
Short | 16-bit | -32,768 | 32,767 | Rarely used |
Int | 32-bit | -2,147,483,648 | 2,147,483,647 | The default for integers |
Long | 64-bit | -9.2 × 10¹⁸ | 9.2 × 10¹⁸ | Large IDs, timestamps, large amounts |
Float | 32-bit | ~1.4 × 10⁻⁴⁵ | ~3.4 × 10³⁸ | Graphics, coordinates (low precision) |
Double | 64-bit | ~5.0 × 10⁻³²⁴ | ~1.8 × 10³⁰⁸ | The default for decimals |
// Numeric literals and their suffixes
val byteVal: Byte = 127
val shortVal: Short = 32_767
val intVal: Int = 2_147_483_647 // underscores for readability
val longVal: Long = 9_223_372_036_854_775_807L // L suffix required
val floatVal: Float = 3.14f // f suffix required
val doubleVal: Double = 3.14159265358979
// Literals in different representations
val hex = 0xFF // 255 (hexadecimal)
val biner = 0b11111111 // 255 (binary)
val oktal = 0o377 // 255 (octal — Kotlin doesn't support it, use hex)
// Important constants
println(Int.MAX_VALUE) // 2,147,483,647
println(Int.MIN_VALUE) // -2,147,483,648
println(Long.MAX_VALUE) // 9,223,372,036,854,775,807
println(Double.MAX_VALUE) // 1.7976931348623157E308
println(Double.MIN_VALUE) // 5.0E-324 (the smallest positive, not the smallest negative)
Default Types and Inference #
Kotlin has default types for numeric literals: Int for integers and Double for decimals.
// Automatic type inference
val a = 42 // Int
val b = 42L // Long (L suffix)
val c = 42.0 // Double
val d = 42.0f // Float (f suffix)
val e = 42.toByte() // Byte
// Inference from context
fun terima(nilai: Long) = println(nilai)
terima(42) // ERROR: Int can't implicitly become Long
terima(42L) // OK
terima(42.toLong()) // OK
// But in arithmetic expressions, there's automatic promotion
val intNilai: Int = 100
val longNilai: Long = 200L
val hasil = intNilai + longNilai // Int + Long = Long (automatic promotion)
// This differs from conversion — this is an arithmetic expression, not an assignment
Arithmetic Operations and Their Behavior #
Kotlin supports the standard arithmetic operators, but there are some behaviors you need to understand well.
Integer Division #
// ANTI-PATTERN: assuming Int division produces a decimal
val hasil = 7 / 2 // 3, not 3.5!
val persentase = 1 / 3 // 0, not 0.333...
// CORRECT: convert to Double before dividing
val hasilDesimal = 7.0 / 2 // 3.5
val hasilDesimal2 = 7 / 2.0 // 3.5
val hasilDesimal3 = 7.toDouble() / 2 // 3.5
// Remainder (modulo)
val sisa = 17 % 5 // 2
val sisoNeg = -17 % 5 // -2 (the sign follows the dividend in Kotlin/JVM)
// floorDiv and mod — different behavior for negatives
println((-17).floorDiv(5)) // -4 (different from -17 / 5 = -3)
println((-17).mod(5)) // 3 (always positive, unlike % which can be negative)
Integer Overflow #
// ANTI-PATTERN: unaware of overflow on Int
val batas = Int.MAX_VALUE // 2,147,483,647
val overflow = batas + 1 // -2,147,483,648 — wraps around silently!
// A dangerous real example:
fun hitungTotal(harga: Int, jumlah: Int): Int {
return harga * jumlah // can overflow if both are large!
}
hitungTotal(100_000, 100_000) // 10,000,000,000 > Int.MAX_VALUE → overflow!
// CORRECT: use Long for calculations that could be large
fun hitungTotal(harga: Long, jumlah: Long): Long {
return harga * jumlah // safe
}
// Or detect overflow with Math.multiplyExact
fun hitungTotalSafe(harga: Int, jumlah: Int): Long {
return harga.toLong() * jumlah.toLong()
}
Bitwise Operations #
val a = 0b1010_1010 // 170
val b = 0b1100_1100 // 204
println(a and b) // 0b1000_1000 = 136 (bitwise AND)
println(a or b) // 0b1110_1110 = 238 (bitwise OR)
println(a xor b) // 0b0110_0110 = 102 (bitwise XOR)
println(a.inv()) // inverts all bits
println(a shl 2) // left shift 2 bits = a * 4 = 680
println(a shr 2) // right shift 2 bits = a / 4 = 42 (signed)
println(a ushr 2) // right shift 2 bits (unsigned, fills with 0)
// Bitwise operations are useful for flags and masking
const val FLAG_AKTIF = 1 shl 0 // 0b0001
const val FLAG_ADMIN = 1 shl 1 // 0b0010
const val FLAG_PREMIUM = 1 shl 2 // 0b0100
var permissions = 0
permissions = permissions or FLAG_AKTIF or FLAG_PREMIUM // set flags
val isAktif = (permissions and FLAG_AKTIF) != 0 // true
val isAdmin = (permissions and FLAG_ADMIN) != 0 // false
val isPremium = (permissions and FLAG_PREMIUM) != 0 // true
Floating Point and Precision #
Floating point is a very common bug source because binary representation can’t represent all decimal numbers exactly.
// ANTI-PATTERN: direct floating point comparison
val a = 0.1 + 0.2
println(a == 0.3) // false! (0.1 + 0.2 = 0.30000000000000004)
println(a) // 0.30000000000000004
// CORRECT: use an epsilon for comparison
val EPSILON = 1e-10
fun Double.hampirSamaDengan(lain: Double, eps: Double = EPSILON): Boolean {
return Math.abs(this - lain) < eps
}
println((0.1 + 0.2).hampirSamaDengan(0.3)) // true
// Special Float/Double values
println(1.0 / 0.0) // Infinity
println(-1.0 / 0.0) // -Infinity
println(0.0 / 0.0) // NaN (Not a Number)
val nan = Double.NaN
println(nan == nan) // false! NaN is not equal to itself
println(nan.isNaN()) // true — the correct way to check NaN
println(Double.POSITIVE_INFINITY.isInfinite()) // true
// ANTI-PATTERN: not checking NaN or Infinity before use
fun hitungRasio(pembilang: Double, penyebut: Double): Double {
return pembilang / penyebut // can be NaN or Infinity!
}
// CORRECT: validate and handle special cases
fun hitungRasio(pembilang: Double, penyebut: Double): Double? {
if (penyebut == 0.0) return null
val hasil = pembilang / penyebut
return if (hasil.isFinite()) hasil else null
}
BigDecimal — High Precision for Finance #
Double and Float aren’t suitable for financial calculations because of floating point imprecision. Use BigDecimal for numbers requiring exact precision.
import java.math.BigDecimal
import java.math.MathContext
import java.math.RoundingMode
// The problem with Double for finance
val harga = 19.99
val pajak = 0.11
val total = harga + harga * pajak
println(total) // 22.1889 — maybe 22.18890000000000... internally
// CORRECT: BigDecimal for finance
val hargaBD = BigDecimal("19.99") // ALWAYS use a String, not a Double!
val pajakBD = BigDecimal("0.11")
val totalBD = hargaBD + hargaBD * pajakBD
println(totalBD) // 22.1889 — exact
// ANTI-PATTERN: BigDecimal from a Double
val salah = BigDecimal(0.1) // 0.1000000000000000055511151231257827021181583404541015625
val benar = BigDecimal("0.1") // 0.1
// BigDecimal operations
val a = BigDecimal("100.50")
val b = BigDecimal("33.33")
val tambah = a + b // 133.83
val kurang = a - b // 67.17
val kali = a * b // 3349.665
val bagi = a.divide(b, 2, RoundingMode.HALF_UP) // 3.02
// Rounding
val nilai = BigDecimal("123.456789")
val bulatDua = nilai.setScale(2, RoundingMode.HALF_UP) // 123.46
val bulatNol = nilai.setScale(0, RoundingMode.HALF_UP) // 123
// Available rounding modes
RoundingMode.HALF_UP // 2.5 → 3 (conventional)
RoundingMode.HALF_DOWN // 2.5 → 2
RoundingMode.HALF_EVEN // 2.5 → 2, 3.5 → 4 (banker's rounding)
RoundingMode.CEILING // always up: 2.1 → 3
RoundingMode.FLOOR // always down: 2.9 → 2
RoundingMode.UP // away from zero: -2.1 → -3
RoundingMode.DOWN // toward zero: -2.9 → -2
// Comparing BigDecimals — use compareTo, not equals!
val x = BigDecimal("2.0")
val y = BigDecimal("2.00")
println(x == y) // false! (different scales: 1 vs 2)
println(x.compareTo(y) == 0) // true — the mathematical values are equal
// Convenience extension functions
fun Double.toBigDecimalSafe() = toBigDecimal().setScale(2, RoundingMode.HALF_UP)
fun Long.toRupiah() = BigDecimal(this).setScale(0)
A Complete Financial Calculation #
data class ItemPesanan(val nama: String, val harga: BigDecimal, val jumlah: Int)
fun hitungTotalPesanan(
items: List<ItemPesanan>,
diskonPersen: BigDecimal = BigDecimal.ZERO,
pajakPersen: BigDecimal = BigDecimal("0.11")
): Map<String, BigDecimal> {
val subtotal = items.fold(BigDecimal.ZERO) { acc, item ->
acc + item.harga * BigDecimal(item.jumlah)
}
val diskon = subtotal * diskonPersen / BigDecimal("100")
val setelahDiskon = subtotal - diskon
val pajak = setelahDiskon * pajakPersen
val total = setelahDiskon + pajak
return mapOf(
"subtotal" to subtotal.setScale(2, RoundingMode.HALF_UP),
"diskon" to diskon.setScale(2, RoundingMode.HALF_UP),
"setelahDiskon" to setelahDiskon.setScale(2, RoundingMode.HALF_UP),
"pajak" to pajak.setScale(2, RoundingMode.HALF_UP),
"total" to total.setScale(2, RoundingMode.HALF_UP)
)
}
val items = listOf(
ItemPesanan("Laptop", BigDecimal("15000000"), 1),
ItemPesanan("Mouse", BigDecimal("250000"), 2),
ItemPesanan("Keyboard", BigDecimal("800000"), 1)
)
val rincian = hitungTotalPesanan(items, diskonPersen = BigDecimal("10"))
// subtotal → 16,300,000.00
// diskon → 1,630,000.00
// setelahDiskon → 14,670,000.00
// pajak → 1,613,700.00
// total → 16,283,700.00
Mathematical Functions #
The Kotlin Standard Library provides mathematical functions through kotlin.math — no need to explicitly import java.lang.Math.
import kotlin.math.*
// Basic functions
println(abs(-42)) // 42
println(abs(-3.14)) // 3.14
println(sqrt(16.0)) // 4.0
println(cbrt(27.0)) // 3.0 (cube root)
println(pow(2.0, 10.0)) // 1024.0
println(2.0.pow(10.0)) // 1024.0 (extension version)
// Rounding
println(floor(3.7)) // 3.0 (down)
println(ceil(3.2)) // 4.0 (up)
println(round(3.5)) // 4 (nearest, .5 goes up)
println(truncate(3.9)) // 3.0 (truncate the decimal)
// Logarithms and exponentials
println(ln(E)) // 1.0 (natural log)
println(log10(1000.0)) // 3.0
println(log2(1024.0)) // 10.0
println(log(8.0, 2.0)) // 3.0 (custom base log)
println(exp(1.0)) // 2.718... (e^1)
// Trigonometry (in radians)
println(sin(PI / 2)) // 1.0
println(cos(0.0)) // 1.0
println(tan(PI / 4)) // 1.0 (approximately, due to floating point)
println(asin(1.0)) // PI/2 = 1.5707...
println(atan2(1.0, 1.0)) // PI/4 = 0.7853...
// Constants
println(PI) // 3.141592653589793
println(E) // 2.718281828459045
// min and max
println(min(3, 7)) // 3
println(max(3.14, 2.71)) // 3.14
println(min(3, 7, 1, 5)) // doesn't exist — Kotlin has no min vararg
println(listOf(3, 7, 1, 5).min()) // 1 — use a collection
Practical Mathematical Functions #
// Distance between two points (Euclidean distance)
data class Titik(val x: Double, val y: Double)
fun jarak(a: Titik, b: Titik): Double {
val dx = a.x - b.x
val dy = a.y - b.y
return sqrt(dx * dx + dy * dy)
// or: hypot(dx, dy) — more accurate for extreme values
}
// Round up to a specific multiple
fun bulatKeKelipatan(nilai: Int, kelipatan: Int): Int {
return ((nilai + kelipatan - 1) / kelipatan) * kelipatan
}
bulatKeKelipatan(17, 5) // 20
bulatKeKelipatan(20, 5) // 20
bulatKeKelipatan(21, 5) // 25
// Clamp a value within a range
fun Double.clamp(min: Double, max: Double) = coerceIn(min, max)
3.7.clamp(0.0, 3.0) // 3.0
(-1.5).clamp(0.0, 1.0) // 0.0
0.5.clamp(0.0, 1.0) // 0.5
// Linear interpolation
fun lerp(start: Double, end: Double, t: Double): Double {
return start + (end - start) * t.coerceIn(0.0, 1.0)
}
lerp(0.0, 100.0, 0.0) // 0.0
lerp(0.0, 100.0, 0.5) // 50.0
lerp(0.0, 100.0, 1.0) // 100.0
lerp(0.0, 100.0, 0.75) // 75.0
Formatting Numbers #
Displaying numbers with the right format is an important skill, especially for user-facing applications.
import java.text.NumberFormat
import java.util.Locale
// Basic formatting with Strings
val n = 1_234_567.89
// Kotlin string templates — suitable for simple formatting
println("Value: $n") // Value: 1234567.89
println("Value: %.2f".format(n)) // Value: 1234567.89
println("Value: %,.2f".format(n)) // Value: 1,234,567.89 (with separators)
println("Percent: %.1f%%".format(85.5)) // Percent: 85.5%
println("Hex: %X".format(255)) // Hex: FF
println("Padding: %10d".format(42)) // Padding: 42 (width 10)
println("Padding: %-10d|".format(42)) // Padding: 42 | (left-aligned)
println("Zero pad: %05d".format(42)) // Zero pad: 00042
// NumberFormat — for locale-aware formatting
val localeID = Locale("id", "ID")
val formatRupiah = NumberFormat.getCurrencyInstance(localeID)
println(formatRupiah.format(15_000_000.0)) // Rp15.000.000,00
val formatAngka = NumberFormat.getNumberInstance(localeID)
println(formatAngka.format(1_234_567.89)) // 1.234.567,89
// Different locales for comparison
val formatUS = NumberFormat.getCurrencyInstance(Locale.US)
println(formatUS.format(15_000_000.0)) // $15,000,000.00
// Percentage formatting
val formatPersen = NumberFormat.getPercentInstance()
formatPersen.minimumFractionDigits = 1
println(formatPersen.format(0.1234)) // 12.3%
// Rupiah extension functions — useful in Indonesian projects
fun Double.formatRupiah(): String {
val format = NumberFormat.getCurrencyInstance(Locale("id", "ID"))
return format.format(this)
}
fun Long.formatRupiah(): String = toDouble().formatRupiah()
fun Double.formatPersen(desimal: Int = 1): String =
"%.${desimal}f%%".format(this * 100)
println(15_000_000.0.formatRupiah()) // Rp15.000.000,00
println(0.856.formatPersen()) // 85.6%
println(0.856.formatPersen(2)) // 85.60%
Unsigned Integers #
Kotlin has supported unsigned integers since version 1.5 — useful for working with binary data, network protocols, or when negative values are meaningless.
// Unsigned types
val uByte: UByte = 255u // 0 to 255
val uShort: UShort = 65_535u // 0 to 65,535
val uInt: UInt = 4_294_967_295u // 0 to 4,294,967,295
val uLong: ULong = 18_446_744_073_709_551_615u // 0 to 2^64-1
// Comparison with signed
val signed: Int = -1
val unsigned: UInt = signed.toUInt()
println(unsigned) // 4294967295 — interpreted as unsigned
// Conversions
val u: UInt = 100u
val s: Int = u.toInt() // 100
val uDariS: UInt = 100.toUInt()
// Operations — the same as signed types
val a: UInt = 10u
val b: UInt = 3u
println(a + b) // 13
println(a - b) // 7
println(a * b) // 30
println(a / b) // 3
println(a % b) // 1
// Useful for port numbers, file sizes, checksums
val portNumber: UShort = 8080u
val fileSize: ULong = 1_073_741_824u // 1 GB in bytes
val checksum: UInt = hitungCRC32(data)
Unsigned integers in Kotlin are still inline classes compiled to signed types on the JVM. This means a small overhead when used as generics or nullable. For critical JVM performance, consider still usingLonginstead ofUInt.
Idiomatic Patterns for Numeric Code #
Avoid Magic Numbers #
// ANTI-PATTERN: meaningless magic numbers
fun hitungGaji(jam: Int, lembur: Int): Double {
return (jam * 50_000 + lembur * 75_000).toDouble()
}
// CORRECT: named constants
const val TARIF_JAM_NORMAL = 50_000L
const val TARIF_JAM_LEMBUR = 75_000L
fun hitungGaji(jam: Int, lembur: Int): Long {
return jam * TARIF_JAM_NORMAL + lembur * TARIF_JAM_LEMBUR
}
Use Long for Money in the Smallest Unit #
// The pattern used by the finance industry: store in the smallest unit
// Rp 15.000 is stored as 1_500_000 (in sen/paisa)
// USD 19.99 is stored as 1999 (in cents)
data class Uang(val jumlah: Long, val satuan: String = "IDR") {
// the amount is in the smallest unit (sen)
val rupiah: Double get() = jumlah / 100.0
operator fun plus(lain: Uang): Uang {
require(satuan == lain.satuan) { "Can't add different currencies" }
return Uang(jumlah + lain.jumlah, satuan)
}
operator fun times(faktor: Int) = Uang(jumlah * faktor, satuan)
fun format(): String = "Rp ${"%,.0f".format(rupiah)}"
}
val harga = Uang(1_500_000) // Rp 15.000,00
val ongkir = Uang(1_500_000) // Rp 15.000,00
val total = (harga + ongkir) * 2
println(total.format()) // Rp 60.000
Simple Statistics #
// Reusable statistics functions
fun List<Double>.rata(): Double = if (isEmpty()) 0.0 else sum() / size
fun List<Double>.median(): Double {
if (isEmpty()) return 0.0
val terurut = sorted()
val tengah = size / 2
return if (size % 2 == 0) {
(terurut[tengah - 1] + terurut[tengah]) / 2.0
} else {
terurut[tengah]
}
}
fun List<Double>.varians(): Double {
val mean = rata()
return map { (it - mean).pow(2) }.rata()
}
fun List<Double>.stdDev(): Double = sqrt(varians())
val data = listOf(2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0)
println(data.rata()) // 5.0
println(data.median()) // 4.5
println(data.varians()) // 4.0
println(data.stdDev()) // 2.0
Which Numeric Type to Use When #
Use Int if:
✓ The default — enough for most calculations
✓ Indices, counters, item counts, ages, years
✓ Values guaranteed not to exceed ~2 billion
Use Long if:
✓ Timestamps (milliseconds since the epoch)
✓ Large database IDs (large auto-increments)
✓ File sizes in bytes
✓ Money totals in the smallest unit
✓ Results of multiplying two Ints that might overflow
Use Double if:
✓ GPS coordinates, graphics coordinates
✓ Scientific calculation results
✓ Percentages, ratios, averages
✗ Avoid for financial calculations
Use BigDecimal if:
✓ Prices, salaries, taxes, discounts
✓ Any accounting calculation
✓ Values that must be exact, not approximate
Use Float only if:
✓ Working with graphics APIs (OpenGL, shaders)
✓ Critical performance and memory, low precision is enough
✗ Avoid for general calculations — use Double
Summary #
- Eight numeric types:
Byte,Short,Int,Longfor integers;Float,Doublefor decimals.IntandDoubleare the most commonly used defaults.- No implicit widening — every numeric conversion must be explicit with
.toLong(),.toDouble(), etc. This prevents bugs from unexpected conversions.- Integer division is always integer —
7 / 2 = 3, not3.5. Convert one operand toDoublefirst if you need a decimal result.- Silent integer overflow —
Int.MAX_VALUE + 1producesInt.MIN_VALUEwithout an error. UseLongfor calculations that could exceed ~2 billion.- Floating point isn’t precise —
0.1 + 0.2 ≠ 0.3in binary representation. Use an epsilon for comparisons, orBigDecimalfor full precision.BigDecimalfor finance — always initialize from aString(BigDecimal("19.99")), not aDouble(BigDecimal(19.99)which is already imprecise). UseRoundingMode.HALF_UPfor conventional rounding.BigDecimalcomparisons must use.compareTo() == 0, not==— becauseBigDecimal("2.0") != BigDecimal("2.00")even though the values are equal.kotlin.mathprovides all mathematical functions:sqrt,abs,pow,log, trigonometric functions, and thePI,Econstants.- Formatting with
"%.2f".format(nilai)for simple formats,NumberFormatwith aLocalefor region-aware formats (including Rupiah).- Store money in the smallest unit (sen, paisa) as a
Long— safer than floating point and avoids rounding problems in financial calculations.