Sebelum ke topik-topik fundamental tersebut, mari kita awali dengan pembahasan tentang
program Hello Kotlin!. Pada modul sebelumnya kita telah berjumpa dengan sebuah proyek
yang menampilkan sebuah teks Hello World! dan juga Hello Kotlin!.
Buat Anda yang pernah belajar pemrograman sebelumnya, tentu tak asing dengan program
ini. Hello World! sering digunakan untuk menunjukkan sintaks dasar pada sebuah bahasa
pemrograman. Karena kita sedang belajar bahasa pemrograman Kotlin, maka kita
mengganti namanya dengan Hello Kotlin!.
1. // main function
2. fun main() {
3. println("Hello Kotlin!")
4. }
Baris pertama dari kode di atas adalah komentar yang ditandai dengan tanda //.
1. // main function
Sebuah komentar akan dilewatkan ketika proses kompilasi, sehingga tidak akan
mempengaruhi alur program yang kita tulis. Komentar bisa kita gunakan untuk
mendokumentasikan kode yang kita tulis agar ketika suatu saat kita membukanya kembali,
kita bisa mengetahui fungsi dari kode yang kita beri komentar tersebut.
Terdapat dua jenis komentar yang bisa kita gunakan. Pertama adalah single line
comment yaitu komentar satu baris yang diawali dengan tanda // dan berakhir di akhir baris
komentar tersebut.
1. /*
2. multi line comment
3. Hello Kotlin
4. */
Selanjutnya adalah fungsi yang bernama main(), fungsi yang wajib kita definisikan ketika
membuat sebuah program. Fungsi main() merupakan sebuah entry point yang otomatis
akan dipanggil ketika program dijalankan. Pada modul berikutnya kita akan belajar lebih
dalam tentang bagaimana mendefinisikan sebuah fungsi.
Kemudian fungsi println(), fungsi yang akan kita gunakan untuk mencetak teks ke dalam
layar atau konsol. Fungsi println() membutuhkan satu argumen berupa message dengan tipe
data yang dikehendaki. Tipe data yang didukung untuk kita masukkan ke dalam
fungsi println() ada di tautan ini.
Selain fungsi println(), terdapat juga fungsi print() yang berfungsi sama seperti
fungsi println(). Bedanya, println() akan menambahkan baris baru setelah selesai mencetak
argumen yang diberikan, sementara fungsi print() tidak melakukan apapun ketika argumen
yang diberikan, selesai dicetak. Untuk memahaminya lebih dalam, coba jalankan kode
berikut:
1. fun main() {
2. val name = "Alfian"
3.
4. print("Hello my name is ")
5. println(name)
6. print(if (true) "Always true" else "Always false")
7. }
8.
9. /*
10. output:
11. Hello my name is Alfian
12. Always true
13. */
1. @kotlin.internal.InlineOnly
2. public actual inline fun print(message: Any?) {
3. System.out.print(message)
4. }
Namun sebelumnya, ada satu hal yang kita perlu tahu terlebih dahulu, yaitu Variabel.
Umumnya variabel digunakan untuk menyimpan informasi atau nilai yang akan dikelola di
dalam sebuah program. Sebuah variabel akan membutuhkan kata
kunci var atau val, identifier, type dan initialization. Kira-kira strukturnya seperti berikut:
var atau val
var atau val digunakan untuk mengontrol nilai dari sebuah variabel. Dengan kata
kunci var kita bisa mengubah nilai yang sudah kita inisialisasikan. Sebagai contoh:
1. var company: String = "Dicoding"
2. company = "Dicoding Academy"
Dan yang terakhir adalah initialization atau nilai awal dari sebuah variabel. Pada
contoh di atas yang berperan
sebagai initialization adalah “Dicoding” dan “Dicoding Academy”.
Tipe data juga menentukan operasi apa saja yang dapat dilakukan pada sebuah variabel
dan bagaimana nilai dari sebuah variabel disimpan. Contoh, ketika kita menggunakan
operator + terhadap dua variabel yang bertipe String seperti berikut:
1. fun main() {
2. val firstWord = "Dicoding "
3. val lastWord = "Academy"
4. print(firstWord + lastWord)
5. }
6. /*
7. output: Dicoding Academy
8. */
1. fun main() {
2. val valueA: Int = 10
3. val valueB = 20
4. print(valueA + valueB)
5. }
6. /*
7. output: 30
8. */
Kompiler akan menjalankan operasi aritmatika, seperti pada contoh di atas di mana nilai dari
variabel valueA dan valueB akan dijumlahkan lalu menghasilkan nilai baru.
Ketika kita mengembangkan sebuah program kita pasti membutuhkan variabel dengan tipe
data yang mampu menyimpan nilai berbentuk teks. Terdapat dua (2) tipe data yang bisa kita
gunakan, yaitu Char dan String.
Char
Characters direpresentasikan menggunakan tipe Char. Untuk mendefinisikan sebuah
variabel dengan tipe data Char kita bisa menggunakan tanda kutip tunggal (' ') seperti
berikut:
Tipe data Char hanya dapat kita gunakan untuk menyimpan karakter tunggal. Sebaliknya
jika kita memasukkan lebih dari 1 (satu) karakter, akan terjadi eror:
1. fun main() {
2. var vocal = 'A'
3.
4. println("Vocal " + vocal++)
5. println("Vocal " + vocal++)
6. println("Vocal " + vocal++)
7. println("Vocal " + vocal--)
8. println("Vocal " + vocal--)
9. println("Vocal " + vocal--)
10. println("Vocal " + vocal--)
11. }
12.
13. /*
14. output:
15. Vocal A
16. Vocal B
17. Vocal C
18. Vocal D
19. Vocal C
20. Vocal B
21. Vocal A
22. */
String direpresentasikan menggunakan tipe String. Nilai yang berada di dalam sebuah
variabel dengan tipe data String merupakan kumpulan dari beberapa karakter. Kita bisa
mendefinisikan variabel tersebut dengan tanda petik ganda (" ") seperti berikut:
Pada dasarnya sekumpulan karakter dalam String tersebut berbentuk Array, sehingga kita
bisa mendapatkan karakter tunggal dengan mudah. Caranya, manfaatkan indexing seperti
berikut:
1. fun main() {
2. val text = "Kotlin"
3. val firstChar = text[0]
4.
5. print("First character of $text is $firstChar")
6. }
7.
8. /*
9. output : First character of Kotlin is K
10. */
Nilai 0 yang berada pada indexing di atas adalah posisi karakter yang akan diakses. Selain
itu, kita juga dapat melakukan iterasi terhadap objek String dengan menggunakan for-
loop seperti berikut:
1. fun main() {
2. val text = "Kotlin"
3. for (char in text){
4. print("$char ")
5. }
6. }
7.
8. /*
9. output : K o t l i n
10. */
Escaped String
Kotlin memiliki dua jenis tipe Literal String, yang pertama adalah Escaped String yang
memungkinkan kita untuk mengurangi ambiguitas nilai yang berada di dalam sebuah String.
Misalnya ketika kita mendefinisikan sebuah String berikut:
Maka akan terjadi ambiguitas nilai pada variabel statement karena kompiler tidak dapat
mengetahui akhir dari baris nilai untuk variabel statement. Untuk mengatasinya, kita bisa
melakukan escaped dengan menambahkan karakter backslash (\) sebelum tanda petik
ganda seperti berikut:
Selain \” di atas, terdapat beberapa karakter lain yang dapat digunakan untuk
melakukan escaped di dalam sebuah String, antara lain:
Selain itu, kita juga bisa menambahkan sebuah Unicode ke dalam sebuah String seperti
berikut:
1. fun main() {
2. val name = "Unicode test: \u00A9"
3. print(name)
4. }
5.
6. /*
7. output: Unicode test : ©
8. */
Raw String
Kedua, adalah Raw String yang memungkinkan kita menuliskan multiline dan arbitrary text.
Ketika ingin membuat beberapa baris String biasanya kita melakukan escaped terhadap
String dengan memanfaatkan karakter escape \n seperti berikut:
Dengan Raw String, kita dapat membuatnya dengan cara yang lebih mudah yaitu seperti
berikut:
1. fun main() {
2. val line = """
3. Line 1
4. Line 2
5. Line 3
6. Line 4
7. """.trimIndent()
8.
9. print(line)
10. }
11.
12. /*
13. output:
14. Line 1
15. Line 2
16. Line 3
17. Line 4
18. */
Pada kode di atas, kita mendefinisikan sebuah Raw String menggunakan triple quote ("""
"""). Raw String memungkinkan kita untuk membuat beberapa baris String tanpa
penggabungan (concatenation) dan penggunaan karakter escaped.
Functions
Function atau fungsi merupakan sebuah prosedur yang memiliki keterkaitan dengan pesan
dan objek. Ketika kita memanggil sebuah fungsi maka sebuah mini-program akan
dijalankan. Fungsi sendiri bisa diartikan sebagai cara sederhana untuk mengatur program
buatan kita.
Sebuah fungsi dapat kita gunakan untuk mengembalikan nilai. Pemanggilan sebuah fungsi
sendiri, bisa diberi argumen atau tidak. Pada modul ini kita akan belajar bagaimana
membuat sebuah fungsi pada Kotlin dan mencoba beberapa poin di atas.
Setelah menentukan nama dan parameter, selanjutnya adalah menentukan tipe kembalian
dari fungsi yang dibuat. Perlu diketahui fungsi pada Kotlin selalu mengembalikan nilai. Tipe
kembalian adalah nilai yang akan dikembalikan ketika fungsi tersebut dipanggil.
Fungsi di atas akan mengembalikan nilai berupa String. Setelah menentukan tipe nilai
kembalian, barulah kita menentukan function body di mana di dalamnya
terdapat expression atau statement untuk dijalankan. Function body berada di dalam curly
braces ({}) setelah tipe nilai kembalian.
Nilai yang akan dikembalikan diikuti oleh kata kunci return. Jika di dalam suatu fungsi hanya
memiliki satu expression untuk menentukan nilai kembalian, maka fungsi tersebut bisa
diubah menjadi expression body. Kita hanya perlu menambahkan tanda = dan
menuliskannya seperti berikut:
1. fun setUser(name: String, age: Int): String = "Your name is $name, and you $age years
old"
Dengan expression body, kompiler dapat menentukan tipe kembalian dari fungsi yang
dibuat. Sehingga kita tidak perlu menentukan tipe nilai kembalian secara eksplisit:
1. fun setUser(name: String, age: Int) = "Your name is $name, and you $age years old"
Jika kita tidak ingin fungsi yang dibuat mengembalikan nilai, kita bisa
menggunakan Unit sebagai tipe nilai kembaliannya. Contohnya seperti berikut:
1. fun printUser(name: String): Unit {
2. print("Your name is $name")
3. }
1. fun main() {
2. val user = setUser("Alfian", 19)
3. println(user)
4.
5. printUser("Alfian")
6. }
7.
8. fun setUser(name: String, age: Int) = "Your name is $name, and you $age years old"
9.
10. fun printUser(name: String) {
11. println("Your name is $name")
12. }
13.
14. /*
15. output :
16. Your name is Alfian, and you 19 years old
17. Your name is Alfian
18. */
If Expressions
Saat mengembangkan sebuah program, kita pasti bertemu dengan alur program yang perlu
sebuah kondisi untuk menjalankan sebuah statement atau expression. Contoh ketika kita
ingin menginisialisasi nilai dari sebuah variabel berdasarkan suatu kondisi. Untuk
menyelesaikannya, gunakan If Expression.
1. val openHours = 7
2. val now = 20
3. if (now > openHours){
4. println("office already open")
5. }
1. val openHours = 7
2. val now = 20
3. val office: String
4. if (now > openHours) {
5. office = "Office already open"
6. } else {
7. office = "Office is closed"
8. }
9.
10. print(office)
1. val openHours = 7
2. val now = 20
3. val office: String
4. office = if (now > openHours) {
5. "Office already open"
6. } else {
7. "Office is closed"
8. }
9.
10. print(office)
Pada kode di atas, kita hanya menggunakan If untuk menguji 2 (dua) kondisi. Lalu
bagaimana jika kita memiliki beberapa kondisi? Kita bisa menggabungkan else dan if seperti
berikut:
1. val openHours = 7
2. val now = 7
3. val office: String
4. office = if (now > 7) {
5. "Office already open"
6. } else if (now == openHours){
7. "Wait a minute, office will be open"
8. } else {
9. "Office is closed"
10. }
11.
12. print(office)
1. fun main() {
2. val officeOpen = 7
3. val officeClosed = 16
4. val now = 20
5.
6. val isOpen = if (now >= officeOpen && now <= officeClosed){
7. true
8. } else {
9. false
10. }
11.
12. print("Office is open : $isOpen")
13.
14. /*
15. Output : Office is open : false
16. */
17. }
Fungsi di atas menguji apakah jam sekarang berada di antara jam waktu buka kantor dan
jam tutup kantor. If expressions di atas bisa Anda sederhanakan jadi seperti berikut:
1. fun main() {
2. val officeOpen = 7
3. val officeClosed = 16
4. val now = 20
5.
6. val isOpen = now >= officeOpen && now <= officeClosed
7.
8. print("Office is open : $isOpen")
9. /*
10. Output : Office is open : false
11. */
12. }
1. fun main() {
2. val officeOpen = 7
3. val officeClosed = 16
4. val now = 20
5.
6. val isClose = now < officeOpen || now > officeClosed
7.
8. print("Office is closed : $isClose")
9. /*
10. Output : Office is closed : true
11. */
12. }
1. fun main() {
2. val officeOpen = 7
3. val now = 10
4. val isOpen = now > officeOpen
5.
6. if (!isOpen) {
7. print("Office is closed")
8. } else {
9. print("Office is open")
10. }
11.
12. /*
13. Output : Office is open
14. */
15. }
Di Kotlin, tipe data Number disimpan dengan cara yang berbeda. Beberapa tipe bawaan
yang merepresentasikan Numbers adalah Double, Long, Int, Short dan Byte. Setiap tipe
data Number memiliki ukuran (satuan Bit) berbeda-beda, tergantung besaran nilai yang
dapat simpan.
Int adalah tipe data yang umumnya digunakan untuk menyimpan nilai numerik. Int
dapat menyimpan data dari range -2^31 sampai +2^31-1. Dengan ukuran 32 Bit kita
bisa menggunakannya untuk menyimpan nilai yang besar. Catatannya, tetap lihatlah
batasan nilai maksimal yang dapat dimasukkan.
Long adalah tipe data yang digunakan untuk menyimpan nilai numerik yang lebih
besar yaitu dari range -2^63 sampai +2^63-1. Long bisa didefinisikan secara
eksplisit:
Short merupakan sebuah bilangan bulat yang hanya dapat menyimpan nilai yang
kecil karena hanya berukuran 16 Bit.
Dengan ukuran yang kecil, Byte hanya mampu menyimpan nilai yang kecil sama
halnya seperti Short. Byte biasa digunakan untuk keperluan proses membaca dan
menulis data dari sebuah stream file atau jaringan.
Sama halnya dengan Long yang memiliki ukuran yang besar, Double mampu
menyimpan nilai numerik yang besar pula. Pada umumnya Double digunakan untuk
menyimpan nilai numerik pecahan sampai dengan maksimal 15-16 angka di
belakang koma.
Sama seperti Double, namun memiliki ukuran yang lebih kecil, yakni hanya sampai
6-7 angka di belakang koma.
Untuk mengetahui nilai maksimal yang dapat disimpan oleh suatu tipe Number, kita bisa
menggunakan properti MAX_VALUE. Sementara untuk mengetahui nilai minimal yang
dapat disimpan, gunakan properti MIN_VALUE.
1. fun main() {
2. val maxInt = Int.MAX_VALUE
3. val minInt = Int.MIN_VALUE
4.
5. println(maxInt)
6. println(minInt)
7.
8. /*
9. output :
10. 2147483647
11. -2147483648
12. */
13. }
Jika kita memasukan nilai melebihi nilai maksimal yang dapat disimpan, maka akan
terjadi overflow. Nilai yang akan dikembalikan adalah nilai minimal yang dapat disimpan.
1. fun main() {
2. val maxInt = Int.MAX_VALUE
3. val overRangeInt = Int.MAX_VALUE + 1 // This operation has led to an overflow
4.
5. println("Max Int: $maxInt")
6. println("Over range Int: $overRangeInt")
7. }
8.
9. /*
10. Output :
11.
12. Max Int: 2147483647
13. Over range Int: -2147483648
14. */
Terdapat beberapa operator matematika pada tipe data Number seperti penjumlahan (+),
pengurangan (-), perkalian (*) , pembagian (/) dan modulus (%, atau sisa hasil bagi).
1. // main function
2. fun main() {
3. val numberOne = 1
4. val numberTwo = 2
5.
6. print(numberOne + numberTwo)
7. /*
8. output : 3
9. */
10. }
Perlu diketahui, hasil operasi pembagian pada tipe data Int akan dibulatkan kebawah.
Contohnya seperti berikut:
1. // main function
2. fun main() {
3. val numberOne: Int = 20
4. val numberTwo: Int = 10
5.
6. print(numberOne / numberTwo)
7. /*
8. output : 2
9. */
10. }
1. fun main() {
2. print(5 + 4 * 4)
3. /*
4. output: 21
5. */
6. }
Operasi 4 * 4 akan dilakukan terlebih dahulu, kemudian diikuti 5 + 16. Jika ingin operasi 5 +
4 dilakukan terlebih dahulu, gunakan tanda kurung:
1. fun main() {
2. print((5 + 4) * 4)
3. /*
4. output: 36
5. */
6. }
Di Kotlin kita tidak bisa melakukan konversi secara langsung. Contoh, ketika ingin
melakukan konversi dari tipe data Byte ke tipe data Int.
1. fun main() {
2. val byteNumber: Byte = 1
3. val intNumber: Int = byteNumber // compile error
4. }
Error:(4, 18) Kotlin: Type mismatch: inferred type is Byte but Int was expected
1. fun main() {
2. val byteNumber: Byte = 10
3. val intNumber: Int = byteNumber.toInt() // ready to go
4. }
Kode di atas menggunakan fungsi toInt() untuk melakukan konversi secara eksplisit dari tipe
data Byte ke tipe data Int. Adapun beberapa fungsi konversi yang dapat kita gunakan antara
lain:
toByte(): Byte
toShort(): Short
toInt(): Int
toLong(): Long
toFloat(): Float
toDouble(): Double
toChar(): Char
Contoh lain penggunaan konversi adalah sebagai berikut:
1. fun main() {
2. val stringNumber = "23"
3. val intNumber = 3
4.
5. print(intNumber + stringNumber.toInt())
6. /*
7. output: 26
8. */
9. }
Dengan fungsi konversi di atas, nilai 23 yang semula bertipe String di konversi ke tipe Int
yang kemudian dimasukan ke dalam operasi matematika.
Dengan Kotlin kita juga bisa menuliskan nilai numerik yang “readable” dengan
menggunakan tanda underscores seperti berikut:
1. fun main() {
2. val readableNumber = 1_000_000
3. print(readableNumber)
4.
5. /*
6. output : 1000000
7. */
8. }
Arrays
Selanjutnya adalah Array, yakni tipe data yang memungkinkan kita untuk menyimpan
beberapa objek di dalam sebuah variabel. Array di Kotlin direpresentasikan oleh
kelas Array yang memiliki fungsi get dan set serta properti size. Untuk membuat sebuah
Array kita bisa memanfaatkan sebuah library function arrayOf() seperti berikut:
Kita juga dapat memasukkan nilai dengan berbagai jenis tipe data ke
dalam arrayOf() misalnya:
Kotlin juga memungkinkan kita untuk membuat Array dengan tipe data primitif dengan
memanfaatkan beberapa fungsi spesifik berikut:
intArrayOf() : IntArray
booleanArrayOf() : BooleanArray
charArrayOf() : CharArray
longArrayOf() : LongArray
shortArrayOf() : ShortArray
byteArrayOf() : ByteArray
Jika kita ingin membuat Array yang hanya bisa dimasukkan nilai dengan tipe
data Int, gunakan intArrayOf(), misalnya:
Kita juga bisa mendapatkan nilai tunggal dari sekumpulan nilai yang berada di dalam
sebuah Array dengan memanfaatkan indexing seperti berikut:
1. fun main() {
2. val intArray = intArrayOf(1, 3, 5, 7)
3. print(intArray[2])
4. }
5.
6. /*
7. Output: 5
8. */
Nilai 2 pada kode di atas merupakan indeks atau posisi dari nilai tunggal yang ingin kita
dapatkan. Perlu diketahui bahwa sebuah indeks selalu dimulai dari 0. Selain mendapatkan
nilai tunggal, dengan indexing kita juga bisa mengubah nilai tunggal tersebut. Sebagai
contoh:
1. fun main() {
2. val intArray = intArrayOf(1, 3, 5, 7) // [1, 3, 5, 7]
3. intArray[2] = 11 // [1, 3, 11, 7]
4.
5. print(intArray[2])
6. }
7.
8. /*
9. Output: 11
10. */
Pada kode di atas kita menentukan angka 4 sebagai size Array. Fungsi lambda di atas ada
dua. Pertama, untuk mengambil indeks Array yang akan digunakan sebagai argumen.
Kedua, menentukan elemen Array yang akan dimasukkan ke dalam indeks tersebut.
Dalam penanganannya, kita harus berhati-hati karena NPE menyebabkan aplikasi yang kita
kembangkan, rusak saat dijalankan.
Kotlin akan memaksa kita untuk menentukan nilai awal dari sebuah objek ketika dibuat dan
tidak boleh bernilai null. Jika ingin sebuah objek bisa bernilai null, kita bisa menambahkan
tanda ? setelah menentukan tipe dari objek tersebut:
Namun kita tidak bisa langsung mengakses atau mengelola nilai dari objek yang sudah kita
tandai sebagai nullable. Sebagai contoh:
Ketika kita menuliskan kode di atas, maka akan gagal dikompilasi dengan log eror berikut:
Error:(4, 26) Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a
nullable receiver of type String?
Lalu bagaimana cara kita mengakses atau mengelola nilai dari objek yang ditandai
sebagai nullable? Cara mudahnya, periksa objek tersebut apakah bernilai null atau tidak:
Pada modul selanjutnya kita akan mempelajari penanganan objek yang ditandai
sebagai nullable dengan cara yang lebih mudah. Kita akan menggunakan Safe
Calls dan Elvis Operator di Kotlin.
Di modul sebelumnya kita sudah mengenal tentang NullPointerException dan cara
penanganannya. Kini kita akan belajar bagaimana menangani objek nullable dengan cara
yang lebih mudah, yaitu menggunakan Safe Calls dan Elvis Operator.
Dengan safe call, kompiler akan melewatkan proses jika objek tersebut bernilai null.
Sebelum lanjut ke modul selanjutnya terdapat satu hal yang perlu diperhatikan dalam
penanganan objek nullable. Perhatikan penggunaan operator non-null assertion (!!),
misalnya seperti berikut:
Lantas dinamakan apakah mekanisme penulisan String seperti di atas? Kotlin mendukung
sebuah fitur bernama String Template. Sebuah fitur yang memungkinkan kita untuk
menyisipkan sebuah variabel ke dalam sebuah String tanpa concatenation (penggabungan
objek String menggunakan +) seperti berikut:
1. fun main() {
2. val name = "Kotlin"
3. print("My name is " + name)
4. }
5. /*
6. output : My name is Kotlin
7. */
1. fun main() {
2. val name = "Kotlin"
3. print("My name is $name")
4. }
5. /*
6. output : My name is Kotlin
7. */
Variabel yang dapat disisipkan tidak sebatas String. Kita juga bisa menyisipkan objek lain
misal Int atau Double seperti berikut:
1. fun main() {
2. val name = "Kotlin"
3. val old = 3
4. print("My name is $name, im $old years old")
5. }
6. /*
7. output : My name is Kotlin, im 3 years old
8. */
Tidak hanya sampai disitu, Anda juga bisa menyisipkan sebuah expression ke dalam
sebuah string template. Caranya, sisipkan expression ke dalam curly braces yang diikuti
karakter $.
1. fun main() {
2. val hour = 7
3. print("Office ${if (hour > 7) "already close" else "is open"}")
4. }
5. /*
6. output : Office is open
7. */
Dengan string template, kita lebih mudah membuat objek String yang dinamis.
Sebelum Anda lanjut ke modul berikutnya, berikut adalah beberapa rangkuman dari sub-
modul yang sudah dipelajari:
Sama seperti bahasa pemrograman lain, Kotlin memiliki fungsi untuk mencetak nilai
pada console yaitu fungsi print() dan println().
Untuk mendeklarasi variable, Anda akan menggunakan kata kunci var ata val. var
atau val digunakan untuk mengontrol nilai dari sebuah variabel. Dengan kata kunci
var kita bisa mengubah nilai yang sudah kita inisialisasikan.
Untuk membuat variable yang menampung data berupa text, Anda dapat
menggunakan tipe data Char untuk menyimpan satu karakter dan tipe
data String untuk menyimpan beberapa karakter.
Sedangkan untuk menampung data berupa number, Anda akan menggunakan
beberapa tipe data di bawah ini:
o Int (32 Bit)
Int adalah tipe data yang umumnya digunakan untuk menyimpan nilai
numerik. Int dapat menyimpan data dari range -2^31 sampai +2^31-1.
Dengan ukuran 32 Bit kita bisa menggunakannya untuk menyimpan nilai
yang besar. Catatannya, tetap lihatlah batasan nilai maksimal yang dapat
dimasukkan.
o Long (64 Bit)
Long adalah tipe data yang digunakan untuk menyimpan nilai numerik yang
lebih besar yaitu dari range -2^63 sampai +2^63-1.
o Short (16 Bit)
Short merupakan sebuah bilangan bulat yang hanya dapat menyimpan nilai
yang kecil karena hanya berukuran 16 Bit.
o Byte (8 Bit)
Dengan ukuran yang kecil, Byte hanya mampu menyimpan nilai yang kecil
sama halnya seperti Short. Byte biasa digunakan untuk keperluan proses
membaca dan menulis data dari sebuah stream file atau jaringan.
o Double (64 Bit)
Sama halnya dengan Long yang memiliki ukuran yang besar, Double mampu
menyimpan nilai numerik yang besar pula. Pada umumnya Double digunakan
untuk menyimpan nilai numerik pecahan.
Selanjutnya adalah Array, yakni tipe data yang memungkinkan Anda untuk
menyimpan beberapa objek di dalam satu variabel.
Kotlin juga memungkinkan Anda untuk membuat Array dengan tipe data primitif
dengan memanfaatkan beberapa fungsi spesifik seperti berikut:
o intArrayOf() : IntArray
o booleanArrayOf() : BooleanArray
o charArrayOf() : CharArray
o longArrayOf() : LongArray
o shortArrayOf() : ShortArray
o byteArrayOf() : ByteArray
Kotlin mendukung juga tipe data Boolean di mana tipe data yang hanya memiliki dua
nilai, yaitu true dan false. Selain itu, Terdapat 3 (tiga) operator yang dapat digunakan
pada Boolean:
o Conjunction atau AND (&&)
Operator AND (&&) akan mengembalikan nilai true jika semua hasil evaluasi
expression yang diberikan bernilai true.
o Disjunction atau OR (||)
Berbeda dengan operator AND (&&), operator OR (||) akan mengembalikan
nilai true jika hasil evaluasi dari salah satu expressions yang diberikan
bernilai true.
o Negation atau NOT (!)
Berbeda dengan operator AND (&&) dan operator OR(||), operator NOT(!)
digunakan untuk melakukan negasi pada hasil evaluasi expression yang
diberikan.
Jika ingin menginisialisasi nilai dari sebuah variabel berdasarkan suatu kondisi.
Untuk menyelesaikannya, gunakan If Expression.
Dengan Kotlin kita mudah dalam mengelola variable nullable sehingga dapat
meminimalisir terjadinya NullPointerException dengan menggunakan Safe
Call dan Elvis Operator.
Function atau fungsi merupakan sebuah prosedur yang memiliki keterkaitan dengan
pesan dan objek. Ketika kita memanggil sebuah fungsi maka sebuah mini-
program akan dijalankan. Fungsi sendiri bisa diartikan sebagai cara sederhana untuk
mengatur program buatan kita.
Kita sudah selesai dengan Kotlin Fundamental di mana Anda sudah mempelajari tentang
bagaimana menampilkan pesan pada konsol, berbagai macam tipe data, dan bagaimana
cara meminimalisir terjadinya NullPointerException. Selanjutnya kita mempelajari modul
Control Flow. Langsung saja ke modul berikutnya.
Persiapan
Setiap modul pada akademi ini memiliki beberapa latihan yang harus Anda kerjakan. Latihan-latihan yang ada akan
meningkatkan pemahaman Anda terhadap sebuah materi, jadi sangat disarankan untuk Anda menyelesaikannya
segera setelah membaca 1 (satu) modul besar. Untuk mengerjakan latihan, Anda akan menggunakan sebuah
plugin pada IntelliJ IDEA, yaitu EduTools. Dengan EduTools, semua pekerjaan Anda akan diperiksa secara otomatis.
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda sudah mempelajari konsep - konsep fundamental pada Kotlin seperti tipe data, fungsi,
If expression, dan juga fitur Null Safety.
Untuk menguji pemahaman Anda terhadap materi yang sudah dipelajari, Anda perlu
mengerjakan beberapa latihan sederhana. Buka kembali proyek latihan dan kerjakan semua
latihan yang ada pada modul Kotlin Fundamental.
Kita telah belajar mengenai If Expression pada modul sebelumnya. If Expression
merupakan salah satu bagian dari Control Flow. Pada modul ini kita akan mempelajari
tentang apa itu control flow dan juga beberapa bagian lain yang ada di dalamnya.
Terdapat beberapa bagian dari control flow yang akan kita pelajari, antara lain:
Enumeration
When Expression
Expression & Statement
While and Do While
Range and For Loop
Break and Continue Labels
Kita akan mempelajari seperti apa dan bagaimana cara menerapkan beberapa control flow
di atas.
Enumeration
Enumeration merupakan salah satu fitur yang bisa kita gunakan untuk menyimpan kumpulan
objek yang telah didefinisikan menjadi tipe data konstanta. Enumeration dapat ditetapkan
sebagai nilai ke dalam sebuah variabel dengan cara yang lebih efisien. Selain itu,
Enumeration juga dapat digunakan untuk meminimalisir kesalahan dalam pengetikan nilai
sebuah variabel, misalnya:
Nilai dari beberapa variabel di atas berpotensi salah atau tertukar dengan nilai variabel lain.
Untuk meminimalisir kesalahan, kita memerlukan Enumeration. Anda bisa melakukannya
seperti ini:
1. fun main() {
2. val colorRed = Color.RED
3. val colorGreen = Color.GREEN
4. val colorBlue = Color.BLUE
5. }
6.
7. enum class Color(val value: Int) {
8. RED(0xFF0000),
9. GREEN(0x00FF00),
10. BLUE(0x0000FF)
11. }
1. fun main() {
2. val color : Color = Color.RED
3. print(color)
4. }
5.
6. enum class Color{
7. RED, GREEN, BLUE
8. }
9.
10. /*
11. output: RED
12. */
Seperti yang dicontohkan di awal, setiap objek yang dideklarasikan merupakan instance dari
kelas Enum tersebut. Kita bisa menginisialisasinya seperti berikut:
Selain itu, kita juga dapat mendeklarasikan anonymous class untuk setiap objek Enum,
misalnya:
1. fun main() {
2. val colors: Array<Color> = Color.values()
3. colors.forEach { color ->
4. print(color)
5. }
6. }
7.
8. enum class Color(val value: Int) {
9. RED(0xFF0000),
10. GREEN(0x00FF00),
11. BLUE(0x0000FF)
12. }
13.
14. /*
15. output : RED, GREEN, BLUE
16. */
17.
18.
Perhatikan. Untuk mendapatkan daftar objek Enum kita bisa menggunakan fungsi values().
Sedangkan untuk mendapatkan nama dari objek Enum kita bisa menggunakan
fungsi valueOf() seperti berikut:
1. fun main() {
2. val color: Color = Color.valueOf("RED")
3. print("Color is $color")
4. }
5.
6. enum class Color(val value: Int) {
7. RED(0xFF0000),
8. GREEN(0x00FF00),
9. BLUE(0x0000FF)
10. }
11.
12. /*
13. output : Color is RED
14. */
1. fun main() {
2. val colors: Array<Color> = enumValues()
3. colors.forEach {color ->
4. println(color)
5. }
6.
7. val color: Color = enumValueOf("RED")
8. println("Color is $color")
9. }
10.
11. enum class Color(val value: Int) {
12. RED(0xFF0000),
13. GREEN(0x00FF00),
14. BLUE(0x0000FF)
15. }
16.
17. /*
18. output :
19. RED
20. GREEN
21. BLUE
22. Color is RED
23.
24. */
Konsep dari Enumeration sendiri sama seperti Array. Oleh karena itu, selain mendapatkan
daftar dan nama dari tiap objek Enum, kita juga bisa mendapatkan posisi tiap objek
menggunakan properti ordinal seperti berikut:
1. fun main() {
2. val color: Color = Color.GREEN
3.
4. print("Position GREEN is ${color.ordinal}")
5. }
6.
7. enum class Color(val value: Int) {
8. RED(0xFF0000),
9. GREEN(0x00FF00),
10. BLUE(0x0000FF)
11. }
12.
13. /*
14. output : Position GREEN is 1
15.
16. */
Di atas telah disebutkan bahwa setiap objek merupakan instance dari enum class yang kita
definisikan. Lantas bagaimana cara kita mengecek instance dari Enum itu sendiri? Nah,
untuk mengeceknya, gunakan When Expression seperti berikut:
1. fun main() {
2. val color: Color = Color.GREEN
3.
4. when(color){
5. Color.RED -> print("Color is Red")
6. Color.BLUE -> print("Color is Blue")
7. Color.GREEN -> print("Color is Green")
8. }
9. }
10.
11. enum class Color(val value: Int) {
12. RED(0xFF0000),
13. GREEN(0x00FF00),
14. BLUE(0x0000FF)
15. }
16.
17. /*
18. output : Color is Green
19.
20. */
1. val openOffice = 7
2. val now = 8
3. if (now > openOffice)
4. print("Office already open")
5. else
6. print("Office close")
Pada contoh kode di atas, if dikatakan sebagai statement karena ia tidak mengembalikan
nilai apapun, hanya sebagai percabangan sebagai pada bahasa Java.
Namun yang menarik If pada bahasa Kotlin juga bisa digunakan sebagai expression. Yang
dimaksud dengan expression adalah statement yang dapat mengembalikan nilai dan bisa
kita simpan ke dalam sebuah variabel seperti contoh berikut:
1. val openOffice = 7
2. val now = 8
3. val office = if (now > openOffice) "Office already open" else "Office close"
4. print(office)
Pada kode di atas, If akan mengembalikan nilai kedalam variabel office. Apabila kondisi
pada if terpenuhi maka variabel office akan berisi "Office already open" dan jika tidak maka
akan berisi "Office close".
1. fun main() {
2. sum(1 , 1 * 4)
3. }
4.
5. fun sum(value1: Int, value2: Int) = value1 + value2
1. fun main() {
2. val value1 = 10
3. val value2 = 10
4.
5. sum(value1, value2)
6. }
7.
8. fun sum(value1: Int, value2: Int) = value1 + value2
Pada kode di atas deklarasi variabel value1 dan value2 merupakan sebuah statement.
Sedangkan pemanggilan fungsi sum seperti yang dijelaskan di atas, merupakan
sebuah expression.
When Expressions
Untuk menentukan statement atau expression kita menggunakan If Expression. Selain itu
kita juga bisa gunakan When Expression, yakni mekanisme yang memungkinkan nilai dari
sebuah variabel/expression, mampu mengubah alur program.
1. fun main() {
2. val value = 7
3.
4. when(value){
5. 6 -> println("value is 6")
6. 7 -> println("value is 7")
7. 8 -> println("value is 8")
8. }
9. }
10.
11. /*
12. output: value is 7
13. */
1. fun main() {
2. val value = 20
3.
4. when(value){
5. 6 -> println("value is 6")
6. 7 -> println("value is 7")
7. 8 -> println("value is 8")
8. else -> println("value cannot be reached")
9. }
10. }
11.
12. /*
13. output: value cannot be reached
14. */
1. fun main() {
2. val value = 7
3. val stringOfValue = when (value) {
4. 6 -> "value is 6"
5. 7 -> "value is 7"
6. 8 -> "value is 8"
7. else -> "value cannot be reached"
8. }
9.
10. println(stringOfValue)
11. }
12.
13. /*
14. output : value is 7
15. */
else adalah hal wajib jika kita menggunakan when expression untuk mengembalikan nilai.
Bagaimana jika kita melewatkannya? Akan tampil eror berikut:
'when' expression must be exhaustive, add necessary 'else' branch
Jika kita memiliki dua atau lebih baris kode yang akan kita jalankan di setiap branch, kita
bisa memindahkannya ke dalam curly braces seperti berikut:
1. fun main() {
2. val value = 7
3. val stringOfValue = when (value) {
4. 6 -> {
5. println("Six")
6. "value is 6"
7. }
8. 7 -> {
9. println("Seven")
10. "value is 7"
11. }
12. 8 -> {
13. println("Eight")
14. "value is 8"
15. }
16. else -> {
17. println("undefined")
18. "value cannot be reached"
19. }
20. }
21.
22. println(stringOfValue)
23. }
24.
25. /*
26. output :
27. Seven
28. value is 7
29.
30. */
1. fun main() {
2. val anyType : Any = 100L
3. when(anyType){
4. is Long -> println("the value has a Long type")
5. is String -> println("the value has a String type")
6. else -> println("undefined")
7. }
8. }
9.
10. /*
11. output : the value has a Long type
12. */
Selain itu, when expression juga bisa kita gunakan untuk memeriksa nilai yang terdapat
pada sebuah Range atau Collection. Range sendiri merupakan salah satu tipe data yang
unik di mana kita dapat menentukan nilai awal dan nilai akhir. Range dan Collection akan
dibahas terpisah pada modul berikutnya.
Berikut adalah contoh saat kita hendak mengecek apakah sebuah nilai ada di dalam sebuah
Range atau tidak.
1. fun main() {
2. val value = 27
3. val ranges = 10..50
4.
5. when(value){
6. in ranges -> println("value is in the range")
7. !in ranges -> println("value is outside the range")
8. else -> println("value undefined")
9. }
10. }
11.
12. /*
13. output : value is in the range
14. */
Branch pertama pada contoh kode di atas akan memeriksa apakah nilai dari value terdapat
di cakupan nilai ranges. Kemudian untuk branch kedua akan memeriksa apakah nilai
dari value tidak terdapat pada nilai yang dicakup oleh ranges. Sedangkan branch else akan
mengevaluasi jika dua kondisi sebelumnya tidak terpenuhi.
Sejak Kotlin 1.3, kita dapat menangkap subjek dari when expression di dalam sebuah
variabel. Contohnya seperti berikut:
1. fun main() {
2. val registerNumber = when(val regis = getRegisterNumber()){
3. in 1..50 -> 50 * regis
4. in 51..100 -> 100 * regis
5. else -> regis
6. }
7. }
8.
9. fun getRegisterNumber() = Random.nextInt(100)
Jika kita melihat penjelasan dan contoh penggunaan dari when expression di atas, ia
memiliki kesamaan dengan if expression. Lantas disituasi seperti apa kita
menggunakannya? if expression sebaiknya digunakan ketika kondisi yang diberikan tidak
lebih dari 2 (dua) dan kondisi yang diberikan tidak terlalu rumit.
Berbeda dengan when expression, ia bisa digunakan ketika kondisi yang diberikan lebih
dari 2 (dua).
1. Hello World
2. Hello World
3. Hello World
4. Hello World
5. Hello World
1. fun main() {
2. println("Hello World")
3. println("Hello World")
4. println("Hello World")
5. println("Hello World")
6. println("Hello World")
7. }
Think! Bagaimana jika teks yang harus ditampilkan berjumlah banyak? Tentu kita tidak
mungkin menuliskan fungsi println() sesuai dengan jumlah yang kita ingin tampilkan.
Nah, untuk mengatasinya kita bisa menggunakan perulangan. Perulangan adalah proses
perulangan blok yang sama tanpa henti sampai kondisi yang diberikan tidak terpenuhi atau
bernilai false.
Perulangan terdiri dari While, Do While dan For Loop. Modul ini akan membahas While
dan Do While. Sementara For Loop akan dibahas terpisah pada modul berikutnya.
While
Untuk menggunakan While, kita membutuhkan kata kunci while, lanjut ke kondisi di dalam
tanda kurung, dan diakhiri oleh blok body dari while itu sendiri. Berikut adalah contoh dari
penggunaan While:
1. fun main() {
2. var counter = 1
3. while (counter <= 7){
4. println("Hello, World!")
5. counter++
6. }
7. }
8. /*
9. output :
10. Hello, World!
11. Hello, World!
12. Hello, World!
13. Hello, World!
14. Hello, World!
15. Hello, World!
16. Hello, World!
17. */
Perhatikan kondisi dari While di atas, selama nilai dari variabel counter kurang dari sama
dengan 7 maka kode yang di dalamnya akan terus dilakukan. Lalu ketika kondisi tersebut
sudah tak terpenuhi maka proses perulangan akan dihentikan.
1. fun main() {
2. var counter = 8
3. while (counter <= 7){
4. println("Hello, World!")
5. counter++
6. }
7. }
Dengan While kita tidak perlu menuliskan fungsi println() secara berulang untuk mencetak
teks ke dalam konsol seperti contoh kasus di awal.
Do While
Selain menggunakan While, kita juga bisa menggunakan Do While untuk melakukan
perulangan seperti berikut:
1. fun main() {
2. var counter = 1
3. do {
4. println("Hello, World!")
5. counter++
6. } while (counter <= 7)
7. }
8.
9. /*
10. output:
11. Hello, World!
12. Hello, World!
13. Hello, World!
14. Hello, World!
15. Hello, World!
16. Hello, World!
17. Hello, World!
18. */
Berbeda dengan While, Do While bersifat Exit Controlled Loop di mana proses perulangan
akan langsung dijalankan di awal. Jika telah selesai, barulah kondisi yang diberikan akan
dievaluasi.
Saat menggunakan While dan Do While perhatikan infinite loop, yaitu kondisi di mana
proses perulangan berlangsung terus menerus sampai aplikasi menjadi crash. Contoh dari
infinite loop adalah seperti berikut:
1. fun main() {
2. var value = 'A'
3. do {
4. print(value)
5. } while (value <= 'Z')
6. }
Infinite loop terjadi jika kondisi yang diberikan selamanya terpenuhi atau bernilai true.
While dan Do While sendiri tidak dapat digunakan untuk melakukan perulangan pada rentan
angka. Untuk melakukannya kita bisa menggunakan For Loop yang akan kita pelajari pada
modul selanjutnya.
Range
Seperti yang disampaikan sebelumnya, Range merupakan salah satu tipe yang unik pada
kotlin. Kita dapat menentukan nilai awal dan nilai akhir pada Range. Range
direpresentasikan dengan operator .. atau dengan fungsi rangeTo() dan downTo().
Terdapat beberapa cara untuk membuat Range di Kotlin. Pertama, seperti berikut:
1. fun main() {
2. val rangeInt = 1..10
3. print(rangeInt.step)
4. }
5.
6. /*
7. output: 1
8. */
Dan untuk mengubah nilai dari step bisa dilakukan ketika kita menginisialisasi nilai yang
dicakup Range itu sendiri:
1. fun main() {
2. val rangeInt = 1..10 step 2
3. rangeInt.forEach {
4. print("$it ")
5. }
6. println(rangeInt.step)
7. }
8.
9. /*
10. output: 1 3 5 7 9 2
11. */
Pada kode di atas kita menentukan nilai step adalah 2, maka nilai yang dicakup variabel
rangeInt adalah 1, 3, 5, 7, 9.
Kita juga bisa menentukan nilai yang dicakup pada Range dengan urutan terbalik seperti
berikut:
Kita juga bisa memeriksa apakah suatu nilai ada pada cakupan nilai Range.
1. fun main() {
2. val tenToOne = 10.downTo(1)
3. if (7 in tenToOne) {
4. println("Value 7 available")
5. }
6. }
7. /*
8. output: Value 7 available
9. */
Pada kode di atas kita menggunakan kata kunci in untuk memeriksa apakah 7 berada
diantara kisaran 1 sampai 10. Expression yang dievaluasi pada if di atas sama seperti ketika
menggunakan expression berikut:
1. fun main() {
2. if (1 <= 7 && 7 <= 10){
3. println("Value 7 available")
4. }
5. }
6. /*
7. output: Value 7 available
8. */
Nah, di atas kita telah memeriksa apakah suatu nilai ada pada nilai cakupan Range.
Sebaliknya, kita juga bisa memeriksa apakah suatu nilai tidak ada pada nilai cakupan
Range tersebut. Kita bisa menggunakan kata kunci !in seperti ini:
1. fun main() {
2. val tenToOne = 10.downTo(1)
3. if (11 !in tenToOne) {
4. println("No value 11 in Range ")
5. }
6. }
7. /*
8. output: No value 11 in Range
9. */
1. fun main() {
2. val ranges = 1..5
3. for (i in ranges){
4. println("value is $i!")
5. }
6. }
7.
8. /*
9. output :
10. value is 1!
11. value is 2!
12. value is 3!
13. value is 4!
14. value is 5!
15.
16. */
Kode di atas merupakan contoh ketika melakukan perulangan pada Ranges dengan
menggunakan range expression yang sudah kita pelajari sebelumnya. Karena
menggunakan range expression, kita juga dapat menuliskannya seperti berikut:
1. fun main() {
2. val ranges = 1.rangeTo(5)
3. for (i in ranges){
4. println("value is $i!")
5. }
6. }
7.
8. /*
9. output :
10. value is 1!
11. value is 2!
12. value is 3!
13. value is 4!
14. value is 5!
15.
16. */
Selain itu, kita juga dapat menuliskan For loop menggunakan range expression seperti
berikut:
1. fun main() {
2. val ranges = 1.rangeTo(10) step 3
3. for (i in ranges ){
4. println("value is $i!")
5. }
6. }
7.
8. /*
9. output :
10. value is 1!
11. value is 4!
12. value is 7!
13. value is 10!
14. */
Pada kode di atas, kita menambahkan ekstensi step yang akan mengembalikan nilai baru
dengan tipe IntProgression dengan jarak nilai sebelumnya adalah 3.
Kita juga dapat mengakses indeks untuk setiap elemen yang ada pada Ranges dengan
memanfaatkan fungsi withIndex() seperti berikut:
1. fun main() {
2. val ranges = 1.rangeTo(10) step 3
3. for ((index, value) in ranges.withIndex()) {
4. println("value $value with index $index")
5. }
6. }
7. /*
8. output :
9. value 1 with index 0
10. value 4 with index 1
11. value 7 with index 2
12. value 10 with index 3
13. */
Kita menggunakan kata kunci for untuk memulai proses perulangan. Untuk tujuan yang
sama, kita juga bisa loh, memanfaatkan salah satu ekstensi pada Kotlin yaitu forEach.
Contohnya seperti berikut:
1. fun main() {
2. val ranges = 1.rangeTo(10) step 3
3. ranges.forEach { value ->
4. println("value is $value!")
5. }
6. }
7.
8. /*
9. output :
10. value is 1!
11. value is 4!
12. value is 7!
13. value is 10!
14. */
forEach pada kode di atas merupakan sebuah lambda expression yang hanya memiliki satu
argumen yaitu nilai tunggal yang dicakup pada ranges. Jika kita mendapatkan indeks dari
tiap nilai yang dicakup kita bisa menggunakan ekstensi forEachIndexed seperti berikut:
1. fun main() {
2.
3. val ranges = 1.rangeTo(10) step 3
4. ranges.forEachIndexed { index, value ->
5. println("value $value with index $index")
6. }
7. }
8. /*
9. output :
10. value 1 with index 0
11. value 4 with index 1
12. value 7 with index 2
13. value 10 with index 3
14. */
Sebenarnya ini merupakan sebuah aturan di mana ketika argumen dari sebuah lambda
expression tidak digunakan, kita disarankan agar mengubahnya menjadi _ untuk
menggantikan nama dari argumen tersebut
Break dan Continue
Ketika melakukan perulangan, terkadang kita dihadapkan dengan data yang tak sesuai
harapan. Contoh, seperti berikut:
1. fun main() {
2. val listOfInt = listOf(1, 2, 3, null, 5, null, 7)
3. for (i in listOfInt) {
4. print(i)
5. }
6. }
7. /*
8. output: 123null5null7
9. */
Proses perulangan pada kode di atas akan menghasilkan nilai null. Jika kita mengelola nilai
tersebut, ada potensi NullPointerException di sana. Lalu bagaimana kita melewatkan atau
menghentikan proses perulangan jika nilai yang dihasilkan bernilai null? Nah, di sini kita
bisa menggunakan Break dan Continue.
Continue digunakan untuk melewatkan proses iterasi dan lanjut dengan proses iterasi
berikutnya. Sementara itu, Break digunakan untuk menghentikan proses iterasi.
Berikut adalah contoh proses iterasi pada kode di atas. Kita akan coba melewatkannya jika
nilai yang dihasilkan adalah null.
1. fun main() {
2. val listOfInt = listOf(1, 2, 3, null, 5, null, 7)
3.
4. for (i in listOfInt) {
5. if (i == null) continue
6. print(i)
7. }
8. }
9. /*
10. output: 12357
11. */
Berikut adalah contoh jika kita ingin menghentikan proses iterasi ketika nilai yang dihasilkan
bernilai null.
1. fun main() {
2. val listOfInt = listOf(1, 2, 3, null, 5, null, 7)
3.
4. for (i in listOfInt) {
5. if (i == null) break
6. print(i)
7. }
8. }
1. fun main() {
2. loop@ for (i in 1..10) {
3. println("Outside Loop")
4.
5. for (j in 1..10) {
6. println("Inside Loop")
7. if ( j > 5) break@loop
8. }
9. }
10. }
11.
12. /*
13. output :
14. Outside Loop
15. Inside Loop
16. Inside Loop
17. Inside Loop
18. Inside Loop
19. Inside Loop
20. Inside Loop
21. */
Pada kode diatas, break yang sudah ditandai dengan label akan dilompati ke titik awal
proses perulangan yang sudah ditandai dengan label. break akan menghentikan proses
perulangan terluar dari dalam proses perulangan, di mana break tersebut dipanggil.
Rangkuman dari Control Flow
Sudah bisa menentukan alur program yang akan dikembangkan? Yuk kita ulas lagi apa
yang sudah kita pelajari pada modul Control Flow:
Untuk menghindari penggunaan konstan yang keliru, kita bisa memanfaatkan fitur
Enumeration untuk menyimpan kumpulan objek yang telah didefinisikan menjadi tipe
data konstanta.
Jika memiliki beberapa ekspresi untuk menentukan hasil evaluasi, Anda bisa
menggunakan when expression. Karena sebuah expression, when dapat
mengembalikan nilai yang dapat ditampung pada sebuah variabel.
Sama seperti when, if expression dapat mengembalikkan nilai yang dapat ditampung
pada sebuah variabel. Namun sedikit berbeda dengan when, if lebih cocok
digunakan jika ekspresi yang akan digunakan untuk dievaluasi hanya 1 (satu).
Dalam mempelajari bahasa pemgrograman, Anda akan sering menjumpai istilah
expressions dan statement.
Jika ingin melakukan perulangan, ada beberapa cara yang dapat diterapkan yaitu:
o While
While bersifat Entry Controlled Loop. Artinya, kondisi yang diberikan akan
dievaluasi terlebih dahulu. Jika kondisi tersebut terpenuhi maka proses
perulangan akan dijalankan.
o Do While
Do While bersifat Exit Controlled Loop di mana proses perulangan akan
langsung dijalankan di awal. Jika telah selesai, barulah kondisi yang diberikan
akan dievaluasi.
o For Loop
For merupakan konsep perulangan pada blok yang sama selama hasil
evaluasi kondisi yang diberikan terpenuhi atau bernilai true. For
memanfaatkan tipe data Range untuk menentukan kondisi yang akan
dievaluasi.
Saat menggunakan While dan Do While perhatikan infinite loop, yaitu kondisi di
mana proses perulangan berlangsung terus menerus sampai aplikasi menjadi crash.
Saat menerapkan perulangan, kita bisa memanfaatkan kata
kunci break dan continue. Kedua kata kunci tersebut digunakan untuk menentukan
proses perulangan akan seperti apa di mana break digunakan untuk menghentikan
proses perulangan walaupun hasil evaluasi masih menghasil true dan continue
digunakan untuk melanjutkan proses perulangan selanjutnya.
Persiapan
Setiap modul pada akademi ini memiliki beberapa latihan yang harus Anda kerjakan. Latihan-latihan yang ada akan
meningkatkan pemahaman Anda terhadap sebuah materi, jadi sangat disarankan untuk Anda menyelesaikannya
segera setelah membaca 1 (satu) modul besar. Untuk mengerjakan latihan, Anda akan menggunakan sebuah
plugin pada IntelliJ IDEA, yaitu EduTools. Dengan EduTools, semua pekerjaan Anda akan diperiksa secara otomatis.
Ikuti tutorial berikut untuk menginstal plugin EduTools pada IntelliJ IDEA:
Setelah EduTools berhasil diinstal, buka proyek latihan dengan mengikuti tutorial berikut:
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda mempelajari macam - macam Control Flow yang ada pada Kotlin. Untuk menguji
pemahaman Anda terhadap materi yang sudah dipelajari, Anda perlu mengerjakan
beberapa latihan sederhana. Buka proyek latihan dan kerjakan semua latihan yang ada
pada modul Control Flow.
Data Class
Pada modul ini, kita akan mempelajari sebuah fitur menarik pada Kotlin, yaitu Data Class.
Kotlin mengenalkan konsep data class yang merupakan sebuah kelas sederhana yang bisa
berperan sebagai data container. Data class adalah sebuah kelas yang tidak memiliki logika
apapun dan juga tidak memiliki fungsionalitas lain selain menangani data.
Kenapa disebut dengan kelas sederhana? Seperti yang sudah kita ketahui, Kotlin
memungkinkan kita untuk menulis kode dengan ringkas dan lebih efisien. Dalam membuat
sebuah data class, kita tidak perlu menuliskan banyak kode yang seharusnya dibutuhkan
untuk mengelola sebuah data. Data class mampu menyediakan beberapa fungsionalitas
yang biasanya kita butuhkan untuk mengelola data hanya dengan sebuah keyword data.
Hanya dengan satu baris kode di atas, kompiler akan secara otomatis
menghasilkan constructor, toString(), equals(), hashCode(), copy() dan juga
fungsi componentN(). Tentunya ini jauh lebih mudah dan bersih dibandingkan kita harus
menuliskan banyak kode secara manual.
Beberapa hal yang perlu diperhatikan dalam membuat sebuah data class adalah:
Konstruktor utama pada kelas tersebut harus memiliki setidaknya satu parameter;
Semua konstruktor utama perlu dideklarasikan sebagai val atau var;
Modifier dari sebuah data class tidak bisa abstract, open, sealed, atau inner.
Penggunaan Data Class
Sebelum kita masuk ke dalam pembahasan tentang apa saja yang bisa data class lakukan,
mari kita perhatikan dulu kode berikut:
Kode di atas merupakan sebuah kelas yang umumnya digunakan untuk menampung
sebuah data. Kelas tersebut memiliki sebuah konstruktor yang berisi beberapa properti yang
bisa kita akses, baik itu create maupun read. Selanjutnya, perhatikan juga kelas berikut:
Jalankan fungsi main dan lihatlah hasil yang ditampilkan pada konsol:
oo.User@4d7e1886
DataUser(name=nrohmen, age=17)
User(name=nrohmen, age=17)
DataUser(name=nrohmen, age=17)
Selanjutnya, kelebihan lain dari data class adalah ia sudah memiliki fungsi equals() secara
otomatis. Maka jika Anda ingin melakukan komparasi antara 2 buah objek, lakukanlah
dengan mudah seperti contoh di bawah ini:
1. fun main(){
2. val dataUser = DataUser("nrohmen", 17)
3. val dataUser2 = DataUser("nrohmen", 17)
4. val dataUser3 = DataUser("dimas", 24)
5.
6. println(dataUser.equals(dataUser2))
7. println(dataUser.equals(dataUser3))
8.
9. }
Konsol akan langsung memberi tahu apakah kedua objek tersebut sama atau tidak ketika
Anda menjalankan fungsi main():
true
false
Lain halnya jika kita melakukan komparasi pada 2 buah objek yang bukan dari data class.
Kita tidak bisa mendapatkan hasil yang akurat karena konsol akan selalu menghasilkan
nilai false. Sebagai contoh, perhatikanlah kode berikut:
1. fun main(){
2. val user = User("nrohmen", 17)
3. val user2 = User("nrohmen", 17)
4. val user3 = User("dimas", 24)
5.
6. println(user.equals(user2))
7. println(user.equals(user3))
8. }
false
false
Dan jika Anda menginginkan hasil yang akurat seperti pada data class, maka Anda perlu
membuat fungsi equals() secara manual:
Anda perlu menuliskan beberapa boilerplate code di atas untuk mendapatkan hasil yang
sesuai. Belum lagi ketika Anda menambahkan fungsi equals(), Anda juga perlu
menambahkan fungsi hashCode().
Menyalin dan Memodifikasi Data Class
Data class juga memungkinkan kita untuk menyalin sebuah objek dengan sangat mudah
hanya dengan memanfaatkan fungsi copy() di dalamnya. Untuk mencobanya, buatlah objek
baru dari kelas DataUser seperti berikut:
1. fun main(){
2. val dataUser = DataUser("nrohmen", 17)
3. val dataUser2 = DataUser("nrohmen", 17)
4. val dataUser3 = DataUser("dimas", 24)
5. val dataUser4 = dataUser.copy()
6.
7. println(dataUser4)
8. }
DataUser(name=nrohmen, age=18)
Tanpa data class, untuk melakukan tugas seperti ini kita memerlukan sebuah instance baru
untuk mengubah nilai dari suatu objek. Dengan demikian kita harus memodifikasi properti
yang kita maksud. Tugas ini akan berulang dan membuat kode yang kita tulis, jauh dari
paradigma clean code.
Destructuring Declarations
Destructuring Declaration adalah proses memetakan objek menjadi sebuah variabel. Ini bisa
dengan mudah kita lakukan pada data class. Dengan fungsi componentN() yang ada pada
data class, kita bisa menguraikan sebuah objek menjadi beberapa properti yang dimilikinya.
Sebagai contoh, kita ingin menguraikan objek dataUser:
1. fun main(){
2. val dataUser = DataUser("nrohmen", 17)
3.
4. val name = dataUser.component1()
5. val age = dataUser.component2()
6.
7. println("My name is $name, I am $age years old")
8. }
Maka jika kode di atas dijalankan, konsol akan menampilkan teks berikut:
Kita juga dapat membuat beberapa variabel dari objek secara langsung dengan kode seperti
berikut:
1. fun main(){
2. val dataUser = DataUser("nrohmen", 17)
3. val (name, age) = dataUser
4.
5. println("My name is $name, I am $age years old")
6. }
Jika dijalankan, seharusnya konsol akan menampilkan hasil yang sama seperti kode
sebelumnya.
Kesimpulannya, seperti aspek - aspek lain dari Kotlin, data class bertujuan untuk
mengurangi jumlah kode boilerplate yang Anda tuliskan. Dan perlu diketahui bahwa data
class tidak hanya sekedar untuk mengelola properti yang ada di dalamnya. Ketika
mempunyai data yang sangat kompleks, kita juga bisa menerapkan sebuah behaviour di
dalam data class. Contoh sederhananya, kita bisa membuat fungsi di dalam data class
seperti berikut:
1. fun main(){
2. val dataUser = DataUser("nrohmen", 23)
3. dataUser.intro()
4. }
Collections
Setelah berkenalan dan mempelajari data class, selanjutnya kita akan mencoba untuk
mempelajari collection. Bayangkan ketika kita ingin menyimpan dan memanipulasi sebuah
objek. Kita perlu sebuah komponen yang mampu menambahkan, menghapus, mencari,
bahkan mengurutkan sebuah data. Semua tugas itu bisa kita lakukan dengan bantuan
collection. Collections sendiri merupakan sebuah objek yang bisa menyimpan kumpulan
objek lain termasuk data class. Dengan collection kita bisa menyimpan banyak data
sekaligus. Di dalam collections terdapat beberapa objek turunan, di antaranya
adalah List, Set, dan Map. Mari kita pelajari satu per satu objek turunan tersebut.
List
Yang pertama adalah List. Dengan List kita dapat menyimpan banyak data menjadi satu
objek. Sebagai contoh, kita bisa membuat sebuah List yang berisi sekumpulan data angka,
karakter atau yang lainnya. Yang menarik, sebuah List tidak hanya bisa menyimpan data
dengan tipe yang sama. Namun juga bisa berisi bermacam - macam tipe data
seperti Int, String, Boolean atau yang lainnya. Cara penulisannya pun sangat mudah.
Perhatikan saja beberapa contoh kode berikut.
Kode di atas adalah contoh dari satu objek List yang berisi kumpulan data dengan tipe
Integer. Karena kompiler bisa mengetahui tipe data yang ada dalam sebuah objek List,
maka tak perlu kita menuliskannya secara eksplisit. Ini tentunya akan menghemat kode
yang kita ketikkan:
Sedangkan untuk membuat List dengan tipe data yang berbeda, cukup masukkan saja data
tersebut seperti kode berikut:
Karena setiap objek pada Kotlin merupakan turunan dari kelas Any, maka variabel anyList
tersebut akan memiliki tipe data List<Any>. Jika kita tampilkan list di atas maka konsol akan
menampilkan:
[a, Kotlin, 3, true]
Bahkan kita pun bisa memasukkan sebuah data class ke dalam List tersebut:
Ketika bermain dengan sebuah List, tentunya ada saat di mana kita ingin mengakses posisi
tertentu dari List tersebut. Untuk melakukannya, kita bisa menggunakan
fungsi indexing seperti berikut:
1. println(anyList[3])
Lalu apa yang akan terjadi jika kita berusaha menampilkan item dari List yang berada di luar
dari ukuran List tersebut? Sebagai contoh, Anda ingin mengakses indeks ke-5 dari anyList:
1. println(anyList[5])
Hasilnya adalah eror! Kompiler akan memberitahukan bahwa perintah itu tidak bisa
dijalankan. Berikut pesan eror yang akan muncul:
Pesan di atas memberitahu kita bahwa List telah diakses dengan indeks ilegal. Ini akan
terjadi jika indeks yang kita inginkan negatif atau lebih besar dari atau sama dengan ukuran
List tersebut.
Informasi Tambahan:
Sejauh ini kita baru belajar menginisialisasikan atau mengakses data dari sebuah List. Pastinya Anda
bertanya, apakah bisa kita memanipulasi data pada List tersebut? Jawabannya tidak. Apa pasal? List
tersebut bersifat immutable alias tidak bisa diubah. Namun jangan khawatir. Kotlin standard library juga
menyediakan collection dengan tipe mutable. Artinya kita melakukan perubahan pada nilainya dengan
cara seperti menambah, menghapus, atau mengganti nilai yang sudah ada. Caranya pun cukup mudah.
Anda hanya perlu menggunakan fungsi mutableListOf seperti berikut:
Dengan begitu, anyList sekarang merupakan sebuah List yang bersifat mutable dan kita bisa
memanipulasi data di dalamnya.
Perhatikan kode di atas. Di sana terdapat beberapa angka yang duplikat, yaitu
angka 1 dan 2. Silakan tampilkan pada konsol dan lihat hasilnya.
1. println(integerSet)
2.
3. // Output: [1, 2, 4, 5]
Kita juga dapat melakukan pengecekan apakah sebuah nilai ada di dalam Set dengan
menggunakan kata kunci in.
1. print(5 in setA)
2.
3. // Output: true
Kemudian ada juga fungsi union dan intersect untuk mengetahui gabungan dan irisan dari 2
(dua) buah Set. Sebagai contoh:
Informasi Tambahan:
Pada Mutable Set kita bisa menambah dan menghapus item namun tak bisa mengubah nilai seperti pada
List.
String yang berada pada sebelah kiri dari kata kunci to adalah sebuah key, sedangkan yang
di sebelah kanan merupakan value-nya. Lalu untuk mengakses nilai dari Map tersebut, kita
bisa menggunakan key yang sudah dimasukkan. Misalnya, kita bisa menggunakan
key “Jakarta” untuk mendapatkan value “Indonesia”:
1. println(capital["Jakarta"])
2.
3. // Output: Indonesia
1. println(capital.getValue("Jakarta"))
2.
3. // Output: Indonesia
Hasilnya sama saja. Namun sebenarnya terdapat sebuah perbedaan antara keduanya. Saat
menggunakan simbol [ ] atau yang kita kenal dengan indexing, konsol akan menampilkan
hasil null saat key yang dicari tidak ada di dalam Map. Sedangkan saat kita
menggunakan getValue(), konsol akan memberikan sebuah Exception.
1. println(capital["Amsterdam"])
2.
3. // Output: null
4.
5.
6. println(capital.getValue("Amsterdam"))
7.
8. // Output: Exception in thread "main" java.util.NoSuchElementException: Key Amsterdam is
missing in the map.
Kita dapat menampilkan key apa saja yang ada di dalam Map dengan menggunakan
fungsi keys(). Fungsi ini akan mengembalikan nilai berupa Set karena key pada Map
haruslah unik.
Sedangkan untuk mengetahui nilai apa saja yang ada di dalam Map kita bisa menggunakan
fungsi values(). Fungsi ini akan mengembalikan collection sebagai tipe datanya.
Informasi Tambahan:
Untuk menambahkan key-value ke dalam map, kita perlu memastikan bahwa map yang digunakan
adalah mutable. Mari kita ubah map capital yang sudah kita buat sebelumnya menjadi mutable.
1. mutableCapital.put("Amsterdam", "Netherlands")
2. mutableCapital.put("Berlin", "Germany")
Namun perlu diperhatikan bahwa menggunakan mutable collection itu tidak disarankan. Apabila terdapat sebuah
mutable collection yang diubah oleh lebih dari 1 proses, hasil nya akan sulit untuk diprediksi.
Collections Operations
Selain memiliki beberapa turunan yang baru saja kita bahas, Collection juga mempunyai
beberapa fungsi operasi yang bisa kita gunakan untuk mengakses data di dalamnya.
Sekarang saatnya kita akan mempelajari fungsi-fungsi yang dimaksud.
Pada kode di atas, kita telah menggunakan filter() untuk menyaring bilangan yang habis
dibagi 2 (dua) atau biasa disebut dengan bilangan genap. Selain itu kita juga dapat mem-
filter list berdasar hasil yang tak sesuai dengan kondisi yang diberikan. Caranya adalah
dengan menggunakan fungsi filterNot().
map()
Fungsi yang akan sering dipakai berikutnya adalah map(). Fungsi ini akan membuat
collection baru sesuai perubahan yang akan kita lakukan dari collection sebelumnya. Kita
ambil contoh dari numberList yang sudah ada. Lalu kita buat collection baru yang isinya
adalah hasil kali 5 (lima) dari masing-masing item. Maka Anda bisa menggunakan kode
seperti berikut:
count()
Fungsi count() dapat kita gunakan untuk menghitung jumlah item yang ada di dalam
collection. Kembali gunakan contoh numberList, kita akan menampilkan jumlah item yang
ada di dalamnya.
Pada kode di atas konsol akan menampilkan 3 sebagai jumlah item yang merupakan
kelipatan 3, yaitu: 3, 6, dan 9.
sorted()
sorted() digunakan untuk mengurutkan item yang ada di dalam collection. Secara default
fungsi sorted() ini akan mengurutkan data secara ascending. Perhatikan kode di bawah ini:
1. fun main() {
2. val list = (1..1000000).toList()
3. list.filter { it % 5 == 0 }.map { it * 2 }.forEach { println(it) }
4. }
Pada contoh kode di atas, kita memiliki data collection sejumlah 1 juta item, kemudian
masing-masing data akan disaring berdasarkan angka yang merupakan kelipatan 5 lalu
dikalikan 2 dan akhirnya ditampilkan pada konsol. Dengan eager evaluation atau dikenal
dengan horizontal evaluation, list akan menyelesaikan proses filter terhadap 1 juta data baru
kemudian melakukan mapping data sampai akhirnya ditampilkan pada konsol.
1. fun main() {
2. val list = (1..1000000).toList()
3. list.asSequence().filter { it % 5 == 0 }.map { it + it }.forEach { println(it) }
4. }
Dengan sequence, operasi akan dilakukan secara vertikal atau per item, misalnya dimulai
angka 1. Karena tidak memenuhi kondisi pada filter maka operasi tidak akan dilanjutkan
ke map(). Sampai dengan iterasi ke-5 akan dilakukan mapping, angka 5 akan dikalikan 2
dan ditampilkan pada konsol, setelah itu akan dilanjutkan ke iterasi berikutnya sampai
dengan iterasi ke-1 juta.
Untuk membuat objek Sequence, kita bisa menggunakan fungsi yang tersedia
pada standard library yaitu generateSequence().
1. fun main() {
2. val sequenceNumber = generateSequence(1) { it + 1 }
3. sequenceNumber.take(5).forEach { print("$it ") }
4. }
5. // Output: 1 2 3 4 5
Yosh! Anda sudah selesai dengan materi beberapa tipe Classes dan Collections. Sekarang
kita akan merangkum apa saja yang sudah Anda pelajari.
Data class merupakan sebuah kelas sederhana yang bisa berperan sebagai data
container.
Data class adalah kelas yang bisa disebut spesial karena kita tidak perlu
menentukkan nilai dari fungsi toString(), equals(), dan hashCode() ketika
digunakan.
Data class juga menyediakan fungsi copy() yang memudahkan kita untuk menyalin
instance dari data class yang sudah dibuat.
Collections merupakan sebuah objek yang bisa menyimpan kumpulan objek lain
termasuk data class. Berikut adalah beberapa jenis Collections yang sudah
dipelajari:
o List
Dengan List kita dapat menyimpan banyak data menjadi satu objek. Sebagai
contoh, kita bisa membuat sebuah List yang berisi sekumpulan data angka,
karakter atau yang lainnya.
o Set
Set merupakan sebuah collection yang hanya dapat menyimpan nilai yang
unik. Ini akan berguna ketika Anda menginginkan tidak ada data yang sama
atau duplikasi dalam sebuah collection. Kita bisa mendeklarasikan sebuah
Set dengan fungsi setOf.
o Map
Map, yakni sebuah collection yang dapat menyimpan data dengan format
key-value.
o Sequences
Sequence merupakan collection yang bisa dikategorikan ke dalam lazy
evaluation. Jika eager evaluation mengevaluasi seluruh item yang ada pada
collection, lazy evaluation hanya akan mengevaluasi item jika benar-benar
diperlukan.
Collection juga mempunyai beberapa fungsi operasi yang bisa kita gunakan untuk
mengakses data di dalamnya dengan cara yang mudah, beberapa diantaranya
adalah:
o filter() dan filterNot()
Kedua fungsi tersebut akan menghasilkan list baru dari seleksi berdasarkan
kondisi yang kita berikan. Sesuai dengan namanya, untuk memfilter atau
menyaring suatu data dalam sebuah collection.
o map()
Fungsi ini akan membuat collection baru sesuai perubahan yang akan kita
lakukan dari collection sebelumnya.
o count()
Fungsi count() dapat kita gunakan untuk menghitung jumlah item yang ada di
dalam collection.
o find(), firstOrNull(), & lastOrNull()
Selanjutnya adalah fungsi yang digunakan untuk mencari item pada sebuah
collection. Untuk mencari item pertama yang sesuai dengan kondisi yang kita
tentukan, kita bisa menggunakan fungsi find().
o first() & last()
Hampir sama seperti fungsi firstOrNull() dan lastOrNull(),
fungsi first() dan last() digunakan untuk menyaring item pertama atau
terakhir dari sebuah collection. Kita juga bisa menambahkan sebuah kondisi
dengan parameter lambda.
o sum()
Mungkin Anda sudah tahu fungsi ini. Fungsi sum() khusus hanya bisa
digunakan untuk collection yang bertipe angka. Fungsi ini akan
menjumlahkan setiap data yang ada pada collection.
o sorted()
sorted() digunakan untuk mengurutkan item yang ada di dalam collection.
Secara default fungsi sorted() ini akan mengurutkan data secara ascending.
Persiapan
Setiap modul pada akademi ini memiliki beberapa latihan yang harus Anda kerjakan. Latihan-latihan yang ada akan
meningkatkan pemahaman Anda terhadap sebuah materi, jadi sangat disarankan untuk Anda menyelesaikannya
segera setelah membaca 1 (satu) modul besar. Untuk mengerjakan latihan, Anda akan menggunakan sebuah
plugin pada IntelliJ IDEA, yaitu EduTools. Dengan EduTools, semua pekerjaan Anda akan diperiksa secara otomatis.
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda sudah mempelajari apa itu data class, bagaimana cara menggunakannya, serta
mempelajari beberapa macam Collection pada Kotlin.
Untuk menguji pemahaman Anda terhadap materi yang sudah dipelajari, Anda perlu
mengerjakan beberapa latihan sederhana. Buka kembali proyek latihan dan kerjakan semua
latihan yang ada pada modul Data Classes dan Collections.
Functional Programming
Seperti yang sudah disampaikan di awal akademi. Kotlin adalah sebuah multiparadigm
programming language. Artinya selain merupakan bahasa pemrograman berorientasi objek,
dalam penulisan sintaksnya Kotlin menggunakan gaya functional programming.
Kode di atas biasanya kita tuliskan untuk mendapatkan nilai tertentu dari sebuah list. Karena
kode pada Kotlin bisa dituliskan dengan gaya fungsional, maka kode di atas cukup dituliskan
seperti berikut:
Itu adalah salah satu contoh kenapa Kotlin termasuk ke dalam functional programming.
Untuk lebih memahaminya, kita akan belajar tentang fitur atau komponen pada Kotlin yang
terkait dengan functional programming. Anda akan mempelajari lebih detail tentang anatomi
dari sebuah fungsi pada Kotlin, bagaimana fungsi Kotlin berperilaku, lambda, higher-
order function dan konsep fungsional lainnya.
Anatomi Function
Pada modul pengenalan, kita sudah belajar tentang function (fungsi). Mulai dari bagaimana
cara kita mendeklarasinya, menentukan apakah fungsi tersebut dapat mengembalikan nilai,
serta melampirkan sebuah argumen ketika fungsi tersebut digunakan.
Pada modul ini kita akan membahas lebih dalam tentang bagian apa saja yang terdapat
dalam sebuah fungsi. Pada dasarnya sebuah fungsi memiliki 2 (dua) bagian utama
yaitu function header dan function body.
Function Header
Function header adalah bagian yang merupakan konstruksi dari sebuah fungsi untuk
menentukan perilakunya akan seperti apa. Di dalam function header terdapat visibility
modifier, kata kunci fun, nama, daftar parameter dan nilai kembalian dari fungsi tersebut.
Visibility Modifier
Visibility modifier atau tingkatan akses merupakan bagian spesifik dari sebuah bahasa
pemrograman yang ditujukan untuk mengatur bagaimana hak akses dari sebuah kelas,
fungsi, properti dan variabel.
Function Name
Setelah visibility modifier, selanjutnya adalah kata kunci fun . Ini digunakan untuk
menandakan jika baris kode tersebut merupakan sebuah fungsi yang kemudian diikuti oleh
nama dari fungsi tersebut.
Nama dari sebuah fungsi merupakan sebuah identifier yang akan memudahkan kita untuk
menggunakan fungsi tersebut. Perlu diketahui bahwa kita tidak bisa memberikan nama yang
sama untuk beberapa fungsi.
Function Parameter
Ketika kita mendeklarasikan sebuah fungsi baru, kita bisa atau tanpa menetapkan
parameter tersebut. Parameter adalah data atau nilai yang disematkan ketika fungsi tersebut
dipanggil. Nantinya parameter akan diproses di dalam fungsi tersebut. Kita bisa
melampirkan nilai konstan atau variabel untuk sebuah fungsi ketika ia dipanggil.
Parameter dari sebuah fungsi terdiri dari nama dan tipe dari parameter itu sendiri. Ia
digunakan untuk menentukan nilai atau variabel apa yang dapat dilampirkan. Setiap
parameter dipisahkan oleh tanda (,). Catatan: setiap parameter bersifat read-only, sehingga
kita hanya diijinkan untuk menggunakan nilainya untuk diproduksi.
Lalu apakah tipe ini harus ditentukan setiap deklarasi sebuah fungsi? Tentu tidak. Jika kita
tidak menentukan return type, secara implisit fungsi tersebut akan mengembalikan nilai
dengan tipe Unit, yaitu return type yang tidak mengembalikan nilai signifikan.
Function Body
Setelah function header, selanjutnya adalah function body, yang ditandai dengan curly
braces. Di dalamnya kita akan menempatkan sebuah logika kode baik itu
sebuah expression atau statement. Seperti yang dijelaskan pada modul pengenalan, jika
kita menetapkan sebuah fungsi dapat mengembalikan nilai, maka kita wajib menambah
sebuah baris kode yang diawali dengan kata kunci return yang diikuti oleh expression untuk
menetapkan nilai yang akan dikembalikan.
Sebaliknya, jika kita tidak ingin mengembalikan nilai, kita tidak perlu menambahkannya
seperti yang disebutkan di atas.
Setiap fungsi yang kita deklarasikan, memiliki ruang lingkup tersendiri. Contohnya saat kita
mendeklarasi sebuah variabel di dalamnya, variabel tersebut akan menjadi variabel lokal
untuk fungsi itu sendiri. Dengan demikian, variabel tersebut tidak dapat diakses dari fungsi
lainnya meskipun berada di dalam satu kelas yang sama.
Di beberapa kasus pembuatannya, sebuah fungsi bisa menjadi cukup kompleks dengan
banyaknya parameter. Alhasil saat ingin memanggil fungsi tersebut, kita harus menghafal
posisi dari parameter agar tidak salah dalam melampirkan sebuah argumen.
Named Argument
Untuk mengatasi hal ini Kotlin menawarkan sebuah cara. Dengan ini, kita tak perlu lagi
menghafal posisi dari sebuah parameter. Karena ketika sebuah fungsi dipanggil, kita bisa
menentukan argumen dari parameter mana yang ingin dilampirkan dengan memanggil
nama dari parameter tersebut. Misalnya kita mempunyai sebuah fungsi seperti berikut:
Nah, dengan memanfaatkan named argument, kita bisa menuliskannya seperti di bawah ini:
1. fun main() {
2. val fullName = getFullName(first = "Kotlin" , middle = " is ", last = "Awesome")
3. print(fullName)
4. }
5.
6. fun getFullName(first: String, middle: String, last: String): String {
7. return "$first $middle $last"
8. }
Menariknya lagi, kita bisa mengubah posisi dari argumen ketika dilampirkan, misalnya
seperti ini:
1. fun main() {
2. val fullName = getFullName(middle = " is " , first = "Kotlin", last = "Awesome")
3. print(fullName)
4. }
5.
6. fun getFullName(first: String, middle: String, last: String): String {
7. return "$first $middle $last"
8. }
Dengan cara seperti di atas, kita tidak perlu lagi menghafal posisi dari parameter jika ingin
melampirkan sebuah argumen. Cukup hafalkan nama dari parameter tersebut.
Default Argument
Tidak sampai di situ, Kotlin juga memungkinkan kita untuk menentukan nilai default dari
sebuah parameter. Jika kita melewatkan argumen untuk dilampirkan, maka
nilai default tersebut lah yang akan digunakan.
Untuk menambahkan nilai default itu sendiri pun cukup mudah, yaitu dengan cara
menempatkannya langsung tepat di samping dari parameter seperti halnya ketika ingin
menginisialisasikan sebuah nilai untuk variabel. Contohnya seperti berikut:
1. fun getFullName(
2. first: String = "Kotlin",
3. middle: String = " is ",
4. last: String = "Awesome"): String {
5. return "$first $middle $last"
6. }
Kita bisa memanggil fungsi di atas seperti biasanya. Tetapi karena parameternya sudah
memiliki nilai, maka argumen untuk fungsi tersebut bisa dilewatkan ketika dipanggil.
1. fun main() {
2. val fullName = getFullName()
3. print(fullName)
4. }
5.
6. fun getFullName(
7. first: String = "Kotlin",
8. middle: String = " is ",
9. last: String = "Awesome"): String {
10. return "$first $middle $last"
11. }
12.
13. /*
14. output : Kotlin is Awesome
15. */
Menarik bukan? Ketika kita telah menetapkan nilai default, kita tak perlu khawatir saat lupa
melampirkan sebuah argumen. Tentunya ini menghindari kita dari eror. Meskipun begitu,
kita tetap bisa melampirkan sebuah argumen. Contohnya seperti berikut:
1. fun main() {
2. val fullName = getFullName(first = "Dicoding")
3. print(fullName)
4. }
5.
6. fun getFullName(
7. first: String = "Kotlin",
8. middle: String = " is ",
9. last: String = "Awesome"): String {
10. return "$first $middle $last"
11. }
12.
13. /*
14. output : Dicoding is Awesome
15. */
Dengan memanfaatkan named dan default argument, kode yang kita tulis lebih mudah
dibaca. Ini pun dapat membantu kita dalam menggunakan fungsi yang cukup kompleks.
Vararg (Variable Argument)
Selain named dan default argument, Kotlin juga memiliki vararg (variable argument).
Dengan menggunakan kata kunci vararg kita juga bisa menyederhanakan beberapa
parameter yang memiliki tipe data yang sama menjadi parameter tunggal.
Bisa kita perhatikan pada contoh kode di atas bahwa kata kunci vararg ditempatkan
sebelum nama parameter. Ketika ingin memanggil fungsi tersebut, kita bisa melampirkan
beberapa argumen, misal seperti berikut:
1. fun main() {
2. val number = sumNumbers(10, 20, 30, 40)
3. print(number)
4. }
5.
6. fun sumNumbers(vararg number: Int): Int {
7. return number.sum()
8. }
9.
10. /*
11. output : 100
12. */
Selain itu kita juga bisa menerapkan Generic untuk tipe parameter ketika parameter tersebut
ditentukan dengan vararg. Contohnya seperti berikut:
Ketika sebuah parameter ditentukan dengan vararg, pada dasarnya semua argumen yang
dilampirkan, ditampung di dalam sebuah Array <out T>. Contohnya bisa kita lihat pada
contoh kode di atas.
Karena pada dasarnya adalah sebuah Array, maka kita bisa memanggil fungsi atau properti
yang tersedia pada kelas Array dari dalam fungsi tersebut. Misal properti size:
1. fun main() {
2. val number = getNumberSize(10, 20, 30, 40, 50)
3. print(number)
4. }
5.
6. fun getNumberSize(vararg number: Int): Int {
7. return number.size
8. }
9.
10. /*
11. output : 5
12. */
Lalu kapan kita membutuhkan vararg? Ketika sebuah fungsi yang menggunakannya tidak
mengetahui jumlah argumen yang akan disematkan ketika fungsi tersebut dipanggil. Contoh
penerapan bisa kita liat pada fungsi String.format(), di mana fungsi tersebut terdapat
parameter yang ditandai dengan vararg dan dapat disematkan beberapa argumen tanpa
harus tahu batasannya.
Ketika kode di atas dijalankan, proses kompilasi akan gagal dengan log eror sebagai berikut:
Kotlin: Multiple vararg-parameters are prohibited
Selanjutnya jika kita ingin menambahkan parameter baru tanpa kata kunci vararg, parameter
yang ditandai dengan vararg sebaiknya berada pada posisi pertama. Contohnya seperti
berikut:
Lalu bagaimana jika kita ingin menempatkan parameter yang ditandai vararg bukan pada
posisi pertama? Kita bisa mendifisikannya secara langsung. Namun berbeda saat fungsi
tersebut dipanggil di mana kita harus menggunakan named argument saat ingin
melampirkan argumen untuk parameter yang tidak ditandai dengan vararg. Contohnya
seperti berikut:
1. fun main() {
2. sets(10, 10, name = "Kotlin")
3. }
4.
5. fun sets(vararg number: Int, name: String): Int {
6. ...
7. }
Kenapa demikian? Saat kita tidak menandai argumen tersebut untuk parameter yang mana,
kompiler akan menetapkan jika argumen tersebut untuk parameter yang ditandai
dengan vararg.
Vararg vs Array<T>
Karena semua argumen ditampung di dalam sebuah Array, maka akan muncul pertanyaan,
"Apa bedanya ketika kita menggunakan Array<T> sebagai tipe parameter?" Misal seperti
berikut:
1. fun main() {
2. val number = arrayOf(10, 20, 30, 40)
3. sets(number)
4. }
5.
6. fun sets(number: Array<Int>) {
7. ...
8. }
1. fun main() {
2. val number = intArrayOf(10, 20, 30, 40)
3. sets(10, 20, 20, *number , 10)
4. }
5.
6. fun sets(vararg number: Int): Int {
7. ...
8. }
Extension Functions
Untuk mendeklarasikan sebuah extension functions, kita perlu menentukan terlebih
dahulu receiver type, kemudian nama dari fungsi tersebut yang mana keduanya dipisahkan
oleh titik (.). Contohnya, seperti berikut:
1. fun Int.printInt() {
2. print("value $this")
3. }
Untuk memanggil extension functions di atas, lakukan dengan cara seperti berikut:
1. fun main() {
2. 10.printInt()
3. }
4.
5. fun Int.printInt() {
6. print("value $this")
7. }
8.
9. /*
10. output : value 10
11. */
Kita juga bisa menetapkan jika extension functions tersebut dapat mengembalikan nilai,
deklarasinya pun sama halnya seperti fungsi pada umumnya. Contohnya seperti berikut:
1. fun main() {
2. println(10.plusThree())
3. }
4.
5. fun Int.plusThree(): Int {
6. return this + 3
7. }
8.
9. /*
10. output : 13
11. */
Extension Properties
Selanjutnya adalah extension properties. Seperti yang disebutkan di awal, Kotlin juga
mendukung extension untuk menambah sebuah properti baru pada sebuah kelas tanpa
harus menyentuh kode di dalam kelas tersebut.
1. fun main() {
2. println(10.slice)
3. }
4.
5. val Int.slice: Int
6. get() = this / 2
7.
8. /*
9. output : 5
10. */
Selain menggunakan if expression, kita juga bisa menggunakan elvis operator. Misalnya
seperti berikut:
1. fun main() {
2. val value: Int? = null
3.
4. println(value.slice)
5. }
6.
7. val Int?.slice: Int
8. get() = this?.div(2) ?: 0
9.
10. /*
11. output : 0
12. */
Lalu kapan kita membutuhkannya? Tentunya jika kita mempunyai sebuah objek yang
bernilai null. Saat kita tidak menetapkannya dengan nullable receiver type, maka kita perlu
memeriksa apakah objek tersebut bernilai null atau tidak? Bisa juga dengan menggunakan
operator safe call setiap kali extension tersebut dipanggil. Contohnya seperti berikut:
1. fun main() {
2. val value: Int? = null
3. val value1: Int? = null
4.
5. println(value?.slice)
6. println(value1?.slice)
7. }
8.
9. val Int.slice: Int
10. get() = this.div(2)
11.
12. /*
13. output : null
14. null
15.
16. */
Kita juga bisa menentukan nilai dari receiver object jika bernilai null. Sehingga kita tidak
perlu lagi menggunakan operator safe call ketika ingin memanggil extension tersebut.
1. fun main() {
2. val value: Int? = null
3. val value1: Int? = null
4.
5. println(value.slice)
6. println(value1.slice)
7. }
8.
9. val Int?.slice: Int
10. get() = this?.div(2) ?: 0
11.
12. /*
13. output : 0
14. 0
15.
16. */
Function Type
Pada modul selanjutnya, kita akan belajar tentang lambda dan higher-order function,
namun sebelum itu kita perlu tahu terlebih dahulu apa itu funcion type. Seperti namanya,
Anda dapat membuat sebuah fungsi menjadi tipe data. Menarik kan?
Kotlin sendiri menggunakan function type untuk tipe deklarasi yang berhubungan dengan
sebuah fungsi. Dalam penggunaannya, terdapat beberapa tanda yang berhubungan dengan
sebuah fungsi seperti jumlah dan tipe parameter serta tipe kembalian.
Setiap function type memiliki tanda kurung . Di dalamnya terdapat sebuah parameter dan
jumlah tipe yang menandakan jumlah parameter dari fungsi tersebut. Pada contoh di atas,
fungsi tersebut memiliki 2 (dua) parameter dengan tipe Int dan memiliki tipe kembalian
String. Ketika kita tidak ingin fungsi tersebut mengembalikan nilai, kita bisa
menggunakan Unit. Berbeda dengan fungsi pada umumnya, jika menggunakan tipe
kembalian Unit, kita tidak wajib menuliskannya.
Ketika kita mempunyai beberapa fungsi yang memiliki function type yang sama, kita bisa
menyederhanakannya. Bagaimana caranya? Manfaatkan kata kunci typealias untuk
memberikan nama alternatif dari sebuah function type seperti berikut:
typealias cocok digunakan ketika kita mempunyai sebuah function type yang panjang.
Dengannya, kita bisa memberikan nama untuk sebuah function type dan menggunakannya
sebagai tipe untuk fungsi lainnya. Nah, sekarang kalau dilihat sudah benar-benar mirip
seperti tipe data kan? Mantap!
Untuk membuat instance dari sebuah function type, terdapat beberapa cara. Salah satunya
dengan lambda expression yang sudah kita bahas pada modul sebelumnya. Sedangkan
untuk menggunakan instance-nya, kita bisa memanfaatkan operator invoke() seperti berikut:
1. sum?.invoke(10, 20)
Lambda
Lambda expression, biasa disebut dengan anonymous function atau function literal adalah
fitur yang cukup populer sampai sekarang dalam dunia functional programming. Bisa disebut
sebagai anonymous karena lambda tidak memiliki sebuah nama seperti halnya sebuah
fungsi pada umumnya. Karena merupakan sebuah fungsi, lambda juga dapat memiliki
daftar parameter, body dan return type. Istilah lambda sendiri berasal dari istilah
akademis lambda calculus yang digunakan untuk menggambarkan proses komputasi.
Sebelum mempelajarinya lebih dalam, ada baiknya jika kita tahu beberapa karakteristik dari
lambda berikut:
Dalam menggunakan lambda, kita tidak perlu mendeklarasi tipe spesifik untuk nilai
kembaliannya. Tipe tersebut akan ditentukan oleh kompiler secara otomatis.
Walaupun merupakan sebuah fungsi, lambda tidak membutuhkan kata
kunci fun dan visibility modifier saat dideklarasikan, karena lambda
bersifat anonymous.
Parameter yang akan ditetapkan berada di dalam kurung kurawal {}.
Ketika ingin mengembalikan nilai, kata kunci return tidak diperlukan lagi karena
kompiler akan secara otomatis mengembalikan nilai dari dalam body.
Lambda expression dapat digunakan sebagai argumen untuk sebuah parameter dan
dapat disimpan ke dalam sebuah variabel.
Dari beberapa karakteristik di atas, lambda sangat berguna karena dapat membuat
penulisan kode menjadi lebih mudah dan sederhana. Salah satu contohnya adalah kita bisa
menghindari boilerplate code dalam menggunakan anonymous class seperti berikut:
Kode di atas adalah contoh deklarasi dari lambda, di mana fungsi lambda di atas ditandai
dengan sepasang kurung kurawal. Di dalamnya terdapat fungsi untuk mencetak teks pada
konsol. Ketika ingin menggunakannya, kita bisa memanggilnya seperti halnya kita
memanggil sebuah fungsi pada umumnya.
1. fun main() {
2. message()
3. }
4.
5. val message = { println("Hello From Lambda") }
6.
7. /*
8. output : Hello From Lambda
9. */
Jika kita ingin menambahkan sebuah parameter pada fungsi lambda, kita bisa
menuliskannya seperti berikut:
1. fun main() {
2. printMessage("Hello From Lambda")
3. }
4.
5. val printMessage = { message: String -> println(message) }
6.
7. /*
8. output : Hello From Lambda
9. */
Seperti yang disebutkan sebelumnya, parameter dari sebuah lambda berada di dalam
kurung kurawal. Untuk membedakannya dengan body, daftar parameter yang ada
dipisahkan dengan tanda ->. Kemudian, bagaimana cara mendeklarasi lambda agar dapat
mengembalikan nilai? Untuk itu kita bisa menuliskannya seperti di bawah ini:
1. fun main() {
2. val length = messageLength("Hello From lambda")
3. println("Message length $length")
4. }
5.
6. val messageLength = { message: String -> message.length }
7.
8. /*
9. output : Message length 17
10. */
Bisa kita perhatikan, kita tidak membutuhkan tipe kembalian dan kata kunci return untuk
mengembalikan sebuah nilai. Pada dasarnya, kompiler akan mengembalikan nilai
berdasarkan expression atau statement di baris terakhir di dalam body.
Higher-Order Function
Dalam mendeklarasi lambda, khususnya jika ingin ditetapkan agar dapat mengembalikan
nilai, terkadang kompiler tidak dapat menentukan tipenya. Alhasil, kita perlu menuliskannya
secara eksplisit. Terdapat beberapa tipe deklarasi yang dapat kita gunakan untuk
mendeklarasi lambda, antara lain:
Tipe deklarasi pada kode di atas adalah contoh ketika kita ingin membuat lambda yang
memiliki 1 (satu) parameter dengan tipe kembalian Int. Untuk tipe deklarasi lainnya akan kita
bahas pada modul berikutnya.
Dengan ditetapkannya tipe deklarasi pada fungsi tersebut, memungkinkan kita untuk bisa
menggunakannya sebagai argumen untuk fungsi lainnya. Contohnya seperti berikut:
1. fun main() {
2. printResult(10 ,sum)
3. }
4.
5. fun printResult(value: Int, sum: (Int) -> Int) {
6. val result = sum(value)
7. println(result)
8. }
9.
10. var sum: (Int) -> Int = { value -> value + value }
11.
12. /*
13. output : 20
14. */
Atau kita bisa melampirkannya secara langsung ketika fungsi printResult() di atas dipanggil
seperti berikut:
1. fun main() {
2. printResult(10){ value ->
3. value + value
4. }
5. }
6.
7. fun printResult(value: Int, sum: (Int) -> Int) {
8. val result = sum(value)
9. println(result)
10. }
11.
12. /*
13. output : 20
14. */
Apa itu DSL? DSL adalah sebuah bahasa komputer yang dikhususkan untuk domain
aplikasi tertentu. Ia berbeda dengan general-purpose language yang bisa diterapkan di
semua domain aplikasi. Dengan DSL, kita bisa menuliskan kode lebih ringkas dan ekspresif.
Contoh sistem yang menerapkan DSL adalah Gradle dan sistem database yang
berbasis SQL.
1. fun main() {
2. val message = buildString {
3. append("Hello ")
4. append("from ")
5. append("lambda ")
6. }
7.
8. println(message)
9. }
10.
11. fun buildString(action: StringBuilder.() -> Unit): String {
12. val stringBuilder = StringBuilder()
13. stringBuilder.action()
14. return stringBuilder.toString()
15. }
16.
17. /*
18. output : Hello from lambda
19. */
Pada dasarnya parameter action di atas dipanggil ketika kita melampirkan argumen lambda
saat fungsi tersebut digunakan. Kemudian bisa kita perhatikan, parenthesis tidak diperlukan
karena fungsi tersebut hanya memiliki satu parameter yaitu sebuah lambda expression.
Kotlin Standard Library
Kotlin hadir dengan berbagai fitur menarik yang sudah kita bahas pada modul - modul
sebelumnya. Salah satu fitur yang selanjutnya perlu kita ketahui adalah standard function
library, yaitu sebuah extension functions dari sebuah objek yang menggunakan lambda
sebagai argumen atau yang disebut sebagai higher-order function.
Scope Function
Kotlin standard library memiliki beberapa fungsi yang tujuan utamanya adalah bagaimana
menuliskan logika kode di dalam konteks dari sebuah objek. Dalam menggunakan fungsi
tersebut kita akan memanfaatkan lambda expression yang memiliki ruang lingkupnya
sendiri, di mana dalam ruang lingkup tersebut kita dapat mengakses konteks dari sebuah
objek.
Fungsi inilah yang dinamakan sebagai scope function. Beberapa fungsi yang berada di
dalamnya antara lain let, run, with, apply, dan also. Pada dasarnya beberapa fungsi
tersebut melakukan tugas yang sama yaitu mengeksekusi blok kode dari dalam sebuah
objek. Yang membedakannya adalah bagaimana objek tersebut tersedia dan seperti apa
hasil yang didapat dari seluruh expression yang berada di dalam blok.
Context Object
Sebelum kita mengulas beberapa fungsi yang menjadi bagian dari scope function di atas,
kita perlu mengetahui terlebih dahulu bagaimana cara mengakses konteks sebuah objek
dari dalam lambda yang menjadi bagian dari scope function.
Terdapat 2 (dua) cara, yaitu: diakses sebagai lambda receiver (this) dan lambda argumen
(it). Keduanya memiliki kapabilitas yang sama dan tentunya digunakan untuk kasus yang
berbeda.
Cara ini memiliki kekurangan yaitu kita tidak dapat membedakan objek receiver dengan
objek yang berada dari luar lingkup fungsi tersebut. Alih-alih, cara ini lebih ditujukan untuk
operasi objek itu sendiri, seperti memanggil fungsi dan inisialisasi nilai dari anggota yang
menjadi bagian dari objek tersebut.
Secara default, nama dari argumen tersebut adalah it, namun kita dapat mengubahnya
seperti berikut:
1. fun main() {
2. val text = "Hello"
3. val result = text.run {
4. val from = this
5. val to = this.replace("Hello", "Kotlin")
6. "replace text from $from to $to"
7. }
8. println("result : $result")
9. }
10.
11. /*
12. output : result : replace text from Hello to Kotlin
13. */
with
Selanjutnya fungsi with. Pada dasarnya fungsi with bukanlah sebuah extension melainkan
hanyalah fungsi biasa. Konteks objeknya disematkan sebagai argumen dan dari blok
lambda diakses sebagai receiver. Contohnya seperti berikut:
1. fun main() {
2. val message = "Hello Kotlin!"
3. val result = with(message) {
4. println("value is $this")
5. println("with length ${this.length}")
6. }
7. }
1. fun main() {
2. val message = "Hello Kotlin!"
3. val result = with(message) {
4. "First character is ${this[0]}" +
5. " and last character is ${this[this.length - 1]}"
6. }
7.
8. println(result)
9. }
10.
11. /*
12. output : First character is H and last character is !
13. */
Fungsi with sendiri disarankan untuk mengakses apa yang menjadi anggotanya tanpa harus
menyediakan nilai kembalian.
apply
Berbeda dengan fungsi-fungsi sebelumnya, nilai yang dikembalikan dari fungsi apply adalah
nilai dari konteks objeknya dan objek konteksnya tersedia sebagai receiver (this). Baiknya
fungsi apply dapat melakukan inisialisasi atau konfigurasi dari receiver-nya. Perhatikan kode
berikut:
1. fun main() {
2. val builder = StringBuilder()
3. builder.append("Hello ")
4. builder.append("Kotlin!")
5.
6. println(builder.toString())
7. }
8.
9. /*
10. output : Hello Kotlin
11. */
1. fun main() {
2. val message = StringBuilder().apply {
3. append("Hello ")
4. append("Kotlin!")
5. }
6.
7. println(message.toString())
8. }
9.
10. /*
11. output : Hello Kotlin
12. */
let
Fungsi let menggunakan argumen (it) untuk mengakses konteks dari sebuah objek.
Penggunaan fungsi let akan banyak kita temukan pada objek yang bertipe non-null.
Contohnya seperti di bawah ini:
1. fun main() {
2. val message: String? = null
3. message?.let {
4. val length = it.length * 2
5. val text = "text length $length"
6. println(text)
7. }
8. }
1. fun main() {
2. val message: String? = null
3. val length = message?.length ?: 0 * 2
4. val text = "text length $length"
5. println(text)
6. }
Lalu bagaimana jika kita ingin menjalankan logika kode lain jika objeknya bernilai null? Di
sini kita akan memanfaatkan fungsi lainnya yaitu run dan elvis operator yang sudah kita
pelajari sebelumnya. Contohnya seperti berikut:
1. fun main() {
2. val message: String? = null
3. message?.let {
4. val length = it.length * 2
5. val text = "text length $length"
6. println(text)
7. } ?: run {
8. val text = "message is null"
9. println(text)
10. }
11. }
also
Fungsi also sama seperti fungsi apply, di mana nilai yang dikembalikan adalah nilai dari
konteks objek. Namun untuk konteks objeknya tersedia sebagai argumen (it). Fungsi also
baiknya digunakan ketika kita ingin menggunakan konteks dari objek sebagai argumen
tanpa harus mengubah nilainya.
1. fun main() {
2. val text = "Hello Kotlin"
3. val result = text.also {
4. println("value length -> ${it.length}")
5. }
6.
7. println("text -> $result")
8. }
9.
10. /*
11. output :
12. value length -> 12
13. text -> Hello Kotlin
14. */
Nah untuk melihat bagaimana beberapa fungsi yang sudah kita bahas di atas , cek tautan
ini.
Seperti yang sudah kita pelajari pada modul sebelumnya, saat mendeklarasikan sebuah
lambda dengan function type, kita bisa menggunakannya seperti berikut:
1. val sum: (Int, Int) -> Int = { valueA, valueB -> valueA + valueB }
Dengan Kotlin, kita bisa memisahkan lambda expression menjadi fungsi tersendiri dan
mereferensikannya langsung sebagai instance dari function type dengan cara seperti di
bawah ini:
Function References
Pada suatu kondisi, terkadang kita butuh mereferensikan sebuah fungsi. Sebagai contoh,
misal kita memiliki fungsi seperti berikut:
Fungsi di atas digunakan untuk memeriksa apakah suatu angka merupakan sebuah
bilangan genap. Dengan menggunakan operator :: kita bisa menggunakannya
sebagai instances dari function type. Sebagai contoh, penggunaan fungsi filter() yang
menjadi bagian dari kelas List berikut:
1. fun main() {
2. val numbers = 1.rangeTo(10)
3. val evenNumbers = numbers.filter(::isEvenNumber)
4.
5. println(evenNumbers)
6. }
7.
8. fun isEvenNumber(number: Int) = number % 2 == 0
9.
10. /*
11. output = [2, 4, 6, 8, 10]
12. */
Selain itu, kita juga bisa mereferensikan sebuah extensions function. Caranya dengan ikut
menyertakan objek yang menjadi receivernya sebelum operator :: seperti berikut:
1. fun main() {
2. val numbers = 1.rangeTo(10)
3. val evenNumbers = numbers.filter(Int::isEvenNumber)
4.
5. println(evenNumbers)
6. }
7.
8. fun Int.isEvenNumber() = this % 2 == 0
9.
10. /*
11. output = [2, 4, 6, 8, 10]
12. */
Property References
Selain digunakan untuk mereferensikan sebuah fungsi. Operator :: juga dapat digunakan
untuk mereferensikan sebuah properti. Dengan Operator, kita bisa mengakses apa yang
menjadi bagian dari properti tersebut seperti nama, fungsi setter getter, dll. Contohnya
seperti berikut:
Untuk mengatasinya, kita bisa memisahkannya lagi menjadi sebuah fungsi lokal (inner
function) dengan hak akses terbatas hanya untuk fungsi terluarnya. Ini bisa dilakukan
karena pada Kotlin kita bisa mendefinisikan sebuah fungsi di mana pun, bahkan di dalam
sebuah fungsi (function inside function).
Lalu, pada kondisi seperti apa kita bisa memanfaatkan inner function? Perhatikan deklarasi
fungsi berikut di bawah ini:
Tidak ada yang salah dengan semua fungsi di atas. Fungsi tersebut akan berjalan dengan
semestinya tanpa adanya eror selama kondisi yang berada di dalamnya tidak terpenuhi.
Namun jika kita perhatikan, terdapat pengulangan kode yang sama yaitu penggunaan if
expression untuk memeriksa apakah nilai dari argumen yang diberikan bernilai null.
Di sinilah kita bisa memanfaatkan inner function untuk membuat kode yang ditulis berulang
tersebut menjadi fungsi tersendiri.
Setelah menjadikannya sebagai sebuah fungsi tersendiri, kode yang ada di dalam
fungsi sum() tersebut lebih singkat dan tentunya lebih mudah dibaca dibandingkan
sebelumnya.
Pada modul ini kita akan mempelajari beberapa fungsi tingkat lanjut lainnya yang tentunya
bisa kita manfaatkan untuk mengelola data seperti yang disebutkan di atas.
Fold
Langsung saja kita mulai dengan fungsi fold, kita bisa dengan mudah melakukan
perhitungan setiap nilai yang berada di dalam sebuah collection tanpa harus melakukan
iterasi item tersebut satu-persatu menggunakan fungsi fold(). Untuk contoh penggunaannya
adalah sebagai berikut:
Fungsi fold() memerlukan 2 (dua) argumen yaitu nilai awal untuk perhitungan dan lambda
expression yang nilai kembaliannya digunakan untuk menentukan nilai awal selanjutnya.
Nah, di dalam lambda expression nya juga terdapat 2 (dua) argumen. Yaitu,
argumen current yang merepresentasikan nilai awal dan argumen item merepresentasikan
masing-masing item yang berada pada numbers.
Drop
Selanjutnya adalah fungsi drop(), fungsi yang bisa kita manfaatkan untuk memangkas item
yang berada di dalam sebuah objek collection berdasarkan jumlah yang kita tentukan.
Sebagai contoh, saat kita mempunyai sebuah collection seperti berikut:
Kemudian kita ingin memangkas 3 (tiga) item dari collection di atas. Dengan fungsi drop(),
kita bisa melakukannya seperti di bawah ini:
Seperti yang dijelaskan sebelumnya, nilai 3 yang menjadi argumen dari fungsi drop() di atas
adalah jumlah item yang akan dipangkas. Pemangkasan dimulai dari posisi atau indeks
pertama, lalu bagaimana jika kita ingin memangkas nilai dari indeks terakhir? Kita bisa
menggunakan fungsi dropLast(). Contohnya seperti berikut:
Take
Jika fungsi drop() digunakan untuk memangkas, fungsi take() bisa kita manfaatkan untuk
menyaring item yang berada di dalam sebuah objek collection. Pemanggilan
fungsi take() sama halnya seperti fungsi drop() di mana kita perlu menentukan jumlah item
yang akan disaring. Berikut contoh penggunaannya:
Kemudian jika ingin menentukan posisi yang lebih spesifik, kita bisa mendefinisikannya di
dalam sebuah collection, kemudian disematkan sebagai argumen. Misal seperti di bawah
berikut:
Kita harus berhati-hati dalam menentukan cakupan index untuk mendapatkan data. Karena
dapat menyebabkan terjadinya ArrayIndexOutOfBoundsException jika posisi yang
ditentukan tidak terdapat pada objek collection.
Distinct
Saat berurusan dengan item yang sama di dalam sebuah collection, untuk menyaring item
yang sama tersebut kita akan melakukan iterasi dan membandingkan setiap itemnya.
Namun dengan Kotlin kita tidak perlu melakukannya secara manual, karena Kotlin Collection
menyediakan fungsi untuk melakukannya dengan mudah yaitu fungsi distinct(). Sebagai
contoh:
Sama halnya seperti beberapa fungsi sebelumnya yang sudah dibahas, fungsi distinct() bisa
langsung dipanggil dari objek collection. Kita juga bisa menggunakannya
pada collection dengan tipe parameter seperti data class. Misal seperti berikut:
Menariknya, kita bisa juga menentukan proses penyaringan item yang sama seperti apa
yang akan dijalankan dengan menggunakan fungsi distinctBy(). Misal kita ingin menyaring
item yang memiliki panjang sama, kita bisa melakukannya seperti berikut:
Yang perlu diperhatikan, fungsi distinct() tidak dapat digunakan pada object Map Collection.
Chunked
Sama seperti fungsi split(), fungsi chunked() bisa kita gunakan untuk memecah nilai String
menjadi beberapa bagian kecil dalam bentuk Array. Namun untuk penerapannya sedikit
berbeda, di mana fungsi split() membutuhkan argumen berupa RegEx sebagai parameter
sedangkan chunked() membutuhkan nilai yang akan digunakan sebagai jumlah indeks untuk
pemisah. Contoh penggunaannya seperti berikut:
Selain itu, kita juga bisa menggunakannya untuk memodifikasi setiap nilai yang sudah
dipecah. Contoh, hasil dari nilai yang sudah dipecah ingin kita buat menjadi huruf kecil,
maka kita bisa menggunakan fungsi chunked() seperti berikut:
Argumen yang berada pada lambda expression di atas merepresentasikan setiap nilai yang
sudah dipecah.
Recursion merupakan sebuah teknik dasar dalam pemrograman yang bisa kita gunakan
untuk menyederhanakan pemecahan masalah yang umumnya diselesaikan dengan cara
yang kompleks. Di Kotlin, recursion disebut juga dengan recursive function.
Recursive function adalah sebuah mekanisme di mana sebuah fungsi digunakan dari dalam
fungsi itu sendiri. Ini memungkinkan sebuah fungsi dapat berjalan beberapa kali. Setiap
pemanggilannya bisa kita atur agar dapat mengembalikan nilai dan digunakan sebagai
argumen untuk pemanggilan fungsi berikutnya serta mengembalikan nilai akhir berupa
perhitungan nilai kembalian dari setiap pemanggilan fungsi tersebut.
Lalu penyelesaian seperti apa yang dapat kita lakukan dengan recursive? Perhatikan kode
di bawah ini:
Fungsi di atas adalah contoh bagaimana menghitung faktorial dari nilai yang kita tentukan.
Nah, tidak ada yang salah dengan kode tersebut dan dapat dijalankan serta mengembalikan
nilai sesuai dengan yang kita inginkan. Namun jika kita perhatikan, untuk menghitung nilai
akhir, kode di atas menggunakan for loop yang di setiap iterasinya terdapat proses
perhitungan nilai yang akan dikembalikan sebagai nilai akhir. Dengan recursive kita bisa
menentukan nilai akhir tersebut dengan cara yang lebih sederhana. Berikut contoh ketika
kode di atas ditulis dengan mekanisme recursive:
1. fun main() {
2. println("Factorial 9999 is: ${factorial(9999)}")
3. }
4.
5.
6. fun factorial(n: Int): Int {
7. return if (n == 1) {
8. n
9. } else {
10. n * factorial(n - 1)
11. }
12. }
Namun dengan kode di atas kita tidak bisa langsung menghindari penumpukan frame. Ini
karena secara default JVM tidak mendukung optimasi tail recursion. Untuk itu, Kotlin
menyediakan modifier agar kita bisa tetap menerapkannya, yaitu modifier tailrec.
Penggunaannya adalah seperti berikut:
Anatomi dari sebuah function terdiri dari 2 (adua) bagian utama, yaitu function
header dan function body:
o Function Header
Function header adalah bagian yang merupakan konstruksi dari sebuah
fungsi untuk menentukan perilakunya akan seperti apa. Di dalam function
header terdapat visibility modifier, kata kunci fun, nama, daftar parameter dan
nilai kembalian dari fungsi tersebut.
o Function Body
Function Body adalah bagian yang dalamnya kita akan menempatkan sebuah
logika kode baik itu sebuah expression atau statement.
Kotlin memiliki fitur seperti named dan default argument yang dapat menghindari kita
dari kesalahan saat menyematkan sebuah argumen saat menggunakan sebuah
fungsi.
Selain named dan default argument, Kotlin juga memiliki fitur varargs yang dapat
digunakan untuk menyederhanakan deklarasi beberapa parameter yang memiliki tipe
yang sama.
Kotlin mendukung 2 (dua) extension yang dapat digunakan, yaitu Extension
Functions dan Extension Properties. Jika extension functions digunakan untuk
menambahkan fungsi baru, extension properties tentunya digunakan untuk
menambahkan sebuah properti baru.
Lambda expression, atau biasa disebut dengan anonymous function atau function
literal adalah fitur yang cukup populer sampai sekarang dalam dunia functional
programming. Bisa disebut sebagai anonymous karena lambda tidak memiliki
sebuah nama seperti halnya sebuah fungsi pada umumnya. Karena merupakan
sebuah fungsi, lambda juga dapat memiliki daftar parameter, body dan return type.
Berikut adalah beberapa karakteristik dari Lambda pada kotlin.
o Fold
Fold, Anda bisa dengan mudah melakukan perhitungan setiap nilai yang
berada di dalam sebuah collection tanpa harus melakukan iterasi item
tersebut satu-persatu menggunakan fungsi fold().
o Drop
drop(), fungsi yang bisa kita manfaatkan untuk memangkas item yang berada
di dalam sebuah objek collection berdasarkan jumlah yang kita tentukan.
o Take
Fungsi take() bisa kita manfaatkan untuk menyaring item yang berada di
dalam sebuah objek collection.
o Slice
Bagaimana jika kita ingin menyaring item dari posisi tertentu? Untuk itu Anda
bisa memanfaatkan fungsi slice().
o Distinct
Saat berurusan dengan item yang sama di dalam sebuah collection, untuk
menyaring item yang sama tersebut kita akan melakukan iterasi dan
membandingkan setiap itemnya. Namun dengan Kotlin kita tidak perlu
melakukannya secara manual, karena Kotlin Collection menyediakan fungsi
untuk melakukannya dengan mudah yaitu fungsi distinct().
o Chunked
Sama seperti fungsi split(), fungsi chunked() bisa kita gunakan untuk
memecah nilai String menjadi beberapa bagian kecil dalam bentuk Array.
Recursion merupakan sebuah teknik dasar dalam pemrograman yang bisa kita
gunakan untuk menyederhanakan pemecahan masalah yang umumnya diselesaikan
dengan cara yang kompleks. Di Kotlin, recursion disebut juga dengan recursive
function.
Persiapan
Setiap modul pada akademi ini memiliki beberapa latihan yang harus Anda kerjakan. Latihan-latihan yang ada akan
meningkatkan pemahaman Anda terhadap sebuah materi, jadi sangat disarankan untuk Anda menyelesaikannya
segera setelah membaca 1 (satu) modul besar. Untuk mengerjakan latihan, Anda akan menggunakan sebuah
plugin pada IntelliJ IDEA, yaitu EduTools. Dengan EduTools, semua pekerjaan Anda akan diperiksa secara otomatis.
Ikuti tutorial berikut untuk menginstal plugin EduTools pada IntelliJ IDEA:
Setelah EduTools berhasil diinstal, buka proyek latihan dengan mengikuti tutorial berikut:
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda sudah mempelajari seperti apa functional programming pada Kotlin dan fitur - fitur apa
saja pada Kotlin yang terkait dengan functional programming.
Untuk menguji pemahaman Anda terhadap materi yang sudah dipelajari, Anda perlu
mengerjakan beberapa latihan sederhana. Buka proyek latihan dan kerjakan semua latihan
yang ada pada modul Functional Programming.
Object-Oriented Programming
Pada modul awal kita sudah mengetahui bahwa Kotlin memberi dukungan luas untuk
mengembangkan program berorientasi objek. Sebabnya, OOP masih menjadi salah satu
paradigma atau teknik pemrograman yang banyak digunakan dalam pengembangan
aplikasi. Dengan paradigma OOP kita dapat mudah memvisualisasikan kode karena OOP
sendiri mirip seperti skenario dalam kehidupan nyata.
Pada kode tersebut kita melakukan pembuatan variabel yang juga merupakan sebuah objek
dengan nama someString. Objek tersebut merupakan realisasi dari kelas String, maka
objek someString memiliki fungsi dan properti yang merupakan anggota dari kelas String.
Dari completion suggestion yang tersedia pada IntelliJ Idea, kita bisa melihat beberapa
fungsi yang dapat digunakan oleh objek someString. Kita bisa menggunakan
fungsi reverse() untuk membuat urutan huruf disusun secara terbalik,
fungsi toUpperCase() yang dapat membuat huruf menjadi kapital atau
fungsi toLowerCase() yang dapat membuat menjadi huruf kecil.
1. fun main() {
2. val someString = "Dicoding"
3. println(someString.reversed())
4. println(someString.toUpperCase())
5. println(someString.toLowerCase())
6. }
7.
8. /*
9. Output:
10. gnidociD
11. DICODING
12. dicoding
13. */
Kita juga dapat mengubah tipe data dengan mengakses fungsi yang tersedia dari sebuah
objek String.
1. fun main() {
2. val someString = "123"
3. val someInt = someString.toInt()
4. val someOtherString = "12.34"
5. val someDouble = someOtherString.toDouble()
6.
7. println(someInt is Int)
8. println(someDouble is Double)
9. }
10.
11. /*
12. Output:
13.
14. true
15. true
16. */
Hasil dari output kode menunjukan nilai true pada kedua variabel tersebut, yang artinya kita
telah berhasil mengubah suatu tipe data String ke tipe data lainnya dengan menggunakan
fungsi yang terdapat pada objek String.
Mungkin seperti itulah gambaran mengenai objek. Penting digarisbawahi bahwa objek
merupakan realisasi dari sebuah blueprint yang tentunya memiliki properti dan fungsi yang
sama dengan blueprint-nya. Salah satu kegunaan objek adalah untuk mengakses berbagai
properti dan fungsi pada kelas.
Class
Seperti yang telah dijelaskan dalam pembahasan objek, Class merupakan sebuah blueprint.
Di dalam kelas ini kita mendefinisikan sesuatu yang
merupakan attribute ataupun behaviour. Contohnya pada sebuah kelas Kendaraan,
atributnya berupa roda, warna, nomor kendaraan, merk, dll. Sedangkan untuk behaviour nya
yaitu maju, mundur, belok kanan, belok kiri, berhenti. Contoh lainnya pada sebuah
kelas Hewan atributnya berupa nama, berat, umur, termasuk mamalia atau bukan dll.
Sedangkan untuk behaviour-nya bisa makan, tidur, berjalan, dsb.
Animal
+ name: String
+ weight: Double
+ age: Int
+ isMammal: Boolean
- eat()
- sleep()
+ merupakan properti
- merupakan fungsi
Pada pembahasan selanjutnya kita akan mencoba membuat sebuah kelas berdasarkan
bentuk tabel di atas. Namun sebelum kita lanjut ke pembahasan berikutnya, mari kita
tekankan kembali beberapa hal yang sudah kita pelajari:
1. class Animal
Sangat mudah bukan? Sekarang kita tambahkan properti dan fungsi pada kelas tersebut.
Lalu untuk membuat sebuah objek dari suatu kelas, Anda bisa perhatikan struktur kode
berikut:
Sama seperti variabel, kita bisa gunakan val atau var, dilanjutkan dengan nama objek yang
akan anda buat. Tanda = menunjukan bahwa kita akan menginisialisasi suatu objek,
kemudian diikuti dengan nama kelas dan tanda kurung. Tanda kurung tersebut menunjukan
bahwa kita membuat sebuah objek baru. Di dalam tanda kurung kita dapat menambahkan
nilai properti sesuai yang dibutuhkan pada primary constructor kelasnya.
Maka jika kita coba membuat objek dari kelas yang sudah kita buat, kodenya akan terlihat
seperti ini:
Mari kita coba buat kode secara keseluruhan dengan ditambahkan fungsi cetak untuk
melihat nilai properti dalam objeknya.
Sama seperti variabel yang sudah kita pelajari pada modul Data Types, properti dapat
dideklarasikan sebagai nilai mutable dengan menggunakan var atau sebagai nilai read-
only dengan menggunakan val.
Property Accessor
Secara standar ketika properti pada kelas dibuat mutable, maka Kotlin akan menghasilkan
fungsi getter dan setter pada properti tersebut. Jika properti pada sebuah kelas dibuat read-
only, Kotlin hanya akan menghasilkan fungsi getter pada properti tersebut. Namun
sebenarnya Anda bisa membuat fungsi getter dan setter secara manual jika pada kasus
tertentu Anda perlu untuk override fungsi tersebut.
class Animal{
var name: String = "Dicoding Miaw"
}
fun main(){
val dicodingCat = Animal()
println("Nama: ${dicodingCat.name}" )
dicodingCat.name = "Goose"
println("Nama: ${dicodingCat.name}")
}
/*
output:
Nama: Dicoding Miaw
Nama: Goose
*/
Pada kode ${dicodingCat.name} sebenarnya terjadi proses pemanggilan fungsi getter pada
properti name. Namun kita tidak melakukan override pada fungsi getter sehingga fungsi
tersebut hanya mengembalikan nilai name saja. Begitu juga pada kode dicodingCat.name =
"Goose" pada kode tersebut terjadi pemanggilan fungsi setter pada properti name.
class Animal{
var name: String = "Dicoding Miaw"
get(){
println("Fungsi Getter terpanggil")
return field
}
set(value){
println("Fungsi Setter terpanggil")
field = value
}
}
fun main(){
val dicodingCat = Animal()
println("Nama: ${dicodingCat.name}" )
dicodingCat.name = "Goose"
println("Nama: ${dicodingCat.name}")
}
/*
output:
Fungsi Getter terpanggil
Nama: Dicoding Miaw
Fungsi Setter terpanggil
Fungsi Getter terpanggil
Nama: Goose
*/
Urutan pendefinisian fungsi get() dan set() tidaklah penting, kita dapat mendefinisikan
fungsi get() tanpa mendefinisikan fungsi set() dan juga sebaliknya. Yang terpenting
pendeklarasiannya dilakukan setelah mendeklarasikan properti tersebut. Pada fungsi get(),
kita perlu mengembalikan nilai sesuai tipe data dari propertinya atau kita dapat
mengembalikan nilai dari properti itu sendiri dengan menggunakan keyword field.
Sedangkan untuk fungsi set() kita memerlukan sebuah parameter. Ini merupakan sebuah
nilai baru yang nantinya akan dijadikan nilai properti. Pada kode di atas parameter tersebut
ditetapkan dengan nama value.
Property Delegation
Pengelolaan properti kelas baik itu memberikan atau merubah sebuah nilai dapat
didelegasikan kepada kelas lain. Dengan ini kita dapat meminimalisir boilerplate dalam
penulisan getter dan setter (jika properties menggunakan var) pada setiap kelas yang kita
buat. Sebagai contoh, kita memiliki tiga buah kelas yang di dalamnya memiliki satu properti
String. Jika kita ingin menerapkan getter dan setter pada setiap properti kelasnya, maka kita
perlu menuliskan getter dan setter tersebut pada seluruh kelas. Hal tersebut dapat
mengurangi efisiensi dalam menuliskan kode karena terlalu banyak kode yang harus kita
tulis secara berulang. Solusinya, kita perlu membuat sebuah kelas yang memang bertugas
untuk mengatur atau mengelola fungsi getter dan setter untuk sebuah properti kelas. Teknik
tersebut pada Kotlin dinamakan Delegate.
Sebelum mendelegasikan sebuah properti kita perlu membuat kelas delegasi terlebih
dahulu. Mari kita buat sebuah kelas delegasi.
1. import kotlin.reflect.KProperty
2.
3.
4. class DelegateName {
5. private var value: String = "Default"
6.
7. operator fun getValue(classRef: Any?, property: KProperty<*>) : String {
8. println("Fungsi ini sama seperti getter untuk properti ${property.name} pada
class $classRef")
9. return value
10. }
11.
12. operator fun setValue(classRef: Any?, property: KProperty<*>, newValue: String){
13. println("Fungsi ini sama seperti setter untuk properti ${property.name} pada
class $classRef")
14. println("Nilai ${property.name} dari: $value akan berubah menjadi $newValue")
15. value = newValue
16. }
17. }
1. class Animal {
2. var name: String by DelegateName()
3. }
1. class Animal {
2. var name: String by DelegateName()
3. }
4.
5. class Person {
6. var name: String by DelegateName()
7. }
8.
9. class Hero {
10. var name: String by DelegateName()
11. }
Mari kita membuat sebuah objek, ubah dan akses nilai propertinya pada setiap kelas,
kemudian jalankan. Maka hasilnya akan seperti pada kode berikut:
1. fun main() {
2. val animal = Animal()
3. animal.name = "Dicoding Miaw"
4. println("Nama Hewan: ${animal.name}")
5.
6. val person = Person()
7. person.name = "Dimas"
8. println("Nama Orang: ${person.name}")
9.
10. val hero = Hero()
11. hero.name = "Gatotkaca"
12. println("Nama Pahlawan: ${hero.name}")
13. }
14.
15. /*
16. output:
17. Fungsi ini sama seperti setter untuk properti name pada class Animal@17f052a3
18. Nilai name dari: Default akan berubah menjadi Dicoding Miaw
19. Fungsi ini sama seperti getter untuk properti name pada class Animal@17f052a3
20. Nama Hewan: Dicoding Miaw
21. Fungsi ini sama seperti setter untuk properti name pada class Person@2e0fa5d3
22. Nilai name dari: Default akan berubah menjadi Dimas
23. Fungsi ini sama seperti getter untuk properti name pada class Person@2e0fa5d3
24. Nama Orang: Dimas
25. Fungsi ini sama seperti setter untuk properti name pada class Hero@5010be6
26. Nilai name dari: Default akan berubah menjadi Gatotkaca
27. Fungsi ini sama seperti getter untuk properti name pada class Hero@5010be6
28. Nama Pahlawan: Gatotkaca
29. */
Pada contoh di atas, delegasi hanya dapat digunakan oleh properti yang memiliki tipe data
String. Namun kita juga dapat membuat sebuah delegasi kelas umum yang dapat digunakan
oleh seluruh tipe data dengan memanfaatkan tipe data Any.
1. class DelegateGenericClass {
2. private var value: Any = "Default"
3.
4. operator fun getValue(classRef: Any, property: KProperty<*>): Any {
5. println("Fungsi ini sama seperti getter untuk properti ${property.name} pada
class $classRef")
6. return value
7. }
8.
9. operator fun setValue(classRef: Any, property: KProperty<*>, newValue: Any) {
10. println("Nilai ${property.name} dari: $value akan berubah menjadi $newValue")
11. value = newValue
12. }
13. }
14.
15. class Animal {
16. var name: Any by DelegateGenericClass()
17. var weight: Any by DelegateGenericClass()
18. var age: Any by DelegateGenericClass()
19. }
Kemudian mari kita membuat sebuah objek dari kelas Animal, ubah dan akses nilai
propertinya kemudian jalankan. Maka hasilnya akan seperti pada kode berikut:
1. fun main(){
2. val animal = Animal()
3. animal.name = "Dicoding cat"
4. animal.weight = 6.2
5. animal.age = 1
6.
7. println("Nama: ${animal.name}")
8. println("Berat: ${animal.weight}")
9. println("Umur: ${animal.age} Tahun")
10. }
11.
12. /*
13. output:
14. Nilai name dari: Default akan berubah menjadi Dicoding cat
15. Nilai weight dari: Default akan berubah menjadi 6.2
16. Nilai age dari: Default akan berubah menjadi 1
17. Fungsi ini sama seperti getter untuk properti name pada class Animal@17f052a3
18. Nama: Dicoding cat
19. Fungsi ini sama seperti getter untuk properti weight pada class Animal@17f052a3
20. Berat: 6.2
21. Fungsi ini sama seperti getter untuk properti age pada class Animal@17f052a3
22. Umur: 1 Tahun
23. */
Perhatikan kode diatas, kita telah memberikan nilai pada setiap properti dengan tipe data
yang berbeda. Tetapi dengan DegelateGenericClass(), pengelolaan properti dapat
digunakan pada seluruh tipe data properti.
Extension Properties
Pada materi Kotlin Functional Programming kita sudah mengenal bahwa Kotlin dapat
meng-extends sebuah fungsi pada kelas tanpa harus mewarisi kelasnya. Hal ini dilakukan
dengan deklarasi khusus yang disebut dengan Extension.
Extension properties pada Kotlin sama halnya seperti melakukannya pada Extension
function. Kita dapat menambahkan sebuah properti tanpa harus membuat sebuah kelas
yang mewarisi kelas tersebut. Tetapi perlu diingat bahwa properti yang kita buat bukan
benar - benar berada pada kelas. Sebabnya, Extension properties dilakukan di luar kelas.
Dengan demikian, Extension properties hanya bisa didefinisikan dengan cara
menyediakan getter dan/atau setter secara eksplisit. Mari kita buat sebuah Extension
properties pada kelas Animal.
1. class Animal(var name: String, var weight: Double, var age: Int, var isMammal: Boolean)
2.
3. val Animal.getAnimalInfo : String
4. get() = "Nama: ${this.name}, Berat: ${this.weight}, Umur: ${this.age} Mamalia: $
{this.isMammal}"
1. fun main() {
2. val dicodingCat = Animal("Dicoding Miaw", 5.0, 2, true)
3. println(dicodingCat.getAnimalInfo)
4. }
Primary Constructor
Seperti namanya, jika kita akan membuat suatu objek dari sebuah kelas dan kelas tersebut
memiliki primary constructor di dalamnya, maka kita diharuskan mengirim nilai sesuai
properti yang dibutuhkan. Penulisan primary constructor mirip seperti parameter pada
fungsi. Properti cukup dituliskan pada header class diawali dengan var atau val. Perhatikan
kode berikut:
1. class Animal(val name: String, val weight: Double, val age: Int, val isMammal: Boolean)
Pada baris kode tersebut kita tidak hanya membuat sebuah kelas, namun sekaligus
menambahkan sebuah primary constructor pada kelas tersebut. Sekarang Mari kita coba
membuat objek dari kelas tersebut:
1. fun main(){
2. val dicodingCat = Animal("Dicoding Miaw", 4.2, 2, true)
3. println("Nama: ${dicodingCat.name}, Berat: ${dicodingCat.weight}, Umur: $
{dicodingCat.age}, mamalia: ${dicodingCat.isMammal}" )
4. }
5.
6. /*
7. output:
8. Nama: Dicoding Miaw, Berat: 4.2, Umur: 2, mamalia: true
9. */
Primary constructor juga dapat memiliki nilai default, dengan begitu jika kita tidak
menetapkan nilai untuk parameter tersebut maka properti tersebut akan memiliki
nilai default. Contohnya, kita bisa memberikan nilai default terhadap properti age. Sehingga
ketika pembuatan objek, pengiriman nilai age pada primary constructor bersifat opsional.
1. class Animal(var name: String, var weight: Double, var age: Int = 0, var isMammal:
Boolean = true)
Kita juga dapat secara eksplisit memilih properti yang ingin kita berikan nilai dengan
menambahkan nama properti dan tanda = sebelum mengisikan nilai properti.
Init block
Kotlin menyediakan blok init yang memungkinkan kita untuk menuliskan properti di
dalam body class ketika kita menggunakan primary constructor. Memang, memiliki kode
banyak di dalam body class bukanlah hal yang baik. Kotlin bertujuan agar kita dapat
menuliskan kode seminimal mungkin. Tapi blok init di sini memiliki beberapa fungsi selain
menginisialisasi properti kelas. satu fungsi lainnya adalah untuk membantu dalam
memvalidasi sebuah nilai properti sebelum diinisialisasi. Pada kelas Animal contohnya, kita
dapat melakukan verifikasi bahwa berat dan umur hewan tidak boleh bernilai kurang dari
nol.
1. fun main() {
2. val dicodingCat = Animal("Dicoding Miaw", 4.2, 2, true)
3. println("Nama: ${dicodingCat.name}, Berat: ${dicodingCat.weight}, Umur: $
{dicodingCat.age}, mamalia: ${dicodingCat.isMammal}")
4. }
5.
6. /*
7. output:
8. Nama: Dicoding Miaw, Berat: 4.2, Umur: 2, mamalia: true
9. */
Kata kunci this tersebut merujuk kepada suatu kelas dimana jika kita menggunakannya
diikuti dengan nama properti maka kita menunjuk pada properti yang terdapat pada kelas
tersebut. Dengan begitu, tidak akan ada ambiguitas walaupun kita menggunakan penamaan
yang sama antara properti dan parameter primary constructor.
Secondary Constructor
Secondary constructor digunakan ketika kita ingin menginisialisasi sebuah kelas dengan
cara yang lain. Anda dapat membuat lebih dari satu secondary constructor. Sebagai contoh,
kita bisa menambahkan secondary constructor pada kelas Animal:
Default Constructor
Kotlin secara otomatis membuat sebuah default constructor pada kelas jika kita tidak
membuat sebuah konstruktor secara manual. Perhatikan kode berikut:
1. class Animal{
2. val name: String = "Dicoding Miaw"
3. val weight: Double = 4.2
4. val age: Int = 2
5. val isMammal: Boolean = true
6. }
7.
8. fun main(){
9. val dicodingCat = Animal()
10. println("Nama: ${dicodingCat.name}, Berat: ${dicodingCat.weight}, Umur: $
{dicodingCat.age}, mamalia: ${dicodingCat.isMammal}" )
11. }
12.
13. /*
14. output:
15. Nama: Dicoding Miaw, Berat: 4.2, Umur: 2, mamalia: true
16. */
Public: Hak akses yang cakupannya paling luas. Anggota yang diberi modifier ini
dapat diakses dari manapun.
Private: Hak akses yang cakupannya paling terbatas. Anggota yang menerapkannya
hanya dapat diakses pada scope yang sama.
Protected: Hak akses yang cakupannya terbatas pada hirarki kelas. Anggota hanya
dapat diakses pada kelas turunannya atau kelas itu sendiri.
Internal: Hak akses yang cakupannya terbatas pada satu modul. Anggota yang
menggunakannya tidak dapat diakses diluar dari modulnya tersebut.
Semua modifier tersebut bisa digunakan untuk kelas, objek, konstruktor, fungsi, beserta
properti yang ada di dalamnya. Kecuali modifier protected yang hanya bisa digunakan untuk
anggota di dalam sebuah kelas dan interface. Protected tidak bisa digunakan
pada package member seperti kelas, objek, dan yang lainnya. Setelah mengetahui
pentingnya hak akses, selanjutnya kita akan membahas bagaimana kita menentukan hak
akses public, private, protected dan internal pada Kotlin.
Public
Berbeda dengan bahasa pemrograman umumnya, default modifier pada Kotlin
adalah public. Ketika sebuah anggota memiliki hak akses public maka anggota tersebut
dapat diakses dari luar kelasnya melalui sebuah objek kelas tersebut.
Private
Ketika suatu anggota memiliki hak akses private, maka anggota tersebut tidak dapat diakses
dari luar scope-nya. Untuk menggunakan modifier private kita perlu
menambahkan keyword private seperti contoh berikut:
Mari kita coba ubah hak akses pada seluruh properti kelas Animal menjadi private.
1. class Animal(private val name: String, private val weight: Double, private val age: Int,
private val isMammal: Boolean)
2.
3. fun main() {
4. val dicodingCat = Animal("Dicoding Miaw", 2.5, 2)
5. println("Nama: ${dicodingCat.name}, Berat: ${dicodingCat.weight}, Umur: $
{dicodingCat.age}, mamalia: ${dicodingCat.isMammal}")
6. }
Dengan menggunakan hak akses private, maka kita tidak diizinkan untuk mengakses
properti pada kelas Animal tersebut dari luar kelasnya. Anda akan berjumpa
dengan eror Cannot access '[PROPERTY]': it is private in 'Animal'. Satu satunya cara untuk
mengakses properti private dari sebuah kelas adalah dengan menambahkan
fungsi getter dan setter secara manual. Fungsi getter dan setter sebenarnya dihasilkan
secara otomatis oleh Kotlin ketika properti tersebut memiliki hak akses public tetapi tidak
untuk private. Untuk penulisan getter dan setter pada hak akses private sama seperti fungsi
pada umumnya:
1. class Animal(private var name: String, private val weight: Double, private val age: Int,
private val isMammal: Boolean = true) {
2.
3. fun getName() : String {
4. return name
5. }
6.
7. fun setName(newName: String) {
8. name = newName
9. }
10.
11. }
12.
13. fun main() {
14. val dicodingCat = Animal("Dicoding Miaw", 2.5, 2)
15. println(dicodingCat.getName())
16. dicodingCat.setName("Gooose")
17. println(dicodingCat.getName())
18. }
19.
20. /*
21. output:
22. Dicoding Miaw
23. Gooose
24. */
Pada kode di atas, terlihat bahwa kita berhasil mengubah nilai properti name dari nilai awal
yang kita inisialisasikan pada konstruktor. Ia menjadi nilai baru yang kita tentukan dengan
menggunakan fungsi setName().
Protected
Hak akses protected mirip seperti private, namun pembatasannya lebih luas dalam sebuah
hirarki kelas. Hak akses protected digunakan ketika kita menginginkan sebuah anggota dari
induk kelas dapat diakses hanya oleh kelas yang merupakan turunannya. Perhatikan kode
di bawah ini untuk contoh penggunaan hak akses protected.
1. fun main() {
2. val cat = Cat("Dicoding Miaw", 2.0)
3. println("Nama Kucing: ${cat.name}")
4. println("Berat Kucing: ${cat.weight}") //Cannot access 'weight': it is protected in
'Cat'
5. }
Internal
Internal merupakan hak akses baru yang diperkenalkan pada Kotlin. Hak akses ini
membatasi suatu anggota untuk dapat diakses hanya pada satu modul. Berikut ini contoh
penggunaan hak akses internal:
Pada contoh di atas, kelas Animal telah ditetapkan sebagai kelas internal, maka kelas
tersebut hanya dapat diakses dari modul yang sama. Hak akses ini sangat berguna ketika
kita mengembangkan sebuah aplikasi yang memiliki beberapa modul di dalamnya.
Inheritances
Dalam gambaran dunia nyata, banyak objek yang berbeda tetapi punya kesamaan atau
kemiripan tertentu. Contohnya Kucing dan Kambing memiliki banyak kesamaan karena
objek tersebut merupakan hewan. Kucing merupakan hewan mamalia, begitu juga dengan
kambing. Mungkin yang membedakan objek tersebut adalah cara mereka mencari makanan
dan jenis makanan yang dimakan. Sama halnya pada OOP, beberapa objek yang berbeda
bisa saja memiliki kesamaan dalam hal tertentu. Di situlah
konsep inheritance atau pewarisan harus diterapkan. Pewarisan dapat mencegah kita
melakukan perulangan kode. Untuk lebih memahaminya lihatlah contoh bagan pada sebuah
kelas berikut:
1. class Cat(val name: String, val furColor: String, val weight: Double, val age: Integer,
val numberOfFeet: Integer, val isCarnivore: Boolean) {
2. fun eat(){
3. println("$name sedang makan!")
4. }
5.
6. fun sleep() {
7. println("$name sedang tidur!")
8. }
9.
10. fun playWithHuman() {
11. println("$name bermain dengan Manusia!")
12. }
13. }
Tidak ada masalah dengan kode tersebut, tetapi ketika kita akan membuat kelas dari
diagram lainnya contohnya Fish maka kita harus menuliskan ulang properti
seperti name, weight, age dan properti atau fungsi yang sama lainnya. Hal ini dapat
mengurangi efisiensi dalam menuliskan kode.
Dengan teknik inheritance, kita bisa mengelompokkan properti dan fungsi yang sama.
Caranya , buat sebuah kelas baru yang nantinya akan diturunkan sifatnya pada sebuah
kelas:
Animal
+ name: String
+ weight: Double
+ age: Integer
+ isCarnivore: Boolean
- eat()
- sleep()
Cat Fish Snake
1. open class Animal(val name: String, val weight: Double, val age: Int, val isCarnivore:
Boolean){
2.
3. open fun eat(){
4. println("$name sedang makan!")
5. }
6.
7. open fun sleep(){
8. println("$name sedang tidur!")
9. }
10. }
1. class Cat(pName: String, pWeight: Double, pAge: Int, pIsCarnivore: Boolean, val
furColor: String, val numberOfFeet: Int)
2. : Animal(pName, pWeight, pAge, pIsCarnivore) {
3.
4. fun playWithHuman() {
5. println("$name bermain bersama Manusia !")
6. }
7.
8. override fun eat(){
9. println("$name sedang memakan ikan !")
10. }
11.
12. override fun sleep() {
13. println("$name sedang tidur di bantal !")
14. }
15. }
Dengan begitu, selain fungsi yang terdapat di dalamnya, kelas Cat juga dapat mengakses
seluruh fungsi dan properti yang terdapat kelas Animal.
1. fun main(){
2. val dicodingCat = Cat("Dicoding Miaw", 3.2, 2, true, "Brown", 4)
3.
4. dicodingCat.playWithHuman()
5. dicodingCat.eat()
6. dicodingCat.sleep()
7. }
8.
9. /*
10. output:
11. Dicoding Miaw bermain bersama Manusia !
12. Dicoding Miaw sedang memakan ikan !
13. Dicoding Miaw sedang tidur di bantal !
14. */
Overloading
Pada Kotlin menggunakan dua atau lebih fungsi dengan nama yang sama disebut
dengan overloading. Overloading dapat dilakukan selama fungsi itu memiliki parameter yang
berbeda. Berikut merupakan contoh overloading fungsi eat() pada sebuah kelas Animal.
Pada kelas Animal terdapat beberapa fungsi dengan penamaan yang sama, tetapi tidak
menyebabkan eror. Sebabnya, fungsi tersebut memiliki parameter yang berbeda sehingga
tidak akan terjadi ambiguitas dalam penggunaan fungsi tersebut. Mari kita coba buat sebuah
objek dari kelas tersebut dan mengakses fungsinya satu persatu.
1. fun main() {
2. val dicodingCat = Animal("Dicoding Miaw")
3.
4. dicodingCat.eat()
5. dicodingCat.eat("Ikan Tuna")
6. dicodingCat.eat("Ikan Tuna", 450.0)
7. }
Overloading pada fungsi merupakan sebuah fitur yang sangat powerful. Untuk dapat lebih
memahami betapa pentingnya overloading, mari kita buat sebuah kelas Calculator yang di
dalamnya memiliki fungsi matematika dengan menerapkan overloading pada sebuah fungsi.
1. class Calculator {
2. fun add(value1: Int, value2: Int) = value1 + value2
3. fun add(value1: Int, value2: Int, value3: Int) = value1 + value2 + value3
4. fun add(value1: Double, value2: Double) = value1 + value2
5. fun add(value1: Float, value2: Float) = value1 + value2
6.
7. fun min(value1: Int, value2: Int) = if (value1 < value2) value1 else value2
8. fun min(value1: Double, value2: Double) = if (value1 < value2) value1 else value2
9. }
1. fun main() {
2. val calc = Calculator()
3.
4. println(calc.add(2, 4))
5. println(calc.add(2.5, 2.2))
6. println(calc.add(6f, 7f))
7. println(calc.add(1, 2, 3))
8.
9. println(calc.min(9, 2))
10. println(calc.min(17.2, 18.3))
11. }
12.
13. /*
14. output
15. 6
16. 4.7
17. 13.0
18. 6
19. 2
20. 17.2
21. */
1. abstract class Animal(var name: String, var weight: Double, var age: Int, var
isCarnivore: Boolean){
2.
3. fun eat(){
4. println("$name sedang makan !")
5. }
6.
7. fun sleep(){
8. println("$name sedang tidur !")
9. }
10. }
1. fun main(){
2. val animal = Animal("dicoding animal", 2.6, 1, true)
3. }
Ketika kita mencoba membuat objek dari kelas Animal, akan terdapat eror berikut:
Cara pembuatan sebuah interface mirip dengan membuat kelas. Pada umumnya penamaan
sebuah interface dituliskan dengan awalan huruf I kapital. Hal ini tidak diharuskan secara
sintaks tapi ini merupakan penerapan terbaik dalam penamaan sebuah interface. Tujuannya
agar dapat mudah membedakannya dengan kelas. Berikut merupakan contoh pembuatan
sebuah interface:
1. interface IFly {
2. fun fly()
3. }
Kita akan mendapati eror ketika selesai menuliskan kode tersebut. Pesan eror tersebut
mengatakan “class Bird is not abstract and does not implement abstract member”.
Maksud dari eror tersebut adalah kita harus mengimplementasi sebuah abstract
member yang pada kasus ini adalah sebuah fungsi abstract yang terdapat pada IFly. Maka
untuk menghilangkan eror tersebut, kita harus melakukan override fungsi yang terdapat
pada IFly.
Untuk menambahkan sebuah properti pada interface, kita cukup menuliskannya seperti
pada kelas namun tanpa melakukan inisialisasi nilai:
1. interface IFly {
2. fun fly()
3. val numberOfWings: Int
4. }
Importing Package
Untuk mengimpor suatu package kelas, fungsi atau variabel, kita cukup
menuliskan keyword import kemudian dilanjutkan dengan alamat spesifiknya seperti:
1. import packagename.ClassName
2. import packagename.functionName
3. import packagename.propertyName
1. import kotlin.random.Random
1. import kotlin.random.Random
2.
3. val someInt = Random(0).nextInt(1, 10)
1. import kotlin.math.PI
2. import kotlin.math.cos
3. import kotlin.math.sqrt
4.
5. fun main(){
6. println(PI)
7. println(cos(120.0))
8. println(sqrt(9.0))
9. }
10.
11. /*
12. Output:
13. 3.141592653589793
14. 0.8141809705265618
15. 3.0
16. */
Kita juga dapat mengganti nama sebuah kelas, fungsi atau variabel yang kita import dengan
menggunakan alias yang direpresentasikan dengan kata kunci as.
1. import kotlin.math.PI
2. import kotlin.math.cos as cosinus
3. import kotlin.math.sqrt as akar
4.
5. fun main(){
6. println(PI)
7. println(cosinus(120.0))
8. println(akar(9.0))
9. }
10.
11. /*
12. Output:
13. 3.141592653589793
14. 0.8141809705265618
15. 3.0
16. */
Kita dapat mengimpor seluruh kelas, fungsi dan variabel yang berada pada
suatu package dengan menggunakan tanda * pada akhir package tersebut.
1. import kotlin.math.*
2.
3. fun main(){
4. println(PI)
5. println(cos(120.0))
6. println(sqrt(9.0))
7. }
8.
9.
10. /*
11. Output:
12. 3.141592653589793
13. 0.8141809705265618
14. 3.0
15. */
Membuat Package Baru
Seperti yang diketahui sebelumnya, package merupakan pembungkus dari kelas (package-
level class), fungsi (package-level function) atau variabel (package-level variable) berfungsi
serupa. Kita juga sudah mengetahui cara mengimpor suatu kelas, fungsi atau variabel yang
terdapat pada sebuah package. Namun kita belum tahu bagaimana package tersebut
dibuat. Jadi pada pembahasan kali ini kita akan mencoba bagaimana untuk membuat
sebuah package pada Kotlin.
Dengan menekan tombol “OK” maka kita berhasil membuat sebuah package folder pada
proyek aplikasi kita. Maka struktur proyek akan menjadi seperti ini:
1. package com.dicoding.oop.utils
2. fun sayHello() = println("Hello From package com.dicoding.oop.utils")
1. fun main(){
2. com.dicoding.oop.utils.sayHello()
3. }
4.
5. /*
6. Output:
7. Hello From package com.dicoding.oop.utils
8. */
Untuk dapat memahami tentang package lebih lanjut, mari kita buat beberapa fungsi dan
variabel pada package tersebut. Buka kembali berkas MyMath.kt, tambahkan beberapa
fungsi dan variabel yang akan kita gunakan nantinya.
1. package com.dicoding.oop.utils
2.
3. fun sayHello() = println("Hello From package utils")
4.
5. const val PI = 3.1415926535 // package level variable
6.
7. fun pow(number: Double, power: Double) : Double {
8. var result = 1.0
9. var counter = power
10. while (counter > 0) {
11. result *= number
12. counter--
13. }
14. return result
15. }
16.
17. fun factorial(number: Double) : Double {
18. var result = 1.0
19. var counter = 1.0
20. while (counter <= number) {
21. result *= counter
22. counter++
23. }
24.
25. return result
26. }
27.
28. fun areaOfCircle(radius: Double) : Double {
29. return PI * 2 * radius
30. }
1. package com.dicoding.oop
2.
3. import com.dicoding.oop.utils.PI
4. import com.dicoding.oop.utils.factorial
5. import com.dicoding.oop.utils.pow
6. import com.dicoding.oop.utils.sayHello
7.
8. fun main() {
9. sayHello()
10. println(factorial(4.0))
11. println(pow(3.0, 2.0))
12. println(PI)
13. }
14.
15. /*
16. output:
17. Hello From package com.dicoding.oop.utils
18. 24.0
19. 9.0
20. 3.1415926535
21. */
Pada awal kode terlihat saat kita menggunakan suatu fungsi atau variabel yang berada pada
package tertentu, kita perlu melakukan impor pada setiap fungsi atau variabelnya. Tetapi
jika kita menggunakan seluruh fungsi atau variabel dalam package tertentu kita bisa
menggunakan tanda bintang (*) untuk melakukan impor pada seluruh fungsi dan variabel
di package tersebut. Perhatikan kode berikut:
1. package com.dicoding.oop
2.
3. import com.dicoding.oop.utils.*
4.
5. fun main() {
6. sayHello()
7. println(factorial(4.0))
8. println(pow(3.0, 2.0))
9. println(PI)
10. println(areaOfCircle(13.0))
11. }
12.
13. /*
14. output:
15. Hello From package com.dicoding.oop.utils
16. 24.0
17. 9.0
18. 3.1415926535
19. 81.681408991
20. */
ArithmeticException
NumberFormatException
NullPointerException
ArithmeticException merupakan exception yang terjadi karena kita membagi suatu
bilangan dengan nilai nol. Berikut merupakan contoh kode yang dapat
membangkitkan ArithmeticException.
1. fun main() {
2. val someValue = 6
3. println(someValue / 0)
4. }
5.
6. /*
7. output:
8. Exception in thread "main" java.lang.ArithmeticException: / by zero
9. */
1. fun main() {
2. val someStringValue = "18.0"
3. println(someStringValue.toInt())
4. }
5.
6. /*
7. output:
8. Exception in thread "main" java.lang.NumberFormatException: For input string: "18.0"
9. */
1. fun main() {
2. val someNullValue: String? = null
3. val someMustNotNullValue: String = someNullValue!!
4. println(someMustNotNullValue)
5. }
6.
7. /*
8. output:
9. Exception in thread "main" kotlin.NullPointerException at MainKt.main(Main.kt:3)
10. */
Exception handling dapat diterapkan dengan beberapa cara. Di antaranya adalah dengan
menggunakan try-catch, try-catch-finally, dan multiple catch. Mari kita pelajari ketiga
cara tersebut.
try-catch
Salah satu cara untuk menangani suatu exception adalah menggunakan try-catch. Kode
yang dapat membangkitkan suatu exception disimpan dalam blok try, dan
jika exception tersebut terjadi, maka blok catch akan terpanggil. Berikut cara penulisan try-
catch pada Kotlin:
1. try {
2. // Block try, menyimpan kode yang membangkitkan exception
3. } catch (e: SomeException) {
4. // Block catch akan terpanggil ketika exception bangkit.
5. }
Dengan menuliskan kode dalam blok try, kode kita menjadi terproteksi dari exception. Jika
terjadi exception maka program tidak akan terhenti atau crash, namun akan dilempar
menuju blok catch. Di sana kita dapat menuliskan sebuah kode alternatif untuk
menampilkan pesan eror atau yang lainnya.
1. fun main() {
2. val someNullValue: String? = null
3. lateinit var someMustNotNullValue: String
4.
5. try {
6. someMustNotNullValue = someNullValue!!
7. println(someMustNotNullValue)
8. } catch (e: Exception) {
9. someMustNotNullValue = "Nilai String Null"
10. println(someMustNotNullValue)
11. }
12. }
13.
14. /*
15. output:
16. Nilai String Null
17. */
try-catch-finally
Selain terdapat blok try dan catch, ada juga blok finally. Hanya saja blok ini bersifat
opsional. finally akan dieksekusi setelah program keluar dari blok try ataupun catch.
Bahkan finally juga tereksekusi ketika terjadi exception yang tidak terduga. Exception tidak
terduga terjadi ketika kita
menggunakan NullPointerException pada catch namun exception yang terjadi
adalah NumberFormatException.
Sebagai contoh, mari kita ubah kode yang sebelumnya dengan menerapkan finally:
1. fun main() {
2. val someNullValue: String? = null
3. lateinit var someMustNotNullValue: String
4.
5. try {
6. someMustNotNullValue = someNullValue!!
7. } catch (e: Exception) {
8. someMustNotNullValue = "Nilai String Null"
9. } finally {
10. println(someMustNotNullValue)
11. }
12. }
13.
14. /*
15. output:
16. Nilai String Null
17. */
Multiple Catch
Dari kode yang kita coba sebelumnya, kita menggunakan exception untuk menangani
semua tipe exception yang terjadi. Baik itu ketika
terjadi NullPointerException atau NumberFormatException. Sebenarnya pada catch kita
dapat secara spesifik memilih tipe exception apa yang ingin ditangani. Multiple
catch memungkinkan untuk penanganan exception dapat ditangani lebih dari satu
tipe exception. Hal ini sangat berguna ketika kita ingin menangani setiap
tipe exception dengan perlakuan yang berbeda. Berikut contoh struktur kode dengan
menerapkan multiple catch:
1. try{
2. // Block try, menyimpan kode yang membangkitkan exception
3. } catch (e: NullPointerException) {
4. // Block catch akan terpanggil ketika terjadi NullPointerException.
5. } catch (e: NumberFormatException) {
6. // Block catch akan terpanggil ketika terjadi NumberFormatException.
7. } catch (e: Exception) {
8. // Block catch akan terpanggil ketika terjadi Exception selain keduanya.
9. }
10. finally {
11. // Block finally akan terpanggil setelah keluar dari block try atau catch
12. }
Dari struktur kode di atas, kita dapat melihat terdapat 3 (tiga) blok catch. Block catch yang
pertama menggunakan parameter NullPointerException, sehingga jika
terjadi NullPointerException maka blok catch tersebut akan dieksekusi. Yang kedua
block catch dengan parameter NumberFormatException, sehingga jika
terjadi NumberFormatException maka blok tersebut yang akan dieksekusi. Dan yang
terakhir blok catch dengan parameter Exception, blok ini akan menangani
seluruh exception yang terjadi kecuali untuk dua exception yang telah ditentukan pada blok
sebelumnya.
Mari kita coba terapkan contoh kode yang sebelumnya kita buat dengan
menggunakan multiple catch.
1. import kotlin.NumberFormatException
2.
3. fun main() {
4. val someStringValue: String? = null
5. var someIntValue: Int = 0
6.
7. try {
8. someIntValue = someStringValue!!.toInt()
9. } catch (e: NullPointerException) {
10. someIntValue = 0
11. } catch (e: NumberFormatException) {
12. someIntValue = -1
13. } finally {
14. when(someIntValue){
15. 0 -> println("Catch block NullPointerException terpanggil !")
16. -1 -> println("Catch block NumberFormatException terpanggil !")
17. else -> println(someIntValue)
18. }
19. }
20. }
21.
22. /*
23. output:
24. Catch block NullPointerException terpanggil!
25. */
Namun jika kedua exception tersebut tidak terjadi, dalam arti nilai someStringValue berhasil
diubah dalam bentuk Integer, maka output yang dihasilkan adalah nilai dari Integernya
tersebut. Contohnya, saat nilai someStringValue diinisialisasi dengan nilai “12.” Berikut ini
hasilnya. :
12
Rangkuman dari Kotlin Object-Oriented Programming
Okay, kita sudah selesai belajar dan mengetahui mengenai paradigma Object-Oriented
pada Kotlin. Untuk me-refresh apa yang sudah kita pelajari, mari kita merangkum beberapa
hal penting mengenai paradigma ini.
o Primary Constructor
Seperti namanya, jika kita akan membuat suatu objek dari sebuah
kelas dan kelas tersebut memiliki constructor di dalamnya, maka
konstruktor tersebut adalah primary constructor dan diharuskan untuk
mengirim nilai sesuai properti yang dibutuhkan.
Primary Constructor juga dapat memiliki nilai default, dengan begitu
jika kita tidak menetapkan nilai untuk parameter tersebut maka
properti tersebut akan memiliki nilai default.
Kotlin menyediakan blok init yang memungkinkan kita untuk
menuliskan properti di dalam body class ketika kita
menggunakan primary constructor.
o Secondary Constructor
o ArithmeticException
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda sudah mempelajari apa dan bagaimana OOP pada Kotlin. Untuk menguji pemahaman
Anda terhadap materi yang sudah dipelajari, Anda perlu mengerjakan beberapa latihan
sederhana. Buka proyek latihan dan kerjakan semua latihan yang ada pada modul Object-
Oriented.
Pada modul sebelumnya kita sudah belajar tentang Kotlin sebagai bahasa pemrograman
yang bisa diklasifikasikan ke dalam OOP beserta konsep-konsep yang terdapat didalamnya.
Kali ini kita akan mempelajari tentang Generics, yaitu sebuah konsep yang memungkinkan
suatu kelas atau interface menjadi tipe parameter yang dapat digunakan untuk berbagai
macam tipe data.
Secara umum generic merupakan konsep yang digunakan untuk menentukan tipe data yang
akan kita gunakan. Pendeklarasiannya ditandai dengan tipe parameter. Kita juga bisa
mengganti tipe parameter menjadi tipe yang lebih spesifik dengan menentukan instance dari
tipe tersebut.
Sebelum kita mempelajari bagaimana cara kita mendeklarasikan sebuah kelas generic, ada
baiknya jika kita melihat contoh bagaimana generic bekerja pada variabel dengan tipe List.
Kita perlu menentukan tipe dari nilai yang bisa disimpan di dalam variabel List tersebut:
Berbeda jika kita ingin membuat variabel list tanpa langsung menambahkan nilainya. Maka
list tersebut tidak memiliki nilai yang bisa dijadikan acuan untuk kompiler menentukan tipe
parameter. Alhasil, kita wajib menentukannya secara eksplisit seperti berikut:
Selain itu, kita juga bisa mendeklarasikan lebih dari satu tipe parameter untuk sebuah kelas.
Contohnya adalah kelas Map yang memiliki dua tipe parameter yang digunakan
sebagai key dan value. Kita bisa menentukannya dengan argumen tertentu, misalnya
seperti berikut:
1. interface List<T>{
2. operator fun get(index: Int) : T
3. }
Pada kode di atas, tipe parameter T bisa kita gunakan sebagai tipe reguler yang
mengembalikan tipe dari sebuah fungsi.
Selanjutnya, jika kita mempunyai sebuah kelas yang mewarisi kelas atau interface generic,
maka kita perlu menentukan tipe argumen sebagai tipe dasar dari parameter generic kelas
tersebut. Parameternya bisa berupa tipe yang spesifik atau lainnya. Contohnya seperti
berikut:
Pada kelas LongList di atas, Long digunakan sebagai tipe argumen untuk List, sehingga
fungsi yang berada di dalamnya akan menggunakan Long sebagai tipe dasarnya. Berbeda
dengan kelas ArrayList, di mana tipe argumen untuk kelas List menggunakan T. Dengan
demikian ketika kita menggunakan kelas ArrayList, kita perlu menentukan tipe argumen dari
kelas tersebut saat diinisialisasi.
1. fun main() {
2. val longArrayList = ArrayList<Long>()
3. val firstLong = longArrayList.get(0)
4. }
5.
6. class ArrayList<T> : List<T> {
7. override fun get(index: Int): T {
8. return this[index]
9. }
10. }
11.
12. interface List<T> {
13. operator fun get(index: Int): T
14. }
Yang perlu diperhatikan dari kelas ArrayList di atas adalah deklarasi dari tipe parameter T.
Tipe parameter tersebut berbeda dengan yang ada pada kelas List, karena T adalah milik
kelas ArrayList itu sendiri. Plus sebenarnya Anda pun bisa menggunakan selain T misalnya
seperti berikut:
Fungsi generic memiliki tipe parameternya sendiri. Tipe argumen dari parameternya
ditentukan ketika fungsi tersebut dipanggil. Cara mendeklarasikannya sedikit berbeda
dengan kelas generic, Tipe parameter yang berada di dalam angle bracket harus
ditempatkan sebelum nama dari fungsi yang kita tentukan. Sebagai contoh:
Contoh penerapan fungsi generic bisa kita lihat pada deklarasi fungsi slice yang
merupakan extensions function dari kelas List berikut:
1. fun main() {
2. val numbers = (1..100).toList()
3. print(numbers.slice<Int>(1..10))
4. }
5.
6. /*
7. output : [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
8. */
Seperti yang telah disebutkan sebelumnya, jika semua nilai yang berada di dalamnya
memiliki tipe yang sama, kita bisa menyederhanakan. Caranya, hapus tipe parameter
tersebut.
1. fun main() {
2. val numbers = (1..100).toList()
3. print(numbers.slice(1..10))
4. }
5.
6. /*
7. output : [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
8. */
Constraint Type Parameter
Dalam penerapan generic, kita bisa membatasi tipe apa saja yang dapat digunakan sebagai
parameter. Untuk menentukkan batasan tersebut, bisa dengan menambahkan tanda titik
dua (:) setelah tipe parameter yang kemudian diikuti oleh tipe yang akan dijadikan batasan.
Contohnya seperti berikut:
Pada kode di atas kita telah menentukan Number sebagai batasan tipe argumen. Dengan
begitu, kita hanya bisa memasukkan tipe argumen Number pada kelas ListNumber. Dan
ketika kita memasukkan selain Number, maka akan terjadi eror seperti berikut:
1. fun main() {
2. val numbers = ListNumber<Long>()
3. val numbers2 = ListNumber<Int>()
4. val numbers3 = ListNumber<String>() // error : Type argument is not within its
bounds
5. }
6.
7. class ListNumber<T : Number> : List<T>{
8. override fun get(index: Int): T {
9. /* .. */
10. }
11. }
1. fun main() {
2. val numbers = listOf(1, 2, 3, 4, 5)
3. numbers.sumNumber()
4. val names = listOf("dicoding", "academy")
5. names.sumNumber() // error : inferred type String is not a subtype of Number
6. }
7.
8. fun <T : Number> List<T>.sumNumber() : T {
9. /* .. */
10. }
Variance
Sebelumnya kita telah mempelajari bagaimana generic bekerja, bagaimana penerapannya,
serta bagaimana kita bisa menentukan batasan tipe argumen yang bisa ditentukan terhadap
tipe parameter. Selanjutnya kita akan belajar salah satu konsep dari generic yaitu variance.
Apa itu variance? Variance adalah konsep yang menggambarkan bagaimana sebuah tipe
yang memiliki subtipe yang sama dan tipe argumen yang berbeda saling berkaitan satu
sama lain. Variance dibutuhkan ketika kita ingin membuat kelas atau fungsi generic dengan
batasan yang tidak akan mengganggu dalam penggunaannya. Sebagai contoh, mari kita
buat beberapa kelas seperti berikut:
1. fun main() {
2. val car = Car(200)
3. val motorCycle = MotorCycle(100)
4. var vehicle: Vehicle = car
5. vehicle = motorCycle
6. }
Selanjutnya mari kita masukkan salah satu kelas yang merupakan subtipe dari
kelas Vehicle di atas kedalam generic list:
1. fun main() {
2. val carList = listOf(Car(100) , Car(120))
3. val vehicleList = carList
4. }
Nah, itu adalah contoh sederhana bagaimana variance bekerja. Lalu bagaimana cara
membuat kelas generic yang memiliki variance? Caranya sama seperti ketika kita membuat
generic kelas pada umumnya. Namun untuk tipe parameternya kita membutuhkan kata
kunci out untuk covariant atau kunci in untuk contravariant.
Covariant
Contoh deklarasi generic dengan covariant bisa kita lihat saat kelas List pada Kotlin
dideklarasikan seperti berikut:
Ketika kita menandai sebuah tipe parameter dengan kata kunci out maka nilai dari tipe
parameter tersebut hanya bisa diproduksi seperti menjadikanya sebagai return type. Serta
tidak dapat dikonsumsi seperti menjadikannya sebagai tipe argumen untuk setiap fungsi di
dalam kelas tersebut.
Contravariant
Berbanding terbalik dengan saat kita menandainya dengan kata kunci out, bagaimana saat
kita menandainya dengan dengan kata kunci in ? Nilai dari tipe parameter tersebut bisa
dikonsumsi dengan menjadikannya sebagai tipe argumen untuk setiap fungsi yang ada di
dalam kelas tersebut dan tidak untuk diproduksi. Contoh dari deklarasinya bisa kita lihat
pada kelas Comparable pada Kotlin berikut:
1. interface List<T>{
2. operator fun get(index: Int) : T
3. }
1. interface List<T>{
2. operator fun get(index: Int) : T
3. }
1. fun <T> run(): T {
2. /*...*/
3. }
1. fun main() {
2. val longArrayList = ArrayList<Long>()
3. displayArray<Long>(longArrayList)
4. }
Constraint Type Parameter adalah teknik untuk membatasi cakupan tipe data apa
saja yang dapat di sediakan oleh Generic Class maupun Generic Function.
Pembatasan ini dilakukan dengan membubuhkan tanda colon (:) setelah tipe
parameter yang kemudian diikuti oleh tipe data yang akan dijadikan batasan.
Batasan dalam Constraint Type Parameter adalah child class dari tipe data
batasan tersebut. Jadi, semisal ditentukan batasannya adalah tipe List, maka hanya
berlaku pada child class dari class tersebut.
Variance, adalah konsep pada Generics yang menggambarkan bagaimana sebuah
tipe yang memiliki subtipe yang sama dan tipe argumen yang berbeda saling
berkaitan satu sama lain. Ada beberapa jenis dari variance yaitu:
o Covariant
Ditandai dengan keyword ‘out’ sebelum deklarasi dari tipe parameter. Nilai
dari tipe parameter tersebut hanya bisa diproduksi seperti menjadikanya
sebagai return type dan tidak dapat dikonsumsi seperti menjadikannya
sebagai tipe argumen untuk setiap fungsi di dalam kelas tersebut.
o Contravariant
Ditandai dengan keyword ‘in’ sebelum deklarasi dari tipe parameter. Nilai dari
tipe parameter tersebut hanya bisa dikonsumsi seperti menjadikannya
sebagai tipe argumen untuk setiap fungsi dan tidak untuk dikonsumsi
sebagai return type dari sebuah function di dalam kelas tersebut
Persiapan
Setiap modul pada akademi ini memiliki beberapa latihan yang harus Anda kerjakan. Latihan-latihan yang ada akan
meningkatkan pemahaman Anda terhadap sebuah materi, jadi sangat disarankan untuk Anda menyelesaikannya
segera setelah membaca 1 (satu) modul besar. Untuk mengerjakan latihan, Anda akan menggunakan sebuah
plugin pada IntelliJ IDEA, yaitu EduTools. Dengan EduTools, semua pekerjaan Anda akan diperiksa secara otomatis.
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda sudah mempelajari bagaimana mendeklarasikan kelas dan fungsi generic pada Kotlin.
Serta mempelajari sebuah konsep yang bernama variance.
Untuk menguji pemahaman Anda terhadap materi yang sudah dipelajari, Anda perlu
mengerjakan beberapa latihan sederhana. Buka proyek latihan dan kerjakan semua latihan
yang ada pada modul Generics.
Memasuki modul terakhir ini, kita akan mempelajari dasar concurrency pada Kotlin hingga
alasan mengapa developer wajib mencoba Kotlin Coroutines. Concurrency merupakan
sebuah topik yang cukup dalam. Jika dibahas secara menyeluruh mungkin tidak akan cukup
di akademi ini. Maka dari itu, modul ini adalah pengantarnya. Diharapkan setelah memahami
materi ini, pembaca dapat mengetahui gambaran apa itu concurrency dan perbedaannya pada
Kotlin dibandingkan bahasa pemrograman lainnya.
Arus lalu lintas bisa menjadi ilustrasi yang tepat untuk menggambarkan proses concurrency.
Lalu lintas paralel di jalan yang berbeda hanya akan menimbulkan interaksi dan potensi
masalah yang kecil antar kendaraan. Berbeda dengan lalu lintas padat yang biasanya kita
jumpai pada persimpangan. Pastinya interaksi dan potensi masalah antar kendaraan akan
lebih besar dan membutuhkan koordinasi yang lebih. Begitu pula dalam sebuah sistem
aplikasi. Proses paralel yang tidak saling berinteraksi hanya akan menyebabkan masalah
concurrency yang sederhana. Berbeda dengan proses yang saling berinteraksi bahkan berbagi
sumber daya. Masalahnya tentu lebih kompleks.
Penting untuk memperhatikan beberapa aspek saat berurusan dengan concurrency pada
semua sistem. Aspek terpenting adalah mampu mendeteksi dan merespon peristiwa eksternal
yang terjadi dalam urutan acak. Selain itu juga pastikan bahwa peristiwa tersebut dapat
ditanggapi dalam interval waktu minimum yang diwajibkan.
Faktor eksternal sering jadi pendorong dibutuhkannya concurrency. Dalam kehidupan sehari-
hari, banyak hal yang terjadi secara bersamaan dan harus ditangani secara real-time oleh
sistem. Oleh karena itu sistem harus "reactive" alias dituntut untuk menanggapi proses yang
dihasilkan secara eksternal. Proses tersebut dapat terjadi pada waktu dan urutan yang tak bisa
ditentukan. Membangun sistem konvensional untuk mengatasi tugas tersebut, tentunya sangat
rumit.
Di dunia komputer, concurrency umumnya terkait dengan banyaknya core atau inti dari
prosesor (CPU). Pada dasarnya, sebuah komputer memiliki mekanisme sequential atau
berurutan untuk menjalankan tugas. Prosesor akan menjalankan satu perintah pada satu
waktu. Dengan concurrency, kita bisa memanfaatkan kinerja prosesor dengan lebih optimal.
Concurrency memungkinkan sebuah sistem untuk bisa dikendalikan dengan mudah. Sebagai
contoh, suatu fungsi bisa dijalankan, dihentikan, ataupun dipengaruhi oleh fungsi lain yang
jalan secara bersamaan.
Concurrency vs Parallelism
Jika membahas concurrency, tentunya terkait dengan parallelism. Mungkin ada yang
bingung mengenai perbedaan antara keduanya. Bagaimanapun, concurrency dan
parallelism mempunyai arti yang mirip, yaitu 2 (dua) atau lebih proses yang berjalan pada
satu waktu. Namun penting diketahui bahwa concurrency bukanlah parallelism.
Concurrency terjadi apabila terdapat 2 (dua) atau lebih proses yang tumpang tindih dalam
satu waktu. Ini bisa terjadi jika ada 2 (dua) atau lebih thread yang sedang aktif. Dan jika
thread tersebut dijalankan oleh komputer yang hanya memiliki 1 (satu) core, semua thread
tidak akan dijalankan secara paralel. Concurrency memungkinkan sebuah komputer yang
hanya memiliki 1 (satu) core tampak seakan mengerjakan banyak tugas sekaligus. Padahal
sebenarnya tugas-tugas tersebut dilakukan secara bergantian.
Sedangkan parallelism terjadi ketika 2 (dua) proses dijalankan pada titik waktu yang sama
persis. Parallelism bisa dilakukan jika terdapat 2 (dua) atau lebih thread dan komputer juga
memiliki 2 (dua) core atau lebih. Sehingga setiap core dapat menjalankan perintah dari
masing-masing thread secara bersamaan.
Perhatikan beberapa ilustrasi berikut agar Anda lebih memahami perbedaan antara
concurrency dan parallelism.
Bayangkan Anda di warung kopi dan melihat antrian seperti yang digambarkan pada
ilustrasi di atas. Pelanggan dengan masing-masing pesanannya pasti senang jika sang
barista bisa melayani dengan tepat dan cepat. Jika pada warung kopi tersebut hanya
terdapat seorang barista, otomatis sang barista harus melakukan cara untuk melayani
semua pelanggan sekaligus. Pada situasi seperti inilah concurrency dibutuhkan.
Karena hanya terdapat seorang barista, maka barista tersebut akan memproses lebih dari
satu pesanan secara bersamaan. Hal ini sangat mungkin terjadi karena pembuatan kopi
membutuhkan beberapa langkah, dan masing-masing langkah memakan waktu tersendiri.
Misalnya menyiapkan air panas, menakar kopi, menyiapkan mesin espresso, dll. Barista
akan membagi langkah-langkah tersebut sehingga seolah-olah ia bisa mengerjakan
pesanan secara bersamaan.
Berbeda jika sang barista punya teman kerja untuk berbagi tugas. Pada situasi ini
parallelism bisa dilakukan. Barista 1 hanya akan melayani beberapa pelanggan, dan sisanya
akan dilayani oleh barista 2.
Karena kedua barista tersebut telah berbagi tugas, maka mereka akan bertindak secara
paralel sehubungan dengan tugas yang lebih besar dalam melayani pelanggan.
Bagaimanapun, selama jumlah barista belum sama dengan jumlah pelanggan, concurrency
masih tetap diperlukan pada masing-masing barista tersebut. Artinya, parallelism dapat
menimbulkan concurrency, tetapi concurrency bisa terjadi tanpa parallelism.
Process, Thread, I/O-Bound
Saat kita mulai menjalankan sebuah aplikasi, sebenarnya sistem operasi akan membuat
sebuah proses, kemudian melampirkan sebuah thread padanya, dan setelah itu mulai
menjalankan thread tersebut. Semua aktivitas tersebut akan bergantung pada perangkat
yang digunakan, terutama perangkat perangkat input dan output (I/O).
Untuk bisa memahami dan juga menerapkan concurrency dengan benar, developer perlu
mempelajari beberapa konsep dasar seperti Process, Thread dan I/O bound. Ketiga
konsep tersebut saling berhubungan. Oleh karena itu, kita akan mencoba mengulas semua
konsep tersebut.
Process
Sebuah proses (process) merupakan bagian dari aplikasi yang sedang dijalankan. Setiap
kali aplikasi dijalankan, maka saat itu juga proses dijalankan. Tergantung pada sistem
operasi yang digunakan, suatu proses dapat terdiri dari beberapa thread yang menjalankan
instruksi secara bersamaan.
Proses sering dianggap identik dengan program atau aplikasi. Namun, sebenarnya sebuah
aplikasi adalah serangkaian proses yang saling bekerja sama. Untuk memfasilitasi
komunikasi antar proses, sebagian besar sistem operasi mendukung sumber daya Inter
Process Communication (IPC), seperti pipes dan soket. Biasanya sistem operasi modern
sudah mendukung IPC. IPC digunakan tidak hanya untuk komunikasi antar proses pada
sistem yang sama, melainkan juga untuk proses pada sistem yang berbeda.
Kita pasti mengenal dengan sebuah konsep yang bernama multitasking atau melakukan
banyak tugas secara bersamaan. Saat multitasking, sebenarnya sistem operasi hanya
beralih di antara berbagai proses dengan sangat cepat untuk memberikan kesan bahwa
proses ini sedang dijalankan secara bersamaan. Sebaliknya, multiprocessing adalah metode
untuk menggunakan lebih dari satu CPU dalam menjalankan tugas.
Thread
Thread biasa dikenal dengan proses yang ringan. Membuat thread baru membutuhkan lebih
sedikit sumber daya daripada membuat proses baru. Sebuah thread mencakup serangkaian
instruksi untuk dijalankan oleh prosesor. Sehingga suatu proses akan memiliki setidaknya
satu thread, yang dibuat untuk mengeksekusi fungsi utama dari sebuah aplikasi. Secara
umum, thread tersebut disebut dengan main thread, dan life cycle dari sebuah proses akan
terikat padanya.
Lebih dari satu thread dapat diimplementasikan dalam proses yang sama, dan dapat
dieksekusi secara bersamaan. Perbedaan utama antara proses dan thread adalah bahwa
thread biasanya merupakan komponen dari suatu proses. Selain itu, thread biasanya
memungkinkan untuk berbagi sumber daya seperti memori dan data. Dimana 2 (dua) hal
tersebut jarang dilakukan oleh sebuah proses.
Setiap thread dapat mengakses dan memodifikasi sumber daya yang terkandung dalam
proses yang dilampirkan, tetapi juga memiliki penyimpanan lokal sendiri, yang biasa disebut
dengan thread-local storage.
Hanya satu dari instruksi dalam sebuah thread yang dapat dijalankan pada waktu tertentu.
Jadi, jika sebuah thread terblokir, instruksi lain dalam thread yang sama tidak akan
dijalankan sampai pemblokiran tersebut berakhir. Namun demikian, banyak thread dapat
dibuat untuk proses yang sama, dan mereka dapat berkomunikasi satu sama lain. Jadi
diharapkan aplikasi tidak akan pernah memblokir thread yang dapat mempengaruhi
pengalaman pengguna secara negatif.
Selain main thread, terdapat juga thread lain yang dikenal dengan UI thread. Thread ini
berfungsi untuk memperbarui user interface (antarmuka) dan juga merespon aksi yang
diberikan pada aplikasi. Jika thread ini diblokir, maka semua tugasnya akan terhalangi. Oleh
karena itu, jangan sampai kita memblokir UI thread agar aplikasi tetap berjalan dengan
semestinya.
I/O-Bound
Bottlenecks atau kemacetan adalah suatu hal yang penting untuk ditangani demi
mengoptimalkan kinerja aplikasi. Bayangkan saja ketika Anda menggunakan sebuah
aplikasi dan terjadi bottleneck di dalamnya, kesal sendiri kan?
Perangkat input dan output biasanya sering mempengaruhi sebuah aplikasi
mengalami bottlenecks. Sebagai contoh, memori yang terbatas, kecepatan prosesor, dsb.
Lalu bagaimanakah cara untuk mengatasinya?
I/O-bound merupakan sebuah algoritma yang bergantung pada perangkat input atau output.
Waktu untuk mengeksekusi sebuah I/O-bound tergantung pada kecepatan perangkat yang
digunakan. Sebagai contoh, suatu algoritma untuk membaca dan menulis sebuah dokumen.
Ini adalah operasi I/O yang akan tergantung pada kecepatan di mana berkas tersebut dapat
diakses. Berkas yang disimpan pada SSD akan lebih cepat diakses dibandingkan berkas
yang disimpan pada HDD.
Algoritma I/O-bound akan selalu menunggu sesuatu yang lain. Penantian terus-menerus ini
memungkinkan perangkat yang hanya memiliki satu core untuk menggunakan prosesor
demi tugas-tugas bermanfaat lainnya sambil menunggu. Jadi algoritma concurrent yang
terikat dengan I/O akan melakukan hal yang sama, terlepas dari eksekusi yang terjadi -
apakah paralel atau dalam satu core?
Seperti yang telah disebutkan sebelumnya, sangat penting untuk tidak memblokir UI thread
dalam sebuah aplikasi. Oleh karena itu, saran kami terapkanlah concurrency jika aplikasi
yang Anda kembangkan punya ketergantungan dengan perangkat I/O.
Permasalahan Pada Concurrency
Membuat concurrent program ? Banyak developer sering mengeluhkan itu sulit. Selain
kodenya bisa dibilang sulit dituliskan, terdapat juga beberapa tantangan yang perlu dihadapi.
Ada beberapa permasalahan yang wajib bisa kita tangani pada concurrency,
yaitu deadlocks, livelocks, starvation dan juga race conditions.
Deadlocks
Untuk menjamin bahwa kode concurrent bisa disinkronkan dengan benar, apa salah satu hal
yang tidak bisa dihindari ? Kita perlu menunda/memblokir eksekusi saat sebuah perintah
diselesaikan di thread yang berbeda. Hal tersebut disebabkan oleh deadlocks, yaitu sebuah
kondisi di mana dua proses atau lebih saling menunggu proses yang lain untuk
melepaskan resource yang sedang digunakan.
Deadlocks mengakibatkan proses-proses yang sedang berjalan, tak kunjung selesai. Kasus ini
merupakan umum terjadi ketika banyak proses yang saling berbagi sumber daya. Dalam
situasi yang bisa dibilang cukup kompleks itu, tak jarang salah satu proses harus terpaksa
diberhentikan.
Kasus deadlocks sebenarnya bisa kita amati pada kehidupan nyata. Sebagai contoh,
perhatikan ilustrasi berikut ini:
Ilustrasi di atas menggambarkan antrian dua mobil yang sama-sama akan menyeberangi
sebuah jembatan. Jembatan tersebut bisa kita analogikan sebagai sebuah resource yang
dibutuhkan oleh kedua antrian kendaraan. Karena keduanya saling membutuhkan jembatan
dalam waktu yang sama, maka yang terjadi adalah saling menunggu. Alhasil, tak ada
kemajuan pada proses antrian tersebut. Mau tidak mau, salah satu kendaraan harus ada yang
mengalah atau dikalahkan.
Dalam programming situasi seperti itu juga umum terjadi. Misal ada 2 (dua) proses yang
sama-sama menunggu proses satunya selesai, baru proses tersebut bisa selesai. Sama seperti
ilustrasi mobil. Karena keduanya menunggu satu sama lain, tidak ada dari kedua proses
tersebut yang akan selesai. Tugas selanjutnya pun tidak akan pernah bisa dijalankan.
Dalam sebuah sistem jaringan kerap jadi masalah pemicu deadlocks. Terlebih jika problem
tersebut dibarengi race condition. Alhasil, apa yang terjadi? Muncullah kondisi-kondisi tak
terduga yang membuat deadlocks jauh lebih rumit dibandingkan dengan masalah antrian
proses.
Livelocks
Sama halnya dengan deadlocks, livelocks juga merupakan kondisi di mana sebuah proses
tidak bisa melanjutkan tugasnya. Namun yang membedakannya adalah bahwa selama
livelocks terjadi, keadaan dari aplikasi tetap bisa berubah. Walaupun perubahan keadaan
tersebut menyebabkan proses berjalan dengan tidak semestinya.
Pernahkah suatu ketika Anda berjalan di trotoar dan secara tidak langsung berhadapan
dengan orang lain yang berjalan lurus tepat ke arah Anda? Ya, situasi ini pasti sering terjadi
dan kadang bisa menjadi awkward moment. Secara spontan pasti Anda dan orang tersebut
akan berusaha menghindari satu sama lain dengan bergerak ke satu sisi. Tak jarang, Anda
bergerak ke kiri sementara orang di depan Anda bergerak ke kanan. Dan karena kalian berdua
berjalan di arah yang berlawanan, tentunya malah menghalangi jalan masing-masing. Apakah
yang akan terjadi selanjutnya? Bisa jadi, Anda akan bergerak ke kanan, dan pada saat yang
sama orang itu bergerak ke kiri. Sekali lagi kalian tidak dapat melanjutkan perjalanan.
Kejadian tersebut bisa terjadi berulang kali sampai waktu yang tidak diketahui.
Livelock bisa terjadi ketika beberapa proses bisa bereaksi saat mengalami deadlocks. Proses
tersebut mencoba keluar dari situasi deadlock, namun waktu yang tidak tepat,
menghalanginya.
Starvation
Starvation merupakan sebuah kondisi yang biasanya terjadi setelah deadlock. Kondisi
deadlock sering kali menyebabkan sebuah proses kekurangan sumber daya sehingga
mengalami starvation atau kelaparan. Pada kondisi seperti ini, thread tak dapat akses
reguler ke sumber daya bersama dan membuat proses terhenti.
Selain deadlock, hal lain yang bisa mengakibatkan starvation adalah ketika terjadi kesalahan
sistem. Akibatnya, ada ketimpangan dalam pembagian sumber daya. Sebagai contoh, ketika
satu proses bisa mendapat sumber daya, namun tidak dengan proses lain. Biasanya,
kesalahan seperti itu disebabkan oleh algoritma penjadwalan (scheduling algorithm) yang
kurang tepat atau bisa juga karena resource leak atau kebocoran sumber daya.
Salah satu contoh kesalahan algoritma penjadwalan bisa dilihat ketika sebuah sistem
multitasking dirancang dengan tidak baik. Apa akibatnya? Dua tugas pertama selalu beralilh,
sementara yang ketiga tidak pernah berjalan. Tugas ketiga itulah yang bisa dikatakan
menderita starvation.
Salah satu solusi untuk starvation adalah dengan menerapkan algoritma penjadwalan
dengan antrian prioritas (process priority) dan juga menerapkan teknik aging atau
penuaan. Aging merupakan sebuah teknik yang secara bertahap meningkatkan prioritas
sebuah proses yang menunggu dalam sistem pada waktu yang cukup lama.
Race Conditions
Pada pembahasan sebelumnya, kita sudah menyinggung istilah race conditions. Kondisi
seperti apakah itu? Race condition merupakan masalah umum pada concurrency, yaitu
kondisi di mana terdapat banyak thread yang mengakses data yang dibagikan bersama dan
mencoba mengubahnya secara bersamaan. Ini bisa terjadi ketika kode concurrent yang
dituliskan untuk berjalan secara sekuensial. Artinya, sebuah perintah akan selalu dijalankan
dalam urutan tertentu.
Ketika race condition terjadi, maka sistem bisa saja melakukan proses yang tidak
semestinya. Pada saat itu bug akan muncul. Race condition dikenal sebagai masalah yang
sulit untuk direproduksi dan di-debug, karena hasil akhirnya tidak deterministik dan
tergantung pada waktu relatif dari thread yang menghalangi. Masalah yang muncul pada
production bisa jadi tidak ditemukan pada saat debug. Oleh karena itu, lebih baik
menghindari race condition dengan cara berhati-hati saat merencanakan sebuah sistem. Ini
lebih baik daripada harus berusaha memperbaikinya setelah itu. Lebih repot.
Contoh sederhana dari race condition bisa kita lihat pada aplikasi perbelanjaan online.
Katakanlah Anda menemukan barang yang ingin Anda beli di sebuah toko online. Pada
halaman deskripsi, tampil informasi bahwa stok barang hanya sisa 1 (satu). Lalu Anda
langsung mengambil keputusan dengan menekan tombol beli. Pada saat yang sama, ada
pengguna lain yang ternyata lebih dahulu membelinya. Kondisi seperti inilah yang
mengakibatkan sebuah state atau keadaan, berubah tanpa Anda sadari. Jika sistem tidak
dirancang sedemikian rupa, maka masalah tak terduga, bisa mengemuka.
Coroutines merupakan fitur unggulan pada Kotlin yang diperkenalkan pada Kotlin versi 1.1.
Saat ini coroutines sudah mencapai versi 1.4.2. Coroutines adalah cara baru untuk menulis
kode asynchronous dan non-blocking. Seperti tagline-nya “Don’t block, Keep moving”
yang dikenalkan pada saat rilis Kotlin versi 1.3. [9]
Kenapa coroutines sering disebut sebagai thread yang ringan? Coroutines juga
mendefinisikan eksekusi dari sekumpulan instruksi untuk dieksekusi oleh prosesor. Selain
itu, coroutines juga memiliki life cycle yang sama dengan thread.
Walaupun coroutines dan threads bekerja dengan cara sama, coroutines jauh lebih ringan
dan efisien. Jutaan coroutines dapat berjalan pada beberapa threads. Jika dibandingkan
dengan threads, coroutines tidak hanya mudah diterapkan, melainkan juga jauh
lebih powerful. Kelebihan tersebut terutama berlaku pada lingkungan mobile, di mana
setiap milliseconds kenaikan kinerja sangat diperhitungkan. Selain itu, perbedaan lainnya
adalah coroutines dikelola oleh pengguna, sedangkan threads dikelola oleh sistem operasi.
Coroutines dijalankan di dalam threads. Satu thread dapat memiliki banyak coroutine di
dalamnya. Namun, seperti yang sudah disebutkan, hanya satu instruksi yang dapat
dijalankan dalam thread pada waktu tertentu. Artinya, jika Anda memiliki sepuluh coroutines
di thread yang sama, hanya satu dari sepuluh coroutines tersebut yang akan berjalan pada
titik waktu tertentu.
Walaupun coroutines dijalankan dalam sebuah thread, perlu diperhatikan bahwa keduanya
tak saling terikat. Menariknya, kita juga bisa menjalankan bagian dari coroutine dalam
sebuah thread, menundanya, kemudian melanjutkannya pada thread yang berbeda.
Coroutines cukup fleksibel untuk kita menentukan- Apakah suatu thread akan menjalankan
sebuah coroutine atau justru menahannya?
Memulai Coroutines
Untuk lebih memahami tentang coroutines, mari kita mulai mencobanya langkah demi
langkah. Hal pertama yang wajib Anda tahu adalah bahwa coroutines bukanlah bagian dari
bahasa Kotlin [10]. Coroutines hanyalah library lain yang disediakan oleh JetBrains. Untuk
itu, agar bisa menggunakannya Anda perlu menambahkan dependensi berikut
pada build.gradle.kts:
1. dependencies {
2. implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
3. }
Dengan menambahkan dependensi di atas, kini Anda sudah siap untuk mencoba
menggunakan fitur-fitur coroutines dalam membuat program concurrency. Yuk kita mulai
dari kode yang sangat sederhana berikut ini:
1. import kotlinx.coroutines.*
2.
3. fun main() = runBlocking{
4. launch {
5. delay(1000L)
6. println("Coroutines!")
7. }
8. println("Hello,")
9. delay(2000L)
10. }
Selama proses penundaan tersebut, main thread akan terus berjalan sehingga
fungsi println("Hello,") akan langsung dijalankan. Setelah 1 detik, baru
fungsi println("Coroutines!") akan dijalankan. Sedangkan kode delay(2000L) digunakan
untuk menunda selama 2 detik sebelum JVM berakhir.
Ini baru sekedar permulaan loh. Masih banyak lagi fungsi-fungsi menarik lain pada
coroutines yang dapat mempermudah kita dalam membuat program concurrency. Anda bisa
memanfaatkan kumpulan library yang dapat ditemukan pada repositori kotlinx.coroutines.
JetBrains selaku tim pengembang juga berkomitmen untuk mempertahankan backward
compatibility untuk tiap perubahan yang akan dirilis. Itulah mengapa coroutines sudah
diperkenalkan pada Kotlin versi 1.1.
Tersedia juga panduan resmi untuk langkah-langkah penerapan coroutines. Ikuti saja tautan
ini.
Coroutines Builder
Pada modul sebelumnya kita sudah mencoba menggunakan
fungsi runBlocking dan launch untuk memulai sebuah coroutines. Kedua fungsi tersebut
merupakan coroutines builder, yaitu sebuah fungsi yang mengambil suspending lambda dan
membuat coroutine untuk menjalankannya.
Kotlin menyediakan beberapa coroutine builder yang bisa disesuaikan dengan berbagai
macam skenario, seperti:
launch
Seperti yang sudah kita coba sebelumnya, fungsi ini digunakan untuk memulai
sebuah coroutines yang tidak akan mengembalikan sebuah hasil. launch akan
menghasilkan Job yang bisa kita gunakan untuk membatalkan eksekusi.
runBlocking
Fungsi ini dibuat untuk menjembatani blocking code menjadi kode yang dapat
ditangguhkan. runBlocking akan memblokir sebuah thread yang sedang berjalan
hingga eksekusi coroutine selesai. Seperti contoh sebelumnya, kita bisa
menggunakannya pada fungsi main() dan bisa juga untuk penerapan unit test.
async
Kebalikan dari launch, fungsi ini digunakan untuk memulai sebuah coroutine yang
akan mengembalikan sebuah hasil. Ketika menggunakannya, Anda harus berhati-
hati karena ia akan menangkap setiap exception yang terjadi di dalam coroutine.
Jadi async akan mengembalikan Deferred yang berisi hasil atau exception. Ketika
yang dikembalikan adalah exception, maka Anda harus siap untuk menanganinya.
Sekarang giliran kita untuk mencoba contoh penerapan coroutine dengan async. Bayangkan
jika kita memiliki 2 (dua) suspending function seperti berikut:
Anggap saja bahwa delay pada kedua fungsi tersebut adalah waktu yang dibutuhkan untuk
melakukan operasi sebelum hasilnya didapatkan. Selanjutnya kita ingin memanfaatkan
keduanya, misalnya untuk menghitung keuntungan seperti berikut:
1. import kotlinx.coroutines.*
2.
3. fun main() = runBlocking {
4. val capital = getCapital()
5. val income = getIncome()
6. println("Your profit is ${income - capital}")
7. }
1. import kotlinx.coroutines.*
2. import kotlin.system.measureTimeMillis
3.
4. fun main() = runBlocking {
5. val timeOne = measureTimeMillis {
6. val capital = getCapital()
7. val income = getIncome()
8. println("Your profit is ${income - capital}")
9. }
10.
11. val timeTwo = measureTimeMillis {
12. val capital = async { getCapital() }
13. val income = async { getIncome() }
14. println("Your profit is ${income.await() - capital.await()}")
15. }
16.
17. println("Completed in $timeOne ms vs $timeTwo ms")
18.
19. }
Kita bisa lihat bahwa kode yang dijalankan di dalam async bisa selesai hampir 2 kali lebih
cepat dibandingkan tanpa async!
Job dan Deferred
Secara umum, fungsi asynchronous pada coroutines terbagi menjadi 2 (dua) jenis, yaitu
fungsi yang mengembalikan hasil dan sebaliknya, fungsi yang tidak mengembalikan hasil.
Fungsi yang mengembalikan hasil biasanya digunakan jika kita menginginkan sebuah data
ketika fungsi tersebut selesai dijalankan. Sebagai contoh, fungsi untuk mengambil informasi
dari web service yang menghasilkan respon berupa JSON atau yang lainnya. Sedangkan
fungsi yang tidak mengembalikan hasil biasanya digunakan untuk mengirimkan analitik,
menuliskan log, atau tugas sejenis lainnya.
Sebagai developer, tentunya kita menginginkan tetap bisa mengakses fungsi yang sudah
dijalankan. Misalnya, ketika kita ingin membatalkan tugasnya atau memberikan instruksi
tambahan ketika fungsi tersebut telah mencapai kondisi tertentu. Untuk bisa melakukannya,
Anda perlu memahami tentang Job dan Deferred pada coroutines.
Job
Job adalah sebuah hasil dari perintah asynchronous yang dijalankan. Objek dari job akan
merepresentasikan coroutine yang sebenarnya. Sebuah job akan memiliki 3 (tiga) properti
yang nantinya bisa dipetakan ke dalam setiap state atau keadaan. Berikut adalah ketiga
properti tersebut:
1. isActive
Sebuah properti yang menunjukkan ketika sebuah job sedang aktif.
2. isCompleted
Sebuah properti yang menunjukkan ketika sebuah job telah selesai.
3. isCancelled
Sebuah properti yang menunjukkan ketika sebuah job telah dibatalkan.
Pada dasarnya, job akan segera dijalankan setelah ia dibuat. Namun kita juga bisa membuat
sebuah job tanpa menjalankannya. Job memiliki beberapa siklus hidup mulai dari pertama
kali ia dibuat hingga akhirnya selesai. Kira-kira seperti inilah siklus dari sebuah job jika
digambarkan dalam sebuah diagram:
Dari diagram di atas, kita bisa melihat bahwa job akan melewati beberapa state. Pada setiap
state tersebut nantinya kita bisa memberikan instruksi sesuai yang kita inginkan. Sebelum
kita mengolahnya, mari pahami terlebih dahulu semua state yang ada pada sebuah job.
New
Keadaan di mana sebuah job telah diinisialisasi namun belum pernah dijalankan.
Active
Sebuah job akan memiliki status aktif ketika ia sedang berjalan. Dalam hal ini, job
yang sedang ditangguhkan (suspended job) juga termasuk ke dalam job yang aktif.
Completed
Ketika job sudah tidak berjalan lagi. Ini berlaku untuk job yang berakhir secara
normal, dibatalkan, ataupun karena suatu pengecualian.
Cancelling
Suatu kondisi ketika fungsi cancel() dipanggil pada job yang sedang aktif dan
memerlukan waktu untuk pembatalan tersebut selesai.
Cancelled
Keadaan yang dimiliki oleh sebuah job yang sudah berhasil dibatalkan. Perlu
diketahui bahwa job yang dibatalkan juga dapat dianggap sebagai Completed job.
Membuat Job Baru
Job dapat diinisialisasikan menggunakan fungsi launch() maupun Job() seperti berikut:
1. //menggunakan launch():
2. fun main() = runBlocking {
3. val job = launch {
4. // Do background task here
5. }
6. }
7.
8. //menggunakan Job():
9. fun main() = runBlocking {
10. val job = Job()
11. }
Menjalankan Job
Setelah membuat sebuah job, kini kita bisa mulai menjalankan job tersebut. Caranya pun
cukup sederhana, kita bisa menggunakan fungsi start() seperti berikut:
Perbedaan dari keduanya adalah bahwa yang start() akan memulai job tanpa harus
menunggu job tersebut selesai, sedangkan join() akan menunda eksekusi sampai job
selesai. Jika kode pertama dijalankan, maka konsol akan menampilkan hasil berikut:
Other task
Start new job!
Sedangkan kode kedua akan menampilkan hasil:
Other task
Setelah dijalankan, job akan memiliki state Active.
Membatalkan Job
Ibarat pekerjaan di dunia nyata, sebuah job seharusnya bisa dibatalkan. Hanya job yang
sedang aktif yang dapat dibatalkan. Anda bisa melakukannya dengan memanggil
fungsi cancel() seperti berikut:
Kode di atas menggambarkan sebuah job membutuhkan waktu 5 detik untuk dijalankan.
Namun ketika mencapai waktu 2 detik, job tersebut telah dibatalkan. Saat
fungsi cancel() dipanggil, job akan memasuki state Cancelling sampai pembatalan tersebut
berhasil. Kemudian setelah pembatalan berhasil, job akan
memiliki state Cancelled dan Completed.
Perlu diketahui bahwa jika cancel() dipanggil dalam job baru yang belum dijalankan, job
tersebut tidak akan melalui state Cancelling, melainkan akan langsung
memasuki state Cancelled.
1. @InternalCoroutinesApi
2. fun main() = runBlocking {
3. val job = launch {
4. delay(5000)
5. println("Start new job!")
6. }
7.
8. delay(2000)
9. job.cancel(cause = CancellationException("time is up!"))
10. println("Cancelling job...")
11. if (job.isCancelled){
12. println("Job is cancelled because ${job.getCancellationException().message}")
13. }
14. }
Cancelling job...
Job is cancelled because time is up!
Deferred
Seperti yang sudah disampaikan sebelumnya di bagian coroutines builder,
fungsi async akan mengembalikan nilai deferred yang berupa hasil atau exception. Deferred
adalah nilai tangguhan yang dihasilkan dari proses coroutines. Nilai ini nantinya bisa kita
kelola sesuai dengan kebutuhan.
Deferred dapat kita ciptakan secara manual. Meskipun begitu, dalam praktiknya, jarang kita
membuat deferred secara manual. Biasanya kita hanya bekerja dengan deferred yang
dihasilkan oleh async.
Deferred juga memiliki life cycle yang sama dengan job. Perbedaannya hanyalah pada tipe
hasil yang diberikan. Selain memberikan hasil ketika proses komputasi sukses, ia juga bisa
memberikan hasil saat proses tersebut gagal. Hasil dari deferred tersedia ketika
mencapai state completed dan dapat diakses dengan fungsi await. Deferred akan
mengirimkan pengecualian jika ia telah gagal. Kita bisa mengakses nilai pengecualian
tersebut dengan fungsi getCompletionExceptionOrNull.
Pada dasarnya, nilai deferred juga merupakan sebuah job. Ia diciptakan dan dimulai pada
saat coroutines mencapai state active. Bagaimanapun, fungsi async juga memiliki opsional
parameter seperti CoroutineStart.LAZY untuk memulainya. Dengan begitu, deferred juga
bisa diaktifkan saat fungsi start, join, atau await dipanggil.
1. import kotlinx.coroutines.*
2.
3. fun main() = runBlocking {
4. val capital = async { getCapital() }
5. val income = async { getIncome() }
6. println("Your profit is ${income.await() - capital.await()}")
7. }
Dispatcher.Default
1. launch {
2. // TODO: Implement suspending lambda here
3. }
4. launch(Dispatcher.Default){
5. // TODO: Implement suspending lambda here
6. }
Dispatcher.IO
Sebuah dispatcher yang dapat digunakan untuk membongkar pemblokiran operasi
I/O. Ia akan menggunakan kumpulan thread yang dibuat berdasarkan permintaan.
Anda bisa menerapkannya dengan menambahkan Dispatcher.IO pada coroutines
builder:
1. launch(Dispatcher.IO){
2. // TODO: Implement algorithm here
3. }
Dispatcher.Unconfined
Dispatcher ini akan menjalankan coroutines pada thread yang sedang berjalan
sampai mencapai titik penangguhan. Setelah penangguhan, coroutines akan
dilanjutkan pada thread dimana komputasi penangguhan yang dipanggil.
Sebagai contoh, ketika fungsi a memanggil fungsi b, yang dijalankan dengan
dispatcher dalam thread tertentu, fungsi a akan dilanjutkan dalam thread yang sama
dengan fungsi b dijalankan. Perhatikan kode berikut:
1. import kotlinx.coroutines.*
2.
3. fun main() = runBlocking<Unit> {
4. launch(Dispatchers.Unconfined) {
5. println("Starting in ${Thread.currentThread().name}")
6. delay(1000)
7. println("Resuming in ${Thread.currentThread().name}")
8. }.start()
9. }
Starting in main
Resuming in kotlinx.coroutines.DefaultExecutor
Artinya, coroutine telah dimulai dari main thread, kemudian tertunda oleh fungsi
delay selama 1 detik. Setelah itu, coroutine dilanjutkan kembali pada
thread DefaultExecutor.
Bersamaan dengan objek-objek tersebut, ada beberapa builder yang dapat digunakan untuk
menentukan thread yang dibutuhkan:
Starting in myThread
Resuming in myThread
Thread Pool
Sebuah dispatcher yang memiliki kumpulan thread. Ia akan memulai dan
melanjutkan coroutine di salah satu thread yang tersedia pada kumpulan tersebut.
Runtime akan menentukan thread mana yang tersedia dan juga menentukan
bagaimana proses distribusi bebannya.
Anda bisa menerapkan thread pool dengan
fungsi newFixedThreadPoolContext() seperti berikut:
1. import kotlinx.coroutines.*
2.
3. fun main() = runBlocking<Unit> {
4. val dispatcher = newFixedThreadPoolContext(3, "myPool")
5.
6. launch(dispatcher) {
7. println("Starting in ${Thread.currentThread().name}")
8. delay(1000)
9. println("Resuming in ${Thread.currentThread().name}")
10. }.start()
11. }
Pada kode di atas, kita telah menetapkan thread myPool sebanyak 3 thread.
Runtime akan secara otomatis menentukan pada thread mana coroutine akan
dijalankan dan dilanjutkan. Hasil dari kode tersebut adalah:
Starting in myPool-1
Resuming in myPool-2
Channels
Kita sudah belajar bagaimana membuat dan mengelola coroutines. Seperti kita ketahui,
sebuah program dapat memiliki banyak thread dan dalam beberapa thread bisa terdapat
jutaan coroutines. Lalu, bagaimana jika ada 2 (dua) coroutines yang saling ingin berinteraksi
satu sama lain? Channels adalah jawabnya.
Beberapa masalah yang muncul pada concurrency seperti deadlock, race conditions, dan
lainnya, sering kali dipicu oleh satu hal, apa itu? Rupanya problem pembagian memori atau
sumber daya antar thread. Untuk mengatasinya, banyak programming
language seperti Go, Dart, dan juga Kotlin telah menyediakan channels.
Channels adalah nilai deferred yang menyediakan cara mudah untuk mentransfer nilai
tunggal antara coroutine. Pada dasarnya, channels sangat mirip dengan
BlockingQueue [11]. Namun, alih-alih memblokir sebuah thread, channels menangguhkan
sebuah coroutine yang jauh lebih ringan. Untuk lebih memahaminya, mari simak kode di
bawah ini:
1. import kotlinx.coroutines.*
2. import kotlinx.coroutines.channels.Channel
3.
4. fun main() = runBlocking(CoroutineName("main")) {
5. val channel = Channel<Int>()
6. launch(CoroutineName("v1coroutine")){
7. println("Sending from ${Thread.currentThread().name}")
8. for (x in 1..5) channel.send(x * x)
9. }
10.
11. repeat(5) { println(channel.receive()) }
12. println("received in ${Thread.currentThread().name}")
13. }
o Process
Merupakan bagian dari proses aplikasi yang dijalankan. Setiap kali aplikasi
dijalankan, maka saat itu juga proses dijalankan. Tergantung pada sistem
operasi yang digunakan, suatu proses dapat terdiri dari beberapa thread yang
menjalankan instruksi secara bersamaan.
o Thread
Biasa dikenal sebagai proses yang ringan dan merupakan komponen dari
suatu proses aplikasi yang menjalankan tugas tertentu secara spesifik.
o I/O-Bound
Sebuah algoritma yang bergantung pada perangkat input atau output. Waktu
untuk mengeksekusi sebuah I/O-bound tergantung pada kecepatan
perangkat yang digunakan.
Ada beberapa permasalahan yang ditimbulkan ketika menerapkan concurrency pada
aplikasi, yaitu :
o Deadlocks
Sebuah kondisi di mana dua proses atau lebih saling menunggu proses yang
lain untuk melepaskan resource yang sedang digunakan yang
mengakibatkan proses yang sedang berjalan tak kunjung selesai melakukan
operasinya.
o Livelocks
Kondisi di mana sebuah proses tidak bisa melanjutkan tugasnya, sama
seperti deadlocks. Namun, perbedaannya adalah selama livelocks terjadi,
keadaan aplikasi tetap bisa berubah. Walau perubahan keadaan tersebut
menyebabkan proses berjalan dengan tidak semestinya.
o Starvation
Kondisi yang biasa terjadi setelah deadlock. Kondisi ini sering kali
menyebabkan sebuah proses kekurangan sumber daya. Pada kondisi ini,
thread menjadi tidak mendapatkan akses reguler ke sumber daya bersama
dan membuat proses terhenti.
o Race Conditions
Kondisi di mana terdapat banyak thread yang mengakses data yang
dibagikan bersama dan mencoba mengubahnya secara bersamaan. Ini bisa
terjadi ketika kode concurrent yang dituliskan untuk berjalan secara
sekuensial.
Coroutines merupakan fitur unggulan dan cara baru dalam menuliskan thread yang
ringan dan efisien pada Kotlin. Disediakan JetBrains khusus untuk Kotlin.
Untuk menjalankan coroutine, diperlukan fungsi yang biasa disebut
sebagai coroutine builder yang mengambil suspending lambda dan membuat
coroutine untuk menjalankannya. Ada beberapa macam coroutine builder yaitu:
o launch
Fungsi ini digunakan untuk memulai sebuah coroutines yang tidak akan
mengembalikan sebuah hasil dan menghasilkan Job yang dapat digunakan
untuk membatalkan eksekusi.
o runBlocking
Dibuat untuk menjembatani blocking code menjadi kode yang dapat
ditangguhkan. runBlocking akan memblokir sebuah thread yang sedang
berjalan hingga eksekusi coroutine selesai
o async
Fungsi ini digunakan untuk memulai sebuah coroutine yang akan
mengembalikan sebuah hasil.
Job merupakan hasil dari perintah asynchronous yang dijalankan dan
merepresentasikan coroutine yang sebenarnya. Memiliki 3 properti terdiri
dari isActive, isCompleted, dan isCancelled yang menunjukkan state dari Job
tersebut.
Deferred adalah nilai kembalian dari fungsi async yang dapat berupa hasil atau
exception. Merupakan nilai tangguhan yang dihasilkan dari proses coroutines dan
bisa kita kelola sesuai dengan kebutuhan.
CoroutineDispatcher, merupakan base class yang menentukan thread yang
berfungsi untuk menjalankan coroutines. Class ini diimplementasikan pada beberapa
class berikut :
o Dispatcher.Default
Merupakan dispatcher dasar yang digunakan oleh semua standard builders
seperti launch, async, dll jika tidak ada dispatcher lain yang ditentukan.
Dispatcher.Default menggunakan kumpulan thread yang ada pada JVM.
Jumlah maksimal thread yang digunakan adalah sama dengan jumlah core
dari CPU.
o Dispatcher.IO
Merupakan dispatcher yang digunakan untuk membongkar pemblokiran
operasi I/O dan akan menggunakan kumpulan thread yang dibuat
berdasarkan permintaan.
o Dispatcher.Unconfined
Merupakan Dispatcher yang menjalankan coroutines pada thread yang
sedang berjalan sampai mencapai titik penangguhan. Setelah penangguhan,
coroutines akan dilanjutkan pada thread dimana komputasi penangguhan
yang dipanggil.
Selain dengan menggunakan metode diatas, ada beberapa builder lain yang dapat
digunakan untuk menentukan thread yang dibutuhkan, antara lain:
Sebelum mengerjakan soal latihan, pastikan terlebih dahulu jika soal latihan tersebut adalah soal yang up-
to-date. Caranya, cukup dengan melakukan Synchronize Course pada IntelliJ IDEA. Ini bertujuan agar
peserta mudah dalam menyelesaikan tugas submission di akhir pembelajaran.
Latihan
Anda sudah mempelajari apa itu concurrency dan juga Kotlin coroutines yang
memungkinkan Anda untuk membuat concurrent program dengan sangat mudah.
Untuk menguji pemahaman Anda terhadap materi yang sudah dipelajari, Anda perlu
mengerjakan beberapa latihan sederhana. Buka proyek latihan dan kerjakan semua latihan
yang ada pada modul Coroutines.
Rangkuman Kelas Memulai
Pemrograman dengan Kotlin
Selain itu dalam sub-modul pendahuluan ini Anda juga telah mengetahui beberapa hal
sebagai berikut:
1. Selain mudah dipelajari, Anda juga dapat ikut berkontribusi di dalamnya karena
Kotlin merupakan project open-source.
2. Kotlin mendukung 2 (dua) paradigma umum yang akan Anda sering jumpai
yaitu object-oriented programming (OOP) dan functional programming (FP). Kedua
paradigma tersebut akan sangat membantu proses pengembangan dengan masing-
masing fitur yang dimilikinya.
3. Selain multiparadigm, Kotlin juga mendukung multiplatform yang berbeda dengan
bahasa pemrograman mainstream lainnya di mana kita dapat membuat aplikasi
mobile (iOS/Android), Web, Desktop, atau Server. Bahkan Kotlin digadang-gadang
bisa digunakan untuk Deep Learning dengan KotlinDL yang saat ini sudah berstatus
alpha!
4. Kotlin pernah berada di posisi pertama dalam Fastest growing languages
versi GitHub Octoverse 2018 berkat banyaknya dukungan komunitas dalam
pengembangannya.
5. Tersedia berbagai macam framework yang bisa Anda gunakan untuk mempermudah
pengembangan aplikasi server-side menggunakan Kotlin seperti:
o Spring
Spring merupakan sebuah framework yang sangat terkenal di Java. Spring
bisa digunakan pada Kotlin untuk komunikasi ke API dengan lebih ringkas.
Tersedia juga Spring Initializr yang memungkinkan kita untuk membuat
proyek Spring baru dengan Kotlin.
o Vert.x
Merupakan sebuah framework untuk membuat reactive Web app di JVM.
Anda bisa melihat repository-nya di https://github.com/vert-x3/vertx-lang-
kotlin.
o Ktor
Ktor adalah sebuah framework yang dikembangkan oleh JetBrains untuk
membuat aplikasi Web di Kotlin. Ktor memanfaatkan coroutine untuk
skalabilitas yang tinggi dan menawarkan API yang mudah digunakan.
o Kotlinx.html
Merupakan sebuah DSL yang dapat digunakan untuk membuat HTML di
aplikasi Web. Kotlinx.html dapat digunakan sebagai alternatif untuk sistem
templating tradisional seperti JSP dan FreeMarker.
o Exposed
Sebuah framework SQL yang menyediakan kumpulan DSL yang mudah
dibaca untuk menggambarkan struktur database SQL dan melakukan kueri
sepenuhnya dengan Kotlin.
6. Kotlin mendukung dengan baik dan memiliki beberapa kelebihan dalam
mengembangkan aplikasi Android seperti di bawah ini:
o Compatibility
Kotlin sepenuhnya kompatibel dengan JDK 6. Ini memastikan bahwa aplikasi
yang dibangun dengan Kotlin dapat berjalan pada perangkat Android yang
lebih lama tanpa ada masalah. Android Studio pun mendukung penuh
pengembangan dengan bahasa Kotlin.
o Performance
Dengan struktur bytecode yang sama dengan Java, aplikasi yang dibangun
dengan Kotlin dapat berjalan setara dengan aplikasi yang dibangun dengan
Java. Terdapat juga fitur seperti inline function pada Kotlin yang membuat
kode yang dituliskan dengan lambda bisa berjalan lebih cepat dibandingkan
kode yang sama dan dituliskan dengan Java.
o Interoperability
Semua library Android yang tersedia, dapat digunakan pada Kotlin.
o Compilation Time
Kotlin mendukung kompilasi inkremental yang efisien. Oleh karena itu, proses
build biasanya sama atau lebih cepat dibandingkan dengan Java.
7. “Safe” merupakan salah satu karakteristik Kotlin yang sangat berguna di mana Anda
bisa meminimalisir kesalahan NullPointerException.
Rangkuman di atas merupakan awal dari perjalanan Anda dalam menyelesaikan kelas ini.
Jadi, tetap semangat untuk menuntaskan modul-modul berikutnya ya!
Sebelum Anda lanjut ke modul berikutnya, berikut adalah beberapa rangkuman dari sub-
modul yang sudah dipelajari:
Sama seperti bahasa pemrograman lain, Kotlin memiliki fungsi untuk mencetak nilai
pada console yaitu fungsi print() dan println().
Untuk mendeklarasi variable, Anda akan menggunakan kata kunci var ata val. var
atau val digunakan untuk mengontrol nilai dari sebuah variabel. Dengan kata kunci
var kita bisa mengubah nilai yang sudah kita inisialisasikan.
Untuk membuat variable yang menampung data berupa text, Anda dapat
menggunakan tipe data Char untuk menyimpan satu karakter dan tipe
data String untuk menyimpan beberapa karakter.
Sedangkan untuk menampung data berupa number, Anda akan menggunakan
beberapa tipe data di bawah ini:
o Int (32 Bit)
Int adalah tipe data yang umumnya digunakan untuk menyimpan nilai
numerik. Int dapat menyimpan data dari range -2^31 sampai +2^31-1.
Dengan ukuran 32 Bit kita bisa menggunakannya untuk menyimpan nilai
yang besar. Catatannya, tetap lihatlah batasan nilai maksimal yang dapat
dimasukkan.
o Long (64 Bit)
Long adalah tipe data yang digunakan untuk menyimpan nilai numerik yang
lebih besar yaitu dari range -2^63 sampai +2^63-1.
o Short (16 Bit)
Short merupakan sebuah bilangan bulat yang hanya dapat menyimpan nilai
yang kecil karena hanya berukuran 16 Bit.
o Byte (8 Bit)
Dengan ukuran yang kecil, Byte hanya mampu menyimpan nilai yang kecil
sama halnya seperti Short. Byte biasa digunakan untuk keperluan proses
membaca dan menulis data dari sebuah stream file atau jaringan.
o Double (64 Bit)
Sama halnya dengan Long yang memiliki ukuran yang besar, Double mampu
menyimpan nilai numerik yang besar pula. Pada umumnya Double digunakan
untuk menyimpan nilai numerik pecahan.
Selanjutnya adalah Array, yakni tipe data yang memungkinkan Anda untuk
menyimpan beberapa objek di dalam satu variabel.
Kotlin juga memungkinkan Anda untuk membuat Array dengan tipe data primitif
dengan memanfaatkan beberapa fungsi spesifik seperti berikut:
o intArrayOf() : IntArray
o booleanArrayOf() : BooleanArray
o charArrayOf() : CharArray
o longArrayOf() : LongArray
o shortArrayOf() : ShortArray
o byteArrayOf() : ByteArray
Kotlin mendukung juga tipe data Boolean di mana tipe data yang hanya memiliki dua
nilai, yaitu true dan false. Selain itu, Terdapat 3 (tiga) operator yang dapat digunakan
pada Boolean:
o Conjunction atau AND (&&)
Operator AND (&&) akan mengembalikan nilai true jika semua hasil evaluasi
expression yang diberikan bernilai true.
o Disjunction atau OR (||)
Berbeda dengan operator AND (&&), operator OR (||) akan mengembalikan
nilai true jika hasil evaluasi dari salah satu expressions yang diberikan
bernilai true.
o Negation atau NOT (!)
Berbeda dengan operator AND (&&) dan operator OR(||), operator NOT(!)
digunakan untuk melakukan negasi pada hasil evaluasi expression yang
diberikan.
Jika ingin menginisialisasi nilai dari sebuah variabel berdasarkan suatu kondisi.
Untuk menyelesaikannya, gunakan If Expression.
Dengan Kotlin kita mudah dalam mengelola variable nullable sehingga dapat
meminimalisir terjadinya NullPointerException dengan menggunakan Safe
Call dan Elvis Operator.
Function atau fungsi merupakan sebuah prosedur yang memiliki keterkaitan dengan
pesan dan objek. Ketika kita memanggil sebuah fungsi maka sebuah mini-
program akan dijalankan. Fungsi sendiri bisa diartikan sebagai cara sederhana untuk
mengatur program buatan kita.
Kita sudah selesai dengan Kotlin Fundamental di mana Anda sudah mempelajari tentang
bagaimana menampilkan pesan pada konsol, berbagai macam tipe data, dan bagaimana
cara meminimalisir terjadinya NullPointerException. Selanjutnya kita mempelajari modul
Control Flow. Langsung saja ke modul berikutnya.
Untuk menghindari penggunaan konstan yang keliru, kita bisa memanfaatkan fitur
Enumeration untuk menyimpan kumpulan objek yang telah didefinisikan menjadi tipe
data konstanta.
Jika memiliki beberapa ekspresi untuk menentukan hasil evaluasi, Anda bisa
menggunakan when expression. Karena sebuah expression, when dapat
mengembalikan nilai yang dapat ditampung pada sebuah variabel.
Sama seperti when, if expression dapat mengembalikkan nilai yang dapat ditampung
pada sebuah variabel. Namun sedikit berbeda dengan when, if lebih cocok
digunakan jika ekspresi yang akan digunakan untuk dievaluasi hanya 1 (satu).
Dalam mempelajari bahasa pemgrograman, Anda akan sering menjumpai istilah
expressions dan statement.
Jika ingin melakukan perulangan, ada beberapa cara yang dapat diterapkan yaitu:
o While
While bersifat Entry Controlled Loop. Artinya, kondisi yang diberikan akan
dievaluasi terlebih dahulu. Jika kondisi tersebut terpenuhi maka proses
perulangan akan dijalankan.
o Do While
Do While bersifat Exit Controlled Loop di mana proses perulangan akan
langsung dijalankan di awal. Jika telah selesai, barulah kondisi yang diberikan
akan dievaluasi.
o For Loop
For merupakan konsep perulangan pada blok yang sama selama hasil
evaluasi kondisi yang diberikan terpenuhi atau bernilai true. For
memanfaatkan tipe data Range untuk menentukan kondisi yang akan
dievaluasi.
Saat menggunakan While dan Do While perhatikan infinite loop, yaitu kondisi di
mana proses perulangan berlangsung terus menerus sampai aplikasi menjadi crash.
Saat menerapkan perulangan, kita bisa memanfaatkan kata
kunci break dan continue. Kedua kata kunci tersebut digunakan untuk menentukan
proses perulangan akan seperti apa di mana break digunakan untuk menghentikan
proses perulangan walaupun hasil evaluasi masih menghasil true dan continue
digunakan untuk melanjutkan proses perulangan selanjutnya.
Data Classes
Pada modul ini, kita akan mempelajari sebuah fitur menarik pada Kotlin, yaitu Data Classes.
Kotlin mengenalkan konsep data class yang merupakan sebuah kelas sederhana yang bisa
berperan sebagai data container. Data class adalah sebuah kelas yang tidak memiliki logika
apapun dan juga tidak memiliki fungsionalitas lain selain menangani data.
Kenapa disebut dengan kelas sederhana? Seperti yang sudah kita ketahui, Kotlin
memungkinkan kita untuk menulis kode dengan ringkas dan lebih efisien. Dalam membuat
sebuah data class, kita tidak perlu menuliskan banyak kode yang seharusnya dibutuhkan
untuk mengelola sebuah data. Data class mampu menyediakan beberapa fungsionalitas
yang biasanya kita butuhkan untuk mengelola data hanya dengan sebuah keyword data.
Hanya dengan satu baris kode di atas, kompiler akan secara otomatis
menghasilkan constructor, toString(), equals(), hashCode(), copy() dan juga
fungsi componentN(). Tentunya ini jauh lebih mudah dan bersih dibandingkan kita harus
menuliskan banyak kode secara manual.
Beberapa hal yang perlu diperhatikan dalam membuat sebuah data class adalah:
Konstruktor utama pada kelas tersebut harus memiliki setidaknya satu parameter;
Semua konstruktor utama perlu dideklarasikan sebagai val atau var;
Modifier dari sebuah data class tidak bisa abstract, open, sealed, atau inner.
Rangkuman dari Kotlin Functional Programming
Sebelum lanjut ke paradigma Object-Oriented Programming, mari kita rangkum apa saja
yang sudah Anda pelajari pada modul Functional Programming.
Anatomi dari sebuah function terdiri dari 2 (adua) bagian utama, yaitu function
header dan function body:
o Function Header
Function header adalah bagian yang merupakan konstruksi dari sebuah
fungsi untuk menentukan perilakunya akan seperti apa. Di dalam function
header terdapat visibility modifier, kata kunci fun, nama, daftar parameter dan
nilai kembalian dari fungsi tersebut.
o Function Body
Function Body adalah bagian yang dalamnya kita akan menempatkan sebuah
logika kode baik itu sebuah expression atau statement.
Kotlin memiliki fitur seperti named dan default argument yang dapat menghindari kita
dari kesalahan saat menyematkan sebuah argumen saat menggunakan sebuah
fungsi.
Selain named dan default argument, Kotlin juga memiliki fitur varargs yang dapat
digunakan untuk menyederhanakan deklarasi beberapa parameter yang memiliki tipe
yang sama.
Kotlin mendukung 2 (dua) extension yang dapat digunakan, yaitu Extension
Functions dan Extension Properties. Jika extension functions digunakan untuk
menambahkan fungsi baru, extension properties tentunya digunakan untuk
menambahkan sebuah properti baru.
Lambda expression, atau biasa disebut dengan anonymous function atau function
literal adalah fitur yang cukup populer sampai sekarang dalam dunia functional
programming. Bisa disebut sebagai anonymous karena lambda tidak memiliki
sebuah nama seperti halnya sebuah fungsi pada umumnya. Karena merupakan
sebuah fungsi, lambda juga dapat memiliki daftar parameter, body dan return type.
Berikut adalah beberapa karakteristik dari Lambda pada kotlin.
o Fold
Fold, Anda bisa dengan mudah melakukan perhitungan setiap nilai yang
berada di dalam sebuah collection tanpa harus melakukan iterasi item
tersebut satu-persatu menggunakan fungsi fold().
o Drop
drop(), fungsi yang bisa kita manfaatkan untuk memangkas item yang berada
di dalam sebuah objek collection berdasarkan jumlah yang kita tentukan.
o Take
Fungsi take() bisa kita manfaatkan untuk menyaring item yang berada di
dalam sebuah objek collection.
o Slice
Bagaimana jika kita ingin menyaring item dari posisi tertentu? Untuk itu Anda
bisa memanfaatkan fungsi slice().
o Distinct
Saat berurusan dengan item yang sama di dalam sebuah collection, untuk
menyaring item yang sama tersebut kita akan melakukan iterasi dan
membandingkan setiap itemnya. Namun dengan Kotlin kita tidak perlu
melakukannya secara manual, karena Kotlin Collection menyediakan fungsi
untuk melakukannya dengan mudah yaitu fungsi distinct().
o Chunked
Sama seperti fungsi split(), fungsi chunked() bisa kita gunakan untuk
memecah nilai String menjadi beberapa bagian kecil dalam bentuk Array.
Recursion merupakan sebuah teknik dasar dalam pemrograman yang bisa kita
gunakan untuk menyederhanakan pemecahan masalah yang umumnya diselesaikan
dengan cara yang kompleks. Di Kotlin, recursion disebut juga dengan recursive
function.
o Primary Constructor
Seperti namanya, jika kita akan membuat suatu objek dari sebuah
kelas dan kelas tersebut memiliki constructor di dalamnya, maka
konstruktor tersebut adalah primary constructor dan diharuskan untuk
mengirim nilai sesuai properti yang dibutuhkan.
Primary Constructor juga dapat memiliki nilai default, dengan begitu
jika kita tidak menetapkan nilai untuk parameter tersebut maka
properti tersebut akan memiliki nilai default.
Kotlin menyediakan blok init yang memungkinkan kita untuk
menuliskan properti di dalam body class ketika kita
menggunakan primary constructor.
o Secondary Constructor
o ArithmeticException
1. interface List<T>{
2. operator fun get(index: Int) : T
3. }
1. interface List<T>{
2. operator fun get(index: Int) : T
3. }
1. fun <T> run(): T {
2. /*...*/
3. }
Constraint Type Parameter adalah teknik untuk membatasi cakupan tipe data apa
saja yang dapat di sediakan oleh Generic Class maupun Generic Function.
Pembatasan ini dilakukan dengan membubuhkan tanda colon (:) setelah tipe
parameter yang kemudian diikuti oleh tipe data yang akan dijadikan batasan.
Batasan dalam Constraint Type Parameter adalah child class dari tipe data
batasan tersebut. Jadi, semisal ditentukan batasannya adalah tipe List, maka hanya
berlaku pada child class dari class tersebut.
Variance, adalah konsep pada Generics yang menggambarkan bagaimana sebuah
tipe yang memiliki subtipe yang sama dan tipe argumen yang berbeda saling
berkaitan satu sama lain. Ada beberapa jenis dari variance yaitu:
o Covariant
Ditandai dengan keyword ‘out’ sebelum deklarasi dari tipe parameter. Nilai
dari tipe parameter tersebut hanya bisa diproduksi seperti menjadikanya
sebagai return type dan tidak dapat dikonsumsi seperti menjadikannya
sebagai tipe argumen untuk setiap fungsi di dalam kelas tersebut.
o Contravariant
Ditandai dengan keyword ‘in’ sebelum deklarasi dari tipe parameter. Nilai dari
tipe parameter tersebut hanya bisa dikonsumsi seperti menjadikannya
sebagai tipe argumen untuk setiap fungsi dan tidak untuk dikonsumsi
sebagai return type dari sebuah function di dalam kelas tersebut.
Concurrency adalah beberapa proses yang terjadi pada suatu sistem. Terjadi apabila
terdapat 2 (dua) atau lebih proses yang tumpang tindih dalam satu waktu.
Parallelism, sama seperti concurrency, namun 2 (dua) atau lebih proses tersebut
dijalankan pada waktu yang sama persis.
Ada beberapa konsep dasar dari penerapan concurrency yang harus dipelajari
yaitu:
o Process
Merupakan bagian dari proses aplikasi yang dijalankan. Setiap kali aplikasi
dijalankan, maka saat itu juga proses dijalankan. Tergantung pada sistem
operasi yang digunakan, suatu proses dapat terdiri dari beberapa thread yang
menjalankan instruksi secara bersamaan.
o Thread
Biasa dikenal sebagai proses yang ringan dan merupakan komponen dari
suatu proses aplikasi yang menjalankan tugas tertentu secara spesifik.
o I/O-Bound
Sebuah algoritma yang bergantung pada perangkat input atau output. Waktu
untuk mengeksekusi sebuah I/O-bound tergantung pada kecepatan
perangkat yang digunakan.
Ada beberapa permasalahan yang ditimbulkan ketika
menerapkan concurrency pada aplikasi, yaitu :
o Deadlocks
Sebuah kondisi di mana dua proses atau lebih saling menunggu proses yang
lain untuk melepaskan resource yang sedang digunakan yang
mengakibatkan proses yang sedang berjalan tak kunjung selesai melakukan
operasinya.
o Livelocks
Kondisi di mana sebuah proses tidak bisa melanjutkan tugasnya, sama
seperti deadlocks. Namun, perbedaannya adalah selama livelocks terjadi,
keadaan aplikasi tetap bisa berubah. Walau perubahan keadaan tersebut
menyebabkan proses berjalan dengan tidak semestinya.
o Starvation
Kondisi yang biasa terjadi setelah deadlock. Kondisi ini sering kali
menyebabkan sebuah proses kekurangan sumber daya. Pada kondisi ini,
thread menjadi tidak mendapatkan akses reguler ke sumber daya bersama
dan membuat proses terhenti.
o Race Conditions
Kondisi di mana terdapat banyak thread yang mengakses data yang
dibagikan bersama dan mencoba mengubahnya secara bersamaan. Ini bisa
terjadi ketika kode concurrent yang dituliskan untuk berjalan secara
sekuensial.
Coroutines merupakan fitur unggulan dan cara baru dalam menuliskan thread yang
ringan dan efisien pada Kotlin. Disediakan JetBrains khusus untuk Kotlin.
Untuk menjalankan coroutine, diperlukan fungsi yang biasa disebut
sebagai coroutine builder yang mengambil suspending lambda dan membuat
coroutine untuk menjalankannya. Ada beberapa macam coroutine builder yaitu:
o launch
Fungsi ini digunakan untuk memulai sebuah coroutines yang tidak akan
mengembalikan sebuah hasil dan menghasilkan Job yang dapat digunakan
untuk membatalkan eksekusi.
o runBlocking
Dibuat untuk menjembatani blocking code menjadi kode yang dapat
ditangguhkan. runBlocking akan memblokir sebuah thread yang sedang
berjalan hingga eksekusi coroutine selesai
o async
Fungsi ini digunakan untuk memulai sebuah coroutine yang akan
mengembalikan sebuah hasil.
Job, merupakan hasil dari perintah asynchronous yang dijalankan dan
merepresentasikan coroutine yang sebenarnya. Memiliki 3 properti terdiri
dari isActive, isCompleted, dan isCancelled yang
menunjukkan state dari Job tersebut.
Deferred, adalah nilai kembalian dari fungsi async yang dapat berupa hasil atau
exception. Merupakan nilai tangguhan yang dihasilkan dari proses coroutines dan
bisa kita kelola sesuai dengan kebutuhan.
CoroutineDispatcher, merupakan base class yang menentukan thread yang
berfungsi untuk menjalankan coroutines. Class ini diimplementasikan pada beberapa
class berikut :
o Dispatcher.Default
Merupakan dispatcher dasar yang digunakan oleh semua standard builders
seperti launch, async, dll jika tidak ada dispatcher lain yang
ditentukan. Dispatcher.Default menggunakan kumpulan thread yang ada
pada JVM. Jumlah maksimal thread yang digunakan adalah sama dengan
jumlah core dari CPU.
o Dispatcher.IO
Merupakan dispatcher yang digunakan untuk membongkar pemblokiran
operasi I/O dan akan menggunakan kumpulan thread yang dibuat
berdasarkan permintaan.
o Dispatcher.Unconfined
Merupakan Dispatcher yang menjalankan coroutines pada thread yang
sedang berjalan sampai mencapai titik penangguhan. Setelah penangguhan,
coroutines akan dilanjutkan pada thread dimana komputasi penangguhan
yang dipanggil.
Selain dengan menggunakan metode diatas, ada beberapa builder lain yang dapat
digunakan untuk menentukan thread yang dibutuhkan, antara lain: