Kotlin Programming 2020 Fall - Lecture 8 === ###### tags: `Kotlin` This lecture intends to cover the materials in Chapter 13. ## Constructors (ctor) A constructor is like a function that initializes the instance. ### Primary Constructors + The arguments of the primary constructor is defined after the class name. + The primary constructor runs `init` blocks. ```kotlin class Person(_name: String, _height: Double, _weight: Double) { val name: String // defined in init block var height = _height // height cannot be negative set(value) { if (value < 0.0) field = 0.0 else field = value } var weight = _weight // weight cannot be negative set(value) { field = maxOf(0.0, value) } val bmi: Double // computed property get() = weight / (height / 100).let { it * it } init { name = _name.split(" ") .filter { it.length > 0 } .map { it.capitalize() } .joinToString(" ") require(name.isNotBlank(), { "Person must have a name." }) } fun showProfile() { println("Name: $name") println("Height: $height cm") println("Weight: $weight kg") println("BMI: $bmi") } } fun main() { val mz = Person("min-zheng shieh", 177.5, 112.55) val doraemon = Person("Doraemon", 129.3, 129.3) mz.showProfile() doraemon.showProfile() // BMI: Divided by zero? val zeroHeight = Person("zero height", 0.0, 45.6) zeroHeight.showProfile() // Don't we have a setter to ensure that weight are non-negative? val negWeight = Person("negative weight", 123.4, -45.6) negWeight.showProfile() // the following crash! val crasher = Person(" ", 123.0, 456.0) crasher.showProfile() } ``` #### Define Properties in a Primary Constructor If your class have some properties that use default getters and setters, then you can define them in the primary constructor of the class. ```kotlin import kotlin.random.Random import kotlin.random.nextInt class PlayableCharacter(_name: String, val maxHP: Int, val maxMP: Int) { var hp = maxHP var mp = maxMP var atk = Random.nextInt(1..15) var def = Random.nextInt(1..15) val name: String = _name.split(" ") .filter { it.length > 0 } .map { it.capitalize() } .joinToString(" ") .let { if (it == "") "Anonymous" else it } init { require(name.isNotBlank(), { "Person must have a name." }) } fun showStatus() { println("Name: $name") println("HP: $hp/$maxHP") println("MP: $mp/$maxMP") println("ATK: $atk DEF: $def") } } fun main() { val mz = PlayableCharacter("min zheng", 100, 100) mz.showStatus() val anonymous = PlayableCharacter(" ", 5, 1) anonymous.showStatus() } ``` ### Secondary Constructors Kotlin allows you to have secondary constructors to create instances to fit your need. But they must call the primary constructor first. ```kotlin import kotlin.random.Random import kotlin.random.nextInt class PlayableCharacter(_name: String, val maxHP: Int, val maxMP: Int) { var hp = maxHP var mp = maxMP var atk = Random.nextInt(1..15) var def = Random.nextInt(1..15) val name: String = _name.split(" ") .filter { it.length > 0 } .map { it.capitalize() } .joinToString(" ") .let { if (it == "") "Anonymous" else it } init { require(name.isNotBlank(), { "Person must have a name." }) } fun showStatus() { println("Name: $name") println("HP: $hp/$maxHP") println("MP: $mp/$maxMP") println("ATK: $atk DEF: $def") } constructor(_name: String, _maxHP: Int, _maxMP: Int, _atk: Int, _def: Int) : this(_name, _maxHP, _maxMP) { println("Using 2nd constructor...") println("ATK was initialized to $atk. Reassign to $_atk") atk = _atk println("DEF was initialized to $def. Reassign to $_def") def = _def } } fun main() { val mz = PlayableCharacter("min zheng", 112, 10) mz.showStatus() val truck = PlayableCharacter("truck ski", 87, 17, 15, 1) truck.showStatus() } ``` ### Default Arguments and Named Arguments + Similar to the default arguments and named arguments for functions. + Use of `this` 1. Call the primary constructor 2. Access the properties of the instance. This allow us to name a argument of contructor with the same name of a property. ```kotlin import kotlin.random.Random import kotlin.random.nextInt class PlayableCharacter(name: String, val maxHP: Int, val maxMP: Int) { var hp = maxHP var mp = maxMP var atk = Random.nextInt(1..15) var def = Random.nextInt(1..15) val name: String = name.split(" ") .filter { it.length > 0 } .map { it.capitalize() } .joinToString(" ") .let { if (it == "") "Anonymous" else it } init { require(name.isNotBlank(), { "Person must have a name." }) } fun showStatus() { println("Name: $name") println("HP: $hp/$maxHP") println("MP: $mp/$maxMP") println("ATK: $atk DEF: $def") } constructor(name: String, maxHP: Int = 100, maxMP: Int = 12, atk: Int = 8, def: Int = 8) : this( name, maxHP, maxMP ) { println("Using 2nd constructor...") println("ATK was initialized to ${this.atk}. Reassign to $atk") this.atk = atk println("DEF was initialized to ${this.def}. Reassign to $def") this.def = def } } fun main() { val mz = PlayableCharacter("min zheng", 112, 10) mz.showStatus() val truck = PlayableCharacter("truck ski", 87, 17, 15, 1) truck.showStatus() val ordinary = PlayableCharacter("odin ary") ordinary.showStatus() // what does it mean? val ambiguity = PlayableCharacter("ambee guity", 111, 11) ambiguity.showStatus() // Use named argument cannot avoid ambiguity val yetAmbiguity = PlayableCharacter("yet ambiguity", maxHP = 111, maxMP = 11) yetAmbiguity.showStatus() // Use named argument val attacker = PlayableCharacter("Atta ker", atk = 1000) attacker.showStatus() } ``` ### Property Initialization You must initialize all properties. #### Initialization Order 1. Primary constructor inline property 2. Class-level property assignments 3. `init` block 4. Secondary constructor (Since it calls the primary constructor, its code will be executed after the primary constructor.) 5. Delay initialization ### Delaying Initialization Kotlin allows delaying initialization: the properties can be initialized after the constructor executed. #### Late Initialization + For `var` only + In some situation, we cannot finish the initialization of properties with non-null values. ```kotlin class Vehicle(val type: String) { var horn: Horn? = null fun honk() { println("$type: ${horn?.sound}") } constructor(type: String, horn: Horn) : this(type) { this.horn = horn horn.vehicle = this } } class Horn(val sound: String) { var vehicle: Vehicle? = null constructor(sound: String, vehicle: Vehicle) : this(sound) { this.vehicle = vehicle vehicle.horn = this } } fun main() { val truck = Vehicle("truck") val bahHorn = Horn("Bah~~", truck) truck.honk() bahHorn.vehicle?.honk() val bubuHorn = Horn("Bu~bu~") val bus = Vehicle("bus", bubuHorn) bus.honk() bubuHorn.vehicle?.honk() val car = Vehicle("car") car.honk() val silentHorn = Horn("...") silentHorn.vehicle?.honk() } ``` + You have to deal a lot of nullable variable, which might make your code harder to read. ```kotlin class Vehicle(val type: String) { lateinit var horn: Horn fun honk() { println("$type: ${horn.sound}") } val hornIsInitialized: Boolean get() = ::horn.isInitialized constructor(type: String, horn: Horn) : this(type) { this.horn = horn horn.vehicle = this } } class Horn(val sound: String) { lateinit var vehicle: Vehicle val vehicleIsInitialized: Boolean get() = ::vehicle.isInitialized constructor(sound: String, vehicle: Vehicle) : this(sound) { this.vehicle = vehicle vehicle.horn = this } } fun main() { val truck = Vehicle("truck") val bahHorn = Horn("Bah~~", truck) truck.honk() bahHorn.vehicle.honk() val bubuHorn = Horn("Bu~bu~") val bus = Vehicle("bus", bubuHorn) bus.honk() bubuHorn.vehicle.honk() // crash!! because you didn't attach a horn to the car! val car = Vehicle("car") println("car.horn is initialized: ${car.hornIsInitialized}") car.honk() // crash!! because you didn't attach silentHorn to any vehicle val silentHorn = Horn("...") println("silentHorn.vehicle is initialized: ${silentHorn.vehicleIsInitialized}") silentHorn.vehicle.honk() } ``` #### Lazy Initialization + For `val` only + Similar to computed properties, but it save the value for future access. ```kotlin class Person(name: String, height: Double, weight: Double) { val name = name.split(" ") .filter { it.length > 0 } .map { it.capitalize() } .joinToString(" ") var height = maxOf(0.0,height) // height cannot be negative set(value) { if (value < 0.0) field = 0.0 else field = value } var weight = maxOf(0.0,weight) // weight cannot be negative set(value) { field = maxOf(0.0, value) } val bmi: Double // computed property get() = weight / (height / 100).let { it * it } val lazyBMI by lazy { println("OK, I am evaluating Lazy BMI now.") weight / (height / 100).let { it * it } } init { require(name.isNotBlank(), { "Person must have a name." }) println("The initialization is done.") } fun showProfile() { println("Name: $name") println("Height: $height cm") println("Weight: $weight kg") println("Computed BMI: $bmi") println("Lazy BMI: $lazyBMI") } } fun main() { val mz = Person("min-zheng shieh", 177.5, 112.55) mz.showProfile() // mz gets thinner println("Now, ${mz.name} loses some weight.") mz.weight -= 2 mz.showProfile() } ```