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")
}
```