Selasa, 14 Juni 2022

Belajar Lengkap Generics di Kotlin


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.

Berkenalan Dengan Generics

Seperti yang kita ketahui, Kotlin termasuk dalam bahasa pemrograman statically typed. Ketika menambahkan variabel baru, maka secara otomatis tipe dari variabel tersebut dapat dikenali pada saat kompilasi. 
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:

  1. val contributor = listOf<String>("jasoet", "alfian","nrohmen","dimas","widy")



Perhatikan kode di atas. Tipe parameter yang digunakan dalam pemanggilan fungsi listOf() adalah String maka nilai yang bisa kita masukkan adalah nilai dengan tipe String. 
Kita bisa menyederhanakannya dengan menghapus tipe parameter tersebut. Karena kompiler akan menetapkannya secara otomatis bahwa variabel yang kita buat adalah List.

  1. val contributor = listOf("alfian","nrohmen","dimans","widy")



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:

  1. val contributor = listOf<String>()



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. val points = mapOf<String, Int>( "alfian" to 10 , "dimas" to 20 )


Mendeklarasikan Kelas Generic

Setelah mengetahui contoh bagaimana generic bekerja pada sebuah kelas, selanjutnya kita akan mempelajari bagaimana penerapan generic itu sendiri. Kita bisa menerapkannya dengan meletakkan tipe parameter ke dalam angle brackets (<>) 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:

  1. class LongList : List<Long>{

  2.     override fun get(index: Int): Long {

  3.         /* .. */

  4.     }

  5. }

  6.  

  7. class ArrayList<T> : List<T>{

  8.     override fun get(index: Int): T {

  9.         /* .. */

  10.     }

  11. }



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.         /* .. */

  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 misalnya seperti berikut:

  1. class ArrayList<T> : List<T> {

  2.     override fun get(index: Int): T {

  3.         /* .. */

  4.     }

  5. }

  6.  

  7. interface List<P> {

  8.     operator fun get(index: Int): P

  9. }



Mendeklarasikan Fungsi Generic

Setelah deklarasi generic pada sebuah kelas, apa berikutnya? Kita akan belajar bagaimana mendeklarasikan generic pada sebuah fungsi. 
Generic pada sebuah fungsi dibutuhkan ketika kita membuat sebuah fungsi yang berhubungan dengan List. Misalnya, list yang dapat digunakan untuk berbagai tipe dan tidak terpaku pada tipe tertentu. 
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:

  1. fun <T> run(): T {

  2.     /*...*/

  3. }



Contoh penerapan fungsi generic bisa kita lihat pada deklarasi fungsi slice yang merupakan extensions function dari kelas List berikut:

  1. public fun <T> List<T>.slice(indices: Iterable<Int>): List<T> {

  2.     /*...*/

  3. }



Tipe parameter pada fungsi slice() di atas digunakan sebagai receiver dan return type. Ketika fungsi tersebut dipanggil dari sebuah List dengan tipe tertentu, kita bisa menentukan tipe argumennya secara spesifik seperti 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:

  1. class ListNumber<T : Number> : List<T>{

  2.     override fun get(index: Int): T {

  3.         /* .. */

  4.     }

  5. }



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. }



Contoh lain dari constraint type parameter adalah seperti berikut:

  1. fun <T : Number> List<T>.sumNumber() : T {

  2.     /* .. */

  3. }



Fungsi di atas merupakan extensions function dari kelas List yang mempunyai tipe parameter. Sama seperti deklarasi generic pada sebuah fungsi, tipe parameter T pada fungsi tersebut juga akan digunakan sebagai receiver dan return type
Perbedaannya terletak pada cara memanggilnya. Fungsi tersebut akan tersedia pada variabel List dengan tipe argumen yang memiliki supertype Number.

  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. abstract class Vehicle(wheel: Int)

  2. class Car(speed: Int) : Vehicle(4)

  3. class MotorCycle(speed: Int) : Vehicle(2)


Kemudian jalankan kode seperti berikut:

  1. fun main() {

  2.     val car = Car(200)

  3.     val motorCycle = MotorCycle(100)

  4.     var vehicle: Vehicle = car

  5.     vehicle = motorCycle

  6. }


Bisa kita perhatikan pada kode di atas, variabel car dan motorcycle merupakan subtipe dari Vehicle sehingga kita bisa melakukan assignment antar dua variabel tersebut. Maka seharusnya kode tersebut akan berhasil dikompilasi.
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. }


Dari contoh di atas, kita melihat bagaimana variance menggambarkan keterkaitan antara carList dan vehicleList di mana Car merupakan subtipe dari Vehicle
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:

  1. interface List<out E> : Collection<E> {

  2.     operator fun get(index: Int): E

  3. }


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 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 Comparable<in T> {

  2.     operator fun compareTo(other: T): Int

  3. }




Posting Komentar