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