Как работает ленивое вычисление (lazy val)?
В Scala модификатор lazy используется для отложенной инициализации значения. Это означает, что выражение, присвоенное переменной lazy val, не вычисляется при создании переменной, а только при первом доступе к ней. После вычисления результат кэшируется, и при следующих обращениях используется уже сохранённое значение, а не пересчитывается заново.
Синтаксис
lazy val x = {
println("Вычисление x")
42
}
println("Перед обращением к x")
println(x) // произойдёт вычисление
println(x) // уже не вычисляется, используется кэш
Вывод:
Перед обращением к x
Вычисление x
42
42
Принцип работы
-
lazy val реализуется как thread-safe (безопасный для многопоточности) механизм.
-
При первом обращении происходит вычисление и результат сохраняется.
-
При последующих обращениях возвращается уже сохранённое значение.
-
Значение вычисляется ровно один раз.
Применение lazy val
1. Отложенные тяжёлые вычисления
Если инициализация значения требует значительных затрат, но может не понадобиться, lazy помогает избежать лишней работы:
lazy val heavyComputation = computeSomethingBig()
2. Циклические зависимости
В случаях, когда один объект ссылается на другой, и есть обратная ссылка, lazy val помогает избежать NullPointerException или проблем с порядком инициализации:
class A {
lazy val b = new B(this)
}
class B(a: A)
3. Загрузка ресурсов по требованию
Если данные или файлы загружаются только при необходимости:
lazy val config: Map\[String, String\] = loadConfigFile("app.conf")
Поведение при ошибках
Если при первом вычислении lazy val произошла ошибка (например, исключение), значение не будет кэшироваться, и при следующем обращении попытка вычисления повторится:
lazy val failing = {
println("Попытка")
throw new RuntimeException("Ошибка")
}
try failing catch { case \_: Throwable => () }
try failing catch { case \_: Throwable => () }
Вывод:
Попытка
Попытка
Это отличается от поведения обычных val, которые инициализируются при создании и могут упасть немедленно.
Отличия от val
val | lazy val | |
---|---|---|
Инициализация | Немедленно | При первом обращении |
--- | --- | --- |
Вычисление | Всегда выполняется | Только при необходимости |
--- | --- | --- |
Кэш | Да | Да |
--- | --- | --- |
Потокобезопасность | Нет гарантии | Гарантирована (с lock'ом) |
--- | --- | --- |
Подходит для тяжёлых вычислений | Нет | Да |
--- | --- | --- |
Потокобезопасность и производительность
-
В Scala реализация lazy val использует блокировку (synchronized) для обеспечения безопасности при первом доступе из нескольких потоков.
-
Это делает lazy val безопасным, но дорогим по сравнению с обычным val.
-
Если многопоточность не важна, можно использовать сторонние оптимизации (@volatile var, memoization, java.util.concurrent и др.).
Ограничения
-
Нельзя использовать lazy val в качестве параметра конструктора.
-
Нельзя использовать lazy val внутри case class для сравнения (equals/hashCode), т.к. val вычисляется немедленно, а lazy val — отложено.
Примеры использования
Пример 1: отложенная инициализация ресурса
class Database {
lazy val connection = {
println("Устанавливаем соединение с БД")
new Connection("url", "user", "pass")
}
}
Пример 2: кэширование результата
def expensive(): Int = {
println("Считаем")
100
}
lazy val cached = expensive()
Пример 3: ленивое чтение из файла
lazy val content: String = scala.io.Source.fromFile("big.txt").mkString
Техническая реализация
Под капотом lazy val реализован через двойную проверку блокировки (double-checked locking). При первом обращении:
-
Проверяется, инициализировано ли значение.
-
Если нет — поток синхронизируется.
-
Внутри критической секции проверка повторяется.
-
Производится инициализация и сохраняется результат.
Совместное использование с def и val
Ключевое слово | Описание |
---|---|
val | Немедленно вычисляется, одно значение |
--- | --- |
def | Функция, вызывается каждый раз заново |
--- | --- |
lazy val | Вычисляется один раз, при первом обращении |
--- | --- |
Пример:
val now1 = System.currentTimeMillis()
def now2 = System.currentTimeMillis()
lazy val now3 = System.currentTimeMillis()
println(now1)
println(now2)
println(now3)
Thread.sleep(1000)
println(now1)
println(now2)
println(now3)
Вывод покажет разницу во времени между val, def и lazy val.