Kotlin by lazy throws NullPointerException

Viewed 937

I am currently trying to learn Kotlin with the help of the book "Kotlin Programming The Big Nerd Ranch Guide" and so far everything worked. But now I am struggling with the "lazy" initialization which throws a NullPointerException which says

Cannot invoke "kotlin.Lazy.getValue()" because "< local1>" is null

The corresponding lines are:

val hometown by lazy { selectHometown() } 
private fun selectHometown(): String = File("data/towns.txt").readText().split("\n").shuffled().first()

In case you want to compile it yourself or need more code for a better understanding I provide the Game.kt and Player.kt down below. If "lazy" is dropped for a "normal" initialization the hometown gets assigned as intended. Any tips for solving the problem and understanding the cause of it is welcome.

// Game.kt
package com.bignerdranch.nyethack

fun main(args: Array<String>) {

    val player = Player("Madrigal")
    player.castFireball()
}

private fun printPlayerStatus(player: Player) {
    println("(Aura: ${player.auraColor()}) " + "(Blessed: ${if (player.isBlessed) "YES" else "NO"})")
    println("${player.name} ${player.formatHealthStatus()}")
}
// Player.kt
package com.bignerdranch.nyethack

import java.io.File

class Player(_name: String, var healthPoints: Int = 100, val isBlessed: Boolean, private val isImmortal: Boolean) {


    var name = _name
        get() = "${field.capitalize()} of $hometown"
        private set(value) {
            field = value.trim()
        }

    constructor(name: String) : this(name, isBlessed = true, isImmortal = false) {
        if (name.toLowerCase() == "kar") healthPoints = 40
    }

    init {
        require(healthPoints > 0, { "healthPoints must be greater than zero." })
        require(name.isNotBlank(), { "Player must have a name" })
    }

    val hometown by lazy { selectHometown() }

    private fun selectHometown(): String = File("data/towns.txt").readText().split("\n").shuffled().first()

    fun castFireball(numFireballs: Int = 2) =
        println("A glass of Fireball springs into existence. (x$numFireballs)")


    fun auraColor(): String {
        val auraVisible = isBlessed && healthPoints > 60 || isImmortal
        return if (auraVisible) "GREEN" else "NONE"
    }
    fun formatHealthStatus() =
        when (healthPoints) {
            100 -> "is an excellent condition!"
            in 90..99 -> "has a few scratches."
            in 75..89 -> if (isBlessed) {
                "has some minor wounds but is healing quite quickly"
            } else {
                "has some minor wounds"
            }
            in 15..74 -> "looks pretty hurt"
            else -> "is in awful condition!"
        }

}

I forgot the towns.txt so here it is (not that it matters much)

Neversummer
Abelhaven
Phandoril
Tampa
Sanorith
Trell
Zan'tro
Hermi Hermi
Curlthistle Forest
1 Answers

When something like this happens, it's usually due to bad ordering of initialization.

The initialization of the Player class goes this way:

  1. the name property has its backing field initialized with the _name value
  2. the init block is run, and tries to access name
  3. the getter of name tries to read the hometown property, but fails because hometown is still not initialized
  4. ...if things had gone right, the hometown property would be initialized now with the lazy delegate

So basically you're trying to access hometown before the lazy delegate is configured. If you move hometown's declaration above the init block, you should be fine.

You can see the fix in action on the playground

Related