Что такое ленивая инициализация (Lazy Initialization)?

Ответ

Ленивая инициализация — это паттерн отложенной инициализации, при котором ресурсоёмкий объект создаётся не в момент загрузки класса или создания владеющего объекта, а в момент первого обращения к нему. Цель — оптимизация использования памяти и ускорение старта приложения.

Типичные сценарии использования:

  1. Создание «тяжёлых» зависимостей (соединения с БД, клиенты внешних сервисов).
  2. Реализация паттерна Singleton.
  3. Настройка отношений @ManyToOne/@OneToOne в JPA (fetch = FetchType.LAZY).

Примеры реализации в Java:

1. Простая (не потокобезопасная) версия:

public class LazyResourceHolder {
    private ExpensiveResource resource = null;

    public ExpensiveResource getResource() {
        if (resource == null) {
            resource = new ExpensiveResource(); // Создание при первом вызове
        }
        return resource;
    }
}

2. Потокобезопасная версия с «удерживателем» (Initialization-on-demand holder idiom): Наиболее эффективный и безопасный способ для Singleton.

public class Singleton {
    // Приватный конструктор
    private Singleton() {}

    // Статический внутренний класс содержит экземпляр
    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE; // Загрузка класса Holder и инициализация INSTANCE
                                // происходит только здесь (лениво и потокобезопасно)
    }
}

3. С использованием Supplier (Java 8+):

public class Lazy<T> {
    private Supplier<T> supplier;
    private T value;

    public Lazy(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T get() {
        if (value == null) {
            value = supplier.get();
        }
        return value;
    }
}
// Использование
Lazy<ExpensiveResource> lazyResource = new Lazy<>(() -> new ExpensiveResource());
// ...
ExpensiveResource res = lazyResource.get(); // Создаётся здесь

Плюсы: Экономия памяти, ускорение инициализации. Минусы: Усложнение кода, необходимость контроля потокобезопасности, риск ошибок NullPointerException.

Ответ 18+ 🔞

А, ленивая инициализация! Ну это ж классика, блядь, как Герасим с Муму. Всё откладываем на потом, пока жопа не загорится, сука.

Смотри, в чём прикол. Представь, у тебя есть объект, который создаётся, как ёперный театр — долго и ресурсов жрёт, как не в себя. Соединение с базой, какой-нибудь тяжёлый клиент API. И зачем его создавать сразу, когда приложение стартует, если он, возможно, вообще не понадобится? Вот тут и приходит наш герой — ленивый чувак, который говорит: «Да похуй, создам, когда спросят».

Как это выглядит в коде, если делать по-простому, на коленке:

public class LazyResourceHolder {
    private ExpensiveResource resource = null; // Пока пусто, блядь, как в моей голове по утрам.

    public ExpensiveResource getResource() {
        if (resource == null) { // А если никто не спрашивал?
            resource = new ExpensiveResource(); // Вот щас, блядь, на тебе! Создаём в первый и последний раз.
        }
        return resource;
    }
}

Но стоп, ёпта! А если с двух потоков одновременно придут и спросят? Оба увидят null, оба начнут создавать этот тяжёлый объект, и получится у нас две штуки, а надо одну. Пиздец, а не оптимизация. Нас ждёт race condition, волнение ебать, терпения ноль.

Поэтому умные дядьки придумали хитрую жопу — Initialization-on-demand holder idiom. Звучит, как диагноз, но работает, блядь, на ура. Особенно для синглтонов.

public class Singleton {
    private Singleton() {} // Конструктор спрятали, чтобы не выёбывались.

    // А вот тут магия, в рот меня чих-пых!
    private static class Holder {
        static final Singleton INSTANCE = new Singleton(); // Создаётся внутри статического класса.
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE; // В этот момент JVM загружает класс Holder и инициализирует INSTANCE.
                                // И делает она это лениво, безопасно и один раз, как мой дед пасху святил.
    }
}

Красота, блядь! Потокобезопасно за счёт ClassLoader, и лениво. Пока не вызовешь getInstance() — нихуя не происходит.

Ну а если насовременней, с Java 8, то можно через Supplier:

public class Lazy<T> {
    private Supplier<T> supplier; // Поставщик нашего добра.
    private T value; // А вот где оно будет храниться.

    public Lazy(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T get() {
        if (value == null) {
            value = supplier.get(); // Тыкаем в поставщика, он выдаёт товар.
        }
        return value;
    }
}
// Использование
Lazy<ExpensiveResource> lazyResource = new Lazy<>(() -> new ExpensiveResource());
// ...
ExpensiveResource res = lazyResource.get(); // Вот тут-то всё и рождается, ебанашка!

Итоги, блядь:

  • Плюсы: Памяти меньше жрём, стартуем быстрее. Не создаём то, что не нужно. Ядрёна вошь!
  • Минусы: Код чуть сложнее. Надо думать про потоки, а то будет, как у Герасима — хотел как лучше, а получил... ну ты понял. И если напортачить, можно нарваться на NullPointerException в самый неподходящий момент.

Короче, инструмент охуенный, но без фанатизма. Как и всё в этой жизни, блядь.