Type Conversion #
Kotlin tidak melakukan konversi tipe secara implisit — ini adalah keputusan desain yang disengaja. Di Java, kamu bisa mengisi variabel long dengan nilai int tanpa melakukan apapun. Di Kotlin, ini adalah error kompilasi. Setiap konversi harus eksplisit dan disengaja. Terdengar membatasi? Justru sebaliknya — ini menghilangkan seluruh kelas bug yang muncul dari konversi implisit yang tidak terduga, overflow yang diam-diam, dan ClassCastException yang baru meledak di runtime. Kotlin menyediakan mekanisme konversi yang lengkap: dari fungsi konversi numerik, safe casting dengan as?, smart cast yang dibantu compiler, hingga konversi antar collection. Artikel ini membahas semuanya beserta pola idiomatik yang membuat konversi tipe aman dan ekspresif.
Mengapa Tidak Ada Implicit Conversion #
Sebelum masuk ke mekanisme konversi, penting memahami mengapa Kotlin memilih jalur ini.
// Di Java, ini valid — implicit widening:
// int i = 100;
// long l = i; // OK, tidak perlu cast
// Di Kotlin, ini ERROR:
val i: Int = 100
val l: Long = i // ERROR: Type mismatch. Required: Long. Found: Int.
// Harus eksplisit:
val l: Long = i.toLong()
// Mengapa ini penting? Lihat contoh ini:
fun prosesAngka(nilai: Long) { println(nilai) }
val angka: Int = 100
prosesAngka(angka) // ERROR — tidak bisa lewat implisit
prosesAngka(angka.toLong()) // Benar — konversi eksplisit dan jelas
flowchart LR
A["Java\nImplicit Widening"] --> B["int → long\nOK tanpa cast"]
A --> C["Bisa menyebabkan\nbug yang sulit dilacak"]
D["Kotlin\nExplicit Conversion"] --> E["Int → Long\nharus .toLong()"]
D --> F["Konversi selalu\nterlihat dan disengaja"]Konversi Antar Tipe Numerik #
Setiap tipe numerik Kotlin (Byte, Short, Int, Long, Float, Double) memiliki fungsi konversi ke tipe lain.
Fungsi Konversi Standar #
val angka: Int = 42
// Ke tipe yang lebih besar (widening)
val l: Long = angka.toLong() // 42L
val d: Double = angka.toDouble() // 42.0
val f: Float = angka.toFloat() // 42.0f
// Ke tipe yang lebih kecil (narrowing — bisa kehilangan presisi!)
val b: Byte = angka.toByte() // 42
val s: Short = angka.toShort() // 42
// Dari Double ke Int — bagian desimal dibuang (bukan dibulatkan)
val pi = 3.14159
val piInt = pi.toInt() // 3, bukan 3 atau 4
val piLong = pi.toLong() // 3L
// Dari String ke numerik (akan crash jika tidak valid!)
val teks = "123"
val n = teks.toInt() // 123
val nn = "abc".toInt() // NumberFormatException!
Overflow saat Narrowing #
Konversi ke tipe yang lebih kecil bisa menghasilkan nilai yang tidak terduga karena overflow — bit yang tidak muat akan dipotong.
// ANTI-PATTERN: narrowing tanpa pertimbangan overflow
val besarSekali: Int = 300
val kecil: Byte = besarSekali.toByte() // overflow! hasilnya 44, bukan 300
val jutaan: Long = 1_000_000_000_000L
val overflow: Int = jutaan.toInt() // overflow! hasilnya -727_379_968
// BENAR: cek range sebelum narrowing jika nilai tidak diketahui
fun safeToInt(nilai: Long): Int? {
return if (nilai in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) {
nilai.toInt()
} else null
}
// Atau gunakan coerceIn untuk membatasi nilai
val dibatasi = jutaan.coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
Konversi dariDoublekeIntdengan.toInt()memotong bagian desimal, bukan membulatkan. Jika kamu butuh pembulatan, gunakankotlin.math.round(nilai).toInt()atauMath.round(nilai).toInt()terlebih dahulu.
Konversi dengan Representasi Berbeda #
// Karakter ke Int (kode ASCII/Unicode)
val huruf = 'A'
val kode = huruf.code // 65
val kembali = kode.toChar() // 'A'
// Int ke representasi berbeda
val n = 255
val hex = n.toString(16) // "ff"
val biner = n.toString(2) // "11111111"
val oktal = n.toString(8) // "377"
// Parsing dari representasi berbeda
val dariHex = "ff".toInt(16) // 255
val dariBiner = "11111111".toInt(2) // 255
// Bit operations
val a = 0b1010 // 10
val b = 0b1100 // 12
val dan = a and b // 8 (0b1000)
val atau = a or b // 14 (0b1110)
val xor = a xor b // 6 (0b0110)
val inv = a.inv() // -11
val geser = a shl 1 // 20 (shift kiri 1 bit)
Konversi String ke Numerik — Safe vs Unsafe #
Ini adalah salah satu sumber bug paling umum: mengkonversi input pengguna atau data eksternal ke angka tanpa antisipasi kegagalan.
Unsafe Conversion — Jangan di Input Pengguna #
// ANTI-PATTERN: langsung toInt() tanpa penanganan error
fun prosesUsia(input: String): Int {
return input.toInt() // crash jika input bukan angka!
}
prosesUsia("25") // OK: 25
prosesUsia("") // NumberFormatException!
prosesUsia("dua") // NumberFormatException!
prosesUsia("25.5") // NumberFormatException!
Safe Conversion dengan OrNull #
Kotlin menyediakan varian *OrNull untuk setiap konversi — mengembalikan null jika konversi gagal, bukan melempar exception.
// BENAR: gunakan OrNull untuk input yang tidak terpercaya
fun prosesUsia(input: String): Int? {
return input.toIntOrNull()
}
"25".toIntOrNull() // 25
"".toIntOrNull() // null
"dua".toIntOrNull() // null
"25.5".toIntOrNull() // null (bukan Double yang valid untuk Int)
"25.5".toDoubleOrNull() // 25.5 (valid sebagai Double)
// Semua tipe punya varian OrNull
"3.14".toFloatOrNull() // 3.14f
"100".toLongOrNull() // 100L
"0.5".toDoubleOrNull() // 0.5
// Kombinasi dengan Elvis untuk nilai default
val usia = input.toIntOrNull() ?: 0
val harga = inputHarga.toDoubleOrNull() ?: 0.0
// Kombinasi dengan let untuk validasi lanjutan
val usiaValid = input
.toIntOrNull()
?.takeIf { it in 0..150 }
?: throw IllegalArgumentException("Usia tidak valid: $input")
// Fungsi validasi yang reusable
fun String.toIntOrDefault(default: Int = 0) = toIntOrNull() ?: default
fun String.toDoubleOrDefault(default: Double = 0.0) = toDoubleOrNull() ?: default
val n = "abc".toIntOrDefault(99) // 99
Parsing Numerik dari String dengan Format Khusus #
import java.text.NumberFormat
import java.util.Locale
// Parsing angka dengan separator ribuan
fun parseAngkaIndonesia(teks: String): Double? {
return try {
val format = NumberFormat.getInstance(Locale("id", "ID"))
format.parse(teks.replace("Rp", "").trim())?.toDouble()
} catch (e: Exception) {
null
}
}
parseAngkaIndonesia("Rp 1.500.000") // 1500000.0
parseAngkaIndonesia("15.000,50") // 15000.5
// Parsing persentase
fun parsePersentase(teks: String): Double? {
return teks.removeSuffix("%").trim().toDoubleOrNull()?.div(100)
}
parsePersentase("85%") // 0.85
parsePersentase("12.5%") // 0.125
Type Casting — as dan as? #
Casting di Kotlin dibagi dua: unsafe cast (as) yang melempar exception jika gagal, dan safe cast (as?) yang mengembalikan null.
Unsafe Cast — as #
// as: lempar ClassCastException jika tipe tidak cocok
val objek: Any = "Halo Kotlin"
val teks: String = objek as String // OK
val angka: Int = objek as Int // ClassCastException!
// Kapan as masuk akal: saat kamu YAKIN tipenya
fun prosesEvent(event: Any) {
// Sudah dicek sebelumnya bahwa event adalah ClickEvent
val click = event as ClickEvent
klik(click.koordinat)
}
Safe Cast — as? #
// ANTI-PATTERN: unsafe cast tanpa jaminan tipe
val objek: Any = ambilDariLuar()
val teks = objek as String // bisa crash!
// BENAR: safe cast, hasilnya nullable
val teks: String? = objek as? String // null jika bukan String
// Kombinasi dengan let atau Elvis
val panjang = (objek as? String)?.length ?: 0
val nilaiInt = (objek as? Int) ?: -1
// Safe cast dalam when expression
fun deskripsi(nilai: Any): String = when (nilai) {
is String -> "String dengan panjang ${nilai.length}"
is Int -> "Int dengan nilai $nilai"
is List<*> -> "List dengan ${nilai.size} elemen"
else -> "Tipe tidak dikenal: ${nilai::class.simpleName}"
}
// as? sangat berguna saat parsing data dari JSON atau API
data class Respons(val data: Any?)
fun ambilNama(respons: Respons): String? {
val dataMap = respons.data as? Map<*, *> ?: return null
return dataMap["nama"] as? String
}
Smart Cast #
Smart cast adalah fitur compiler Kotlin yang secara otomatis mengkonversi tipe setelah pengecekan is — kamu tidak perlu cast manual setelah pengecekan tipe.
Smart Cast dengan is #
fun prosesNilai(nilai: Any) {
// Tanpa smart cast — perlu cast manual (seperti Java)
if (nilai is String) {
val teks = nilai as String // redundan!
println(teks.uppercase())
}
// Dengan smart cast — Kotlin tahu tipenya setelah is
if (nilai is String) {
println(nilai.uppercase()) // nilai sudah otomatis bertipe String di sini
println(nilai.length) // juga bisa akses length
}
// Smart cast di when — paling elegan
when (nilai) {
is String -> println("String: ${nilai.uppercase()}") // nilai = String
is Int -> println("Int kuadrat: ${nilai * nilai}") // nilai = Int
is List<*> -> println("List: ${nilai.size} elemen") // nilai = List<*>
is Boolean -> println("Boolean: ${if (nilai) "ya" else "tidak"}")
else -> println("Tidak dikenal")
}
}
Smart Cast dengan Null Check #
fun prosesNama(nama: String?) {
// Smart cast setelah null check
if (nama != null) {
println(nama.uppercase()) // nama = String (bukan String?) di sini
println(nama.length) // tidak perlu ?. lagi
}
// Juga bekerja dengan Elvis
val panjang = nama?.length ?: return // setelah ini nama tidak mungkin null
println("Panjang: $panjang")
// require / check juga memicu smart cast
requireNotNull(nama) { "Nama tidak boleh null" }
println(nama.uppercase()) // smart cast: nama = String
}
// Smart cast dan kondisi &&
fun validasiInput(input: Any?) {
if (input != null && input is String && input.length > 3) {
println(input.uppercase()) // smart cast berlapis
}
}
Keterbatasan Smart Cast #
Smart cast tidak selalu bisa diterapkan — compiler hanya menjamin smart cast jika variabel tidak bisa berubah antara pengecekan dan penggunaan.
class Pengguna {
var nama: String? = "Andi" // var — bisa berubah kapan saja!
}
val pengguna = Pengguna()
// Smart cast GAGAL untuk var property — compiler tidak bisa jamin
if (pengguna.nama != null) {
println(pengguna.nama.length) // ERROR: Smart cast ke 'String' tidak mungkin
// karena pengguna.nama bisa berubah dari thread lain antara cek dan akses
}
// SOLUSI 1: salin ke val lokal
val nama = pengguna.nama
if (nama != null) {
println(nama.length) // OK — nama adalah val lokal, tidak bisa berubah
}
// SOLUSI 2: gunakan safe call
println(pengguna.nama?.length)
// Smart cast BERHASIL untuk:
// - val lokal yang tidak ada custom getter
// - val property (bukan var)
// - parameter fungsi
val nilai: Any = ambilNilai()
if (nilai is String) {
println(nilai.length) // OK — nilai adalah val lokal
}
Konversi antar Collection #
Kotlin menyediakan fungsi konversi yang kaya antara berbagai tipe collection.
val list = listOf(3, 1, 4, 1, 5, 9, 2, 6, 5, 3)
// List → Set (menghilangkan duplikat)
val set: Set<Int> = list.toSet() // {3, 1, 4, 5, 9, 2, 6}
val mutableSet: MutableSet<Int> = list.toMutableSet()
// List → MutableList
val mutableList: MutableList<Int> = list.toMutableList()
// Set → List (urutan tidak terjamin)
val listDariSet: List<Int> = set.toList()
// List → Map (dengan fungsi kunci)
data class Produk(val id: Int, val nama: String, val harga: Double)
val produk = listOf(
Produk(1, "Laptop", 15_000_000.0),
Produk(2, "Mouse", 250_000.0),
Produk(3, "Keyboard", 800_000.0)
)
val produkById: Map<Int, Produk> = produk.associateBy { it.id }
val namaDanHarga: Map<String, Double> = produk.associate { it.nama to it.harga }
// Map → List of Pairs
val pairs: List<Pair<Int, Produk>> = produkById.toList()
// Array ↔ List
val array = intArrayOf(1, 2, 3, 4, 5)
val dariArray: List<Int> = array.toList()
val keArray: IntArray = dariArray.toIntArray()
// Array generik
val arrayOf = arrayOf("apel", "jeruk", "mangga")
val listDariArray = arrayOf.toList()
val kembaliArray = listDariArray.toTypedArray()
// Sequence ↔ List
val sequence = list.asSequence()
val kembaliList = sequence.toList()
Konversi Tipe Khusus #
Any dan Unit #
// Any adalah supertype semua tipe non-null di Kotlin
val apapun: Any = "bisa string"
val apapun2: Any = 42
val apapun3: Any = listOf(1, 2, 3)
// Unit adalah pengganti void — selalu ada satu instance
fun tidakKembalikanNilai(): Unit { println("Halo") }
fun tidakKembalikanNilai2() { println("Halo") } // Unit implisit
// Unit bisa digunakan sebagai type argument
val fungsiUnit: () -> Unit = { println("Halo") }
val listUnit: List<Unit> = List(3) { Unit } // [Unit, Unit, Unit]
// Nothing — tipe yang tidak pernah punya nilai, untuk fungsi yang tidak return
fun lemparError(pesan: String): Nothing {
throw IllegalStateException(pesan)
}
// Nothing berguna untuk type inference
val nilai = if (kondisi) "ada" else lemparError("tidak ada nilai")
// nilai bertipe String, bukan Any
Enum Conversion #
enum class Status { AKTIF, NONAKTIF, PENDING }
// String → Enum
val status = Status.valueOf("AKTIF") // Status.AKTIF
val status2 = enumValueOf<Status>("NONAKTIF") // Status.NONAKTIF
"INVALID".let { runCatching { Status.valueOf(it) }.getOrNull() } // null
// Int → Enum via ordinal
val ordinal = 0
val statusDariOrdinal = Status.entries[ordinal] // Status.AKTIF
// Enum → String
val namaStatus = Status.AKTIF.name // "AKTIF"
val ordinalStatus = Status.AKTIF.ordinal // 0
// Pattern aman: konversi dengan default
fun String.toStatusOrNull(): Status? = runCatching {
Status.valueOf(uppercase())
}.getOrNull()
fun String.toStatusOrDefault(default: Status = Status.PENDING): Status =
toStatusOrNull() ?: default
"aktif".toStatusOrDefault() // Status.AKTIF (case-insensitive)
"invalid".toStatusOrDefault() // Status.PENDING
Boolean Conversion #
// String → Boolean
"true".toBoolean() // true
"false".toBoolean() // false
"TRUE".toBoolean() // true (case-insensitive)
"yes".toBoolean() // false (hanya "true" yang bernilai true)
// toBooleanStrictOrNull — lebih ketat, null untuk input selain "true"/"false"
"true".toBooleanStrictOrNull() // true
"false".toBooleanStrictOrNull() // false
"yes".toBooleanStrictOrNull() // null
"1".toBooleanStrictOrNull() // null
// Int → Boolean (konvensi)
fun Int.toBoolean() = this != 0
val flagAktif: Boolean = 1.toBoolean() // true
val flagNon: Boolean = 0.toBoolean() // false
Pola Idiomatik untuk Konversi Aman #
Pipeline Konversi dengan Validasi #
// Proses form input yang kompleks
data class FormRegistrasi(
val usiaInput: String,
val gajiInput: String,
val aktifInput: String
)
data class DataUser(val usia: Int, val gaji: Double, val aktif: Boolean)
fun parseFormRegistrasi(form: FormRegistrasi): Result<DataUser> {
val usia = form.usiaInput.toIntOrNull()
?: return Result.failure(IllegalArgumentException("Usia harus berupa angka"))
if (usia !in 18..100) {
return Result.failure(IllegalArgumentException("Usia harus antara 18-100"))
}
val gaji = form.gajiInput.toDoubleOrNull()
?: return Result.failure(IllegalArgumentException("Gaji harus berupa angka"))
if (gaji < 0) {
return Result.failure(IllegalArgumentException("Gaji tidak boleh negatif"))
}
val aktif = form.aktifInput.toBooleanStrictOrNull()
?: return Result.failure(IllegalArgumentException("Status aktif harus 'true' atau 'false'"))
return Result.success(DataUser(usia, gaji, aktif))
}
Safe Cast dalam Arsitektur Berlapis #
// Pola umum saat bekerja dengan respons API yang dinamis
fun <T> parseRespom(json: Map<String, Any?>, kunci: String, tipe: Class<T>): T? {
val nilai = json[kunci] ?: return null
return tipe.cast(nilai)
}
// Atau dengan reified type parameter (lebih idiomatik di Kotlin)
inline fun <reified T> Map<String, Any?>.getAs(kunci: String): T? {
return this[kunci] as? T
}
val respons: Map<String, Any?> = mapOf(
"nama" to "Andi",
"usia" to 25,
"aktif" to true,
"skor" to 98.5
)
val nama: String? = respons.getAs("nama") // "Andi"
val usia: Int? = respons.getAs("usia") // 25
val aktif: Boolean? = respons.getAs("aktif") // true
val invalid: Int? = respons.getAs("nama") // null (nama adalah String, bukan Int)
Decision Tree — Pilih Konversi yang Tepat #
flowchart TD
A{Apa yang ingin\ndikonversi?} --> B["Numerik ke numerik\n(Int ↔ Long ↔ Double)"]
A --> C["String ke numerik\n(input pengguna / API)"]
A --> D["Tipe tidak diketahui\n(Any / Object)"]
A --> E["Collection ke\ncollection lain"]
A --> F["String / Int ke\nEnum"]
B --> B1["Gunakan .toInt()\n.toLong() .toDouble()\nPerhatikan overflow\npada narrowing"]
C --> C1{Sumber data\nterpercaya?}
C1 -- Ya --> C2[".toInt()\n.toDouble()\nDll"]
C1 -- Tidak --> C3[".toIntOrNull()\n.toDoubleOrNull()\n+ Elvis untuk default"]
D --> D1{Yakin tipenya?}
D1 -- Ya --> D2["as Type\n(crash jika salah)"]
D1 -- Tidak --> D3["as? Type\n(null jika salah)\natau is + smart cast"]
E --> E1[".toList() .toSet()\n.toMutableList()\n.associateBy { }\n.toTypedArray()"]
F --> F1["Enum.valueOf()\natau .toStatusOrNull()\nuntuk penanganan error"]Ringkasan #
- Tidak ada implicit conversion di Kotlin — setiap konversi tipe numerik harus eksplisit dengan
.toInt(),.toLong(),.toDouble(), dan seterusnya. Ini desain yang disengaja untuk menghilangkan bug dari konversi tak terduga.- Narrowing bisa overflow — konversi dari tipe besar ke kecil (
LongkeInt,DoublekeInt) bisa menghasilkan nilai yang tidak terduga. Cek range terlebih dahulu atau gunakancoerceInjika nilai tidak diketahui..toIntOrNull()bukan.toInt()untuk input yang tidak terpercaya (form, API, file). Varian*OrNullmengembalikannullalih-alih melemparNumberFormatException.as?bukanasuntuk safe cast —as?mengembalikannulljika tipe tidak cocok;asmelemparClassCastException. Gunakanashanya saat kamu benar-benar yakin tipenya.- Smart cast otomatis berlaku setelah pengecekan
isatau null check padavaldan parameter. Tidak berlaku untukvarproperty karena bisa berubah dari thread lain.- Keterbatasan smart cast pada
varproperty — salin kevallokal terlebih dahulu, lalu lakukan pengecekan dan akses pada variabel lokal tersebut.when+isadalah cara paling idiomatik untuk menangani banyak tipe berbeda — smart cast otomatis berlaku di setiap branch.- Konversi collection menggunakan
.toList(),.toSet(),.toMutableList(),.associateBy { }— semua menghasilkan collection baru, tidak memodifikasi yang asli.- Enum conversion paling aman dengan
runCatching { Status.valueOf(input) }.getOrNull()atau fungsi extension kustom —valueOf()langsung melempar exception untuk input tidak valid.DoublekeIntmemotong desimal, bukan membulatkan. Gunakankotlin.math.round(nilai).toInt()jika butuh pembulatan.