Kotlin Programming 2020 Fall - Lecture 11 === ###### tags: `Kotlin` This lecture intends to cover the materials in Chapter 17. ## Generics + Allow you to reuse codes for different types of data. + Example: `List<Int>`, `List<String>`, `List<Double>` use the same codes for different types `Int`, `String`, `Double`. The only difference among them is the type of data. + Kotlin support generic data types and generic functions. ### Generic Types ```kotlin interface Lockable<T> { var locked: Boolean fun lock(key: T) fun unlock(key: T) } class Locker<K, T>(key: K, item: T, override var locked: Boolean) : Lockable<K> { private val secretKey = key var item: T? = item get() = if (locked) null else field set(value) { if (!locked) field = value } override fun lock(key: K) { if (key == secretKey) locked = true } override fun unlock(key: K) { if (key == secretKey) locked = false } // to reveal the secret data override fun toString(): String { if (!locked) return "key: $secretKey, item: $item, locked: false" unlock(secretKey) val ret = "key: $secretKey, item: $item, locked: true" lock(secretKey) return ret } } fun main() { val lockerA = Locker<Int, Int>(5566, 7788, true) // this will not unlock the locker lockerA.unlock(55667) // you cannot access the item directly println("lockerA.item: ${lockerA.item}") // you cannot modify the item when locked lockerA.item = 8899 println("lockerA:\n$lockerA") // this unlocks the locker lockerA.unlock(5566) println("lockerA.item: ${lockerA.item}") // you cannot modify the item when locked lockerA.item = 8899 println("lockerA:\n$lockerA") val lockerB = Locker<String, Double>("NCTU", 140.113, false) // the following won't compile: wrong type // lockerB.unlock(55667) // this will not lock the locker lockerB.lock("NTHU") // you can access the item println("lockerB.item: ${lockerB.item}") // you can modify the item lockerB.item = 140.114 println("lockerB:\n$lockerB") // this will lock the locker lockerB.lock("NCTU") // you cannot access the item, since it is locked println("lockerB.item: ${lockerB.item}") // you cannot modify the item, since it is locked lockerB.item = 140.112 println("lockerB:\n$lockerB") } ``` ### Generic Functions ```kotlin fun <T> duplicate(item: T, num: Int): List<T> { return (1..num).map{ item } } fun main() { val five566 = duplicate(566, 5) println(five566) val fiveFiveSixSix = duplicate("five sixty-six", 5) println(fiveFiveSixSix) } ``` ### Generic Constraints + We put some limitation on the type. For example, we want to generate the binary representation for numbers (`Number` abstract class in Kotlin). Then, the following code only allows the subclass of `Number` to be used as an argument when call `binaryString`. ```kotlin // note: this is intended to demo generic constraints // "fun binaryString(num: Number): String" make more sense fun <T: Number> binaryString(num: T): String = when (num) { is Byte -> num.toString(2) is Short -> num.toString(2) is Int -> num.toString(2) is Long -> num.toString(2) else -> "Not supported" } fun main() { println(binaryString(1234)) println(binaryString(12345678901234L)) println(binaryString(123.456)) // won't compile // println(binaryString("1234567") } ``` + If a type has more than one constraints, we use `where` to specify them. + The following code is modified from Lecture 10. We add two functions from line 149. It simplifies the main function. ```kotlin= enum class DamageStatus { DESTROYED, SEVERE, MEDIUM, SLIGHT, UNDAMAGED } interface Destructible { val damage: DamageStatus val destroyed: Boolean get() = damage == DamageStatus.DESTROYED fun restore(cost: Int) fun absorb(hit: Int) } interface Reporter { fun report() } abstract class Ship(val name: String, val maxHitPoint: Int) : Destructible, Reporter { var hitPoint = maxHitPoint abstract val type: String override val damage: DamageStatus get() = when (3 * hitPoint / maxHitPoint) { 3 -> DamageStatus.UNDAMAGED 2 -> DamageStatus.SLIGHT 1 -> DamageStatus.MEDIUM else -> { if (hitPoint == 0) DamageStatus.DESTROYED else DamageStatus.SEVERE } } // Pay 100 to recover 1 hit point override fun restore(cost: Int) { if (destroyed) { println("There is no way to repair a destroyed ship.") return } hitPoint = minOf(maxHitPoint, hitPoint + cost / 100) println("$name is being repaired, and it's hit point becomes $hitPoint") } // Every 500 hit reduce 1 hit point override fun absorb(hit: Int) { val damageHP = minOf(hitPoint, hit / 500) hitPoint -= damageHP println("$name absorbs attacks and loses $damageHP hit points.") } override fun report() { println("=======================================") println("$type $name:") println("HP/MAX: $hitPoint/$maxHitPoint $damage") } abstract fun attack(obj: Destructible) abstract fun fix(obj: Destructible) } class Destroyer(name: String, maxHitPoint: Int, val power: Int) : Ship(name, maxHitPoint) { override val type = "Destroyer" val attackPoint: Int get() = when (damage) { DamageStatus.UNDAMAGED -> 1000 * power DamageStatus.SLIGHT -> 900 * power DamageStatus.MEDIUM -> 700 * power DamageStatus.SEVERE -> 100 * power DamageStatus.DESTROYED -> 0 } override fun attack(obj: Destructible) { if (destroyed) { return println("$type $name cannot attack any other.") } println("$type $name attacks!") obj.absorb(attackPoint) } override fun fix(obj: Destructible) { println("A destroyer does not fix anything.") } override fun report() { super.report() println("Power/Attack Point: $power/$attackPoint") } } class RepairShip(name: String, maxHitPoint: Int, val power: Int) : Ship(name, maxHitPoint) { override val type = "Repair Ship" override val damage: DamageStatus get() = super.damage val attackPoint: Int get() = if (damage == DamageStatus.UNDAMAGED) 500 * power else 0 val fixPoint: Int get() = when (damage) { DamageStatus.UNDAMAGED -> 200 * power DamageStatus.SLIGHT -> 100 * power DamageStatus.MEDIUM -> 50 * power DamageStatus.SEVERE -> 20 * power DamageStatus.DESTROYED -> 0 } override fun attack(obj: Destructible) { if (destroyed) { return println("$type $name cannot attack any other.") } println("$type $name attacks!") obj.absorb(attackPoint) } override fun fix(obj: Destructible) { if (destroyed) { return println("$type $name cannot fix any other.") } println("$type $name start to fixes some destructible.") obj.restore(fixPoint) } override fun report() { super.report() println("Power/Attack Point/Fix Point: $power/$attackPoint/$fixPoint") } } class Bunker(val name: String) : Destructible, Reporter { private var new = true override val damage: DamageStatus get() = if (!new) DamageStatus.DESTROYED else DamageStatus.UNDAMAGED // Any hit > 0 will destroy the bunker override fun absorb(hit: Int) { if (hit > 0) new = false } // Any cost > 0 will repair the bunker to undamaged override fun restore(cost: Int) { if (cost > 0) new = true } override fun report() { println("=======================================") println("$name is $damage.") } } fun <T> attackAndReport(ship: Ship, obj: T) where T: Destructible, T: Reporter { ship.attack(obj) obj.report() } fun <T> fixAndReport(ship: Ship, obj: T) where T: Destructible, T: Reporter { ship.fix(obj) obj.report() } fun main() { val hibiki = Destroyer("Hibiki", 20, 9) val ikazuchi = Destroyer("Ikazuchi", 15, 8) val akashi = RepairShip("Akashi", 100, 3) val ships = listOf(hibiki, ikazuchi, akashi) ships.forEach { it.report() } println("=======================================") attackAndReport(ikazuchi, hibiki) println("=======================================") fixAndReport(akashi, hibiki) println("=======================================") attackAndReport(hibiki,akashi) val bunker = Bunker("Bunker") println("=======================================") attackAndReport(akashi, bunker) println("=======================================") attackAndReport(hibiki, ikazuchi) println("=======================================") println("Report the damage status of the ships:") ships.forEach { println("${it.type} ${it.name}: ${it.damage}") } } ``` #### Extension + We can define new member functions for existing classes. For example: + Replace line 149-157 in above example with the following code ```kotlin=149 fun <T> Ship.attackAndReport(obj: T) where T: Destructible, T: Reporter { attack(obj) obj.report() } fun <T> Ship.fixAndReport(obj: T) where T: Destructible, T: Reporter { fix(obj) obj.report() } ``` + Replace `attackAndReport(ikazuchi,hibiki)` with `ikazuchi.attackAndReport(hibiki)` and similar codes in the main function. ### Variable Number of Arguments + `vararg` keyword: functions and constructors may have variable number of arguments. + Example: `listOf(1,2,3)`, `listOf(1,2,3,4,5)` + The following code shows how to define such functions and constructors. + `vararg args: T` will make the argument `args` of type `Array<T>` ```kotlin interface Lockable<T> { var locked: Boolean fun lock(key: T) fun unlock(key: T) } // now it can lock several items! class Locker<K, T>(key: K, override var locked: Boolean, vararg item: T) : Lockable<K> { private val secretKey = key val item: MutableList<T>? = item.toMutableList() get() = if (locked) null else field override fun lock(key: K) { if (key == secretKey) locked = true } override fun unlock(key: K) { if (key == secretKey) locked = false } // to reveal the secret data override fun toString(): String { if (!locked) return "key: $secretKey, item: $item, locked: false" unlock(secretKey) val ret = "key: $secretKey, item: $item, locked: true" lock(secretKey) return ret } } fun main() { val lockerA = Locker<Int,Int>(5566, true, 7, 78, 8) // this will not unlock the locker lockerA.unlock(55667) // you cannot access the item directly println("lockerA.item: ${lockerA.item}") // you cannot add element when locked lockerA.item?.add(8899) println("lockerA:\n$lockerA") // this unlocks the locker lockerA.unlock(5566) println("lockerA.item: ${lockerA.item}") // you cannot modify the item when locked lockerA.item?.add(8899) println("lockerA:\n$lockerA") val lockerB = Locker<String, Double>("NCTU", false, 140.113, 666.666) // this will not lock the locker lockerB.lock("NTHU") // you can access the item println("lockerB.item: ${lockerB.item}") // you can add new element to item lockerB.item?.add(140.114) println("lockerB:\n$lockerB") // this will lock the locker lockerB.lock("NCTU") // you cannot access the item, since it is locked println("lockerB.item: ${lockerB.item}") // you cannot add, since it is locked lockerB.item?.add(140.112) println("lockerB:\n$lockerB") } ```