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"]
TypeSizeMinimum ValueMaximum ValueUse cases
Byte8-bit-128127Binary data, network protocols
Short16-bit-32,76832,767Rarely used
Int32-bit-2,147,483,6482,147,483,647The default for integers
Long64-bit-9.2 × 10¹⁸9.2 × 10¹⁸Large IDs, timestamps, large amounts
Float32-bit~1.4 × 10⁻⁴⁵~3.4 × 10³⁸Graphics, coordinates (low precision)
Double64-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 using Long instead of UInt.

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, Long for integers; Float, Double for decimals. Int and Double are 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 integer7 / 2 = 3, not 3.5. Convert one operand to Double first if you need a decimal result.
  • Silent integer overflowInt.MAX_VALUE + 1 produces Int.MIN_VALUE without an error. Use Long for calculations that could exceed ~2 billion.
  • Floating point isn’t precise0.1 + 0.2 ≠ 0.3 in binary representation. Use an epsilon for comparisons, or BigDecimal for full precision.
  • BigDecimal for finance — always initialize from a String (BigDecimal("19.99")), not a Double (BigDecimal(19.99) which is already imprecise). Use RoundingMode.HALF_UP for conventional rounding.
  • BigDecimal comparisons must use .compareTo() == 0, not == — because BigDecimal("2.0") != BigDecimal("2.00") even though the values are equal.
  • kotlin.math provides all mathematical functions: sqrt, abs, pow, log, trigonometric functions, and the PI, E constants.
  • Formatting with "%.2f".format(nilai) for simple formats, NumberFormat with a Locale for 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.

← Previous: Type Conversion   Next: Characters →

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