зачем использовать обычный val в не финальных классах

Если класс не является окончательным, он может быть расширен.

Есть две возможности для значений: оно может быть переопределено и должно быть ленивым, оно не может быть переопределено и должно быть окончательным.

Если val является окончательным - вы можете предположить, что все вычисления над ним будут работать через иерархию классов. Если val может быть переопределен, вы должны объявить его ленивым, чтобы он не сломался после расширения. Вы можете оставить val равным, и это не дает никаких гарантий, что он будет продлен в правильном направлении.

Какие варианты использования подразумевают использование простых значений?

Пример сбоя инициализации класса без ленивых значений

abstract class A {
  lazy val x1 : String = throw new Exception()
  val x2 : String = "mom"
  val x3 : String = x1 + ", " + x2
  println("A: " + x3)
}
class B extends A {
  override lazy val x1: String = "hello"
  println("B: " + x3)
}
class C extends B {
  override val x2: String = "dad"
  println("C: " + x3)
}

тестирую это:

scala> new B
A: hello, mom
B: hello, mom
res8: B = B@7e2bd615

это работает, но дальнейшее субклассирование сломало уже существующий функционал

scala> new C
A: hello, null
B: hello, null
C: hello, null
res5: C = C@52a53948

установка lazy на x2 исправляет случай:

abstract class A {
  lazy val x1 : String = throw new Exception()
  lazy val x2 : String = "mom"
  val x3 : String = x1 + ", " + x2
  println("A: " + x3)
}
class B extends A {
  override lazy val x1: String = "hello"
  println("B: " + x3)
}
class C extends B {
  override lazy val x2: String = "dad"
  println("C: " + x3)
}

правильный порядок инициализации:

scala> new C
A: hello, dad
B: hello, dad
C: hello, dad
res6: C = C@5e970110

Ответы на вопрос(1)

Ваш ответ на вопрос