JavaRush /Java блог /Random UA /Кава-брейк #143. Запечатані (sealed) класи в Java 17. 4 с...

Кава-брейк #143. Запечатані (sealed) класи в Java 17. 4 способи реалізації Singleton

Стаття з групи Random UA

Запечатані (sealed) класи в Java 17

Джерело: Codippa У цій публікації ми розглянемо запечатані (sealed) класи - нову функцію, представлену в Java 17, а також способи їх оголошення та використання з прикладами. Кава-брейк #143.  Запечатані (sealed) класи в Java 17. 4 способи реалізації Singleton - 1Запечатані класи вперше з'явабося в Java 15 як функцію попереднього перегляду, а потім і в Java 16, в тому ж самому статусі. Повноцінною ця функція стала з виходом Java 17 ( JEP 409 ).

Що таке запечатані класи?

Запечатаний (sealed) клас дозволяє обмежувати чи вибирати підкласи. Клас не може розширювати закритий клас, якщо його немає у списку дозволених дочірніх класів батьківського класу. Клас запечатується за допомогою ключового слова sealed . За запечатаним класом слід слідувати ключове слово permits разом зі списком класів, які можуть його розширити. Ось приклад:
public sealed class Device permits Computer, Mobile {
}
Ця декларація означає, що Device може бути розширений лише класами Computer та Mobile . Якщо інший клас спробує його розширити, з'явиться помилка компілятора. Клас, який розширює запечатаний клас, повинен мати у своїй декларації ключове слово final , sealed або non-sealed . Таким чином, у нас є фіксована ієрархія класів. Як це пов'язано із створенням дочірнього класу?
  1. final означає, що він може бути далі підкласифікований.

  2. sealed означає, що нам потрібно оголосити дочірні класи з permits .

  3. non-sealed означає, що тут ми закінчуємо ієрархію parent-child (батьківсько-дочірній).

Наприклад, Computer дозволяє (permits) класи Laptop і Desktop , поки Laptop сам залишається non-sealed . Це означає, що Laptop може бути розширений такими класами, як Apple , Dell , HP і таке інше.

Основні цілі запровадження запечатаних класів:

  1. До цих пір ви могли обмежити розширення класу лише за допомогою ключового слова final . Запечатаний клас контролює, які класи можуть його розширювати, включаючи в дозволений список.

  2. Також це дозволяє класу контролювати, які їх будуть його дочірніми класами.

Правила

Декілька правил, які потрібно пам'ятати при використанні запечатаних класів:
  1. Запечатаний клас повинен визначати класи, які можуть розширювати його за допомогою permits . Цього не потрібно, якщо дочірні класи визначені всередині батьківського класу як внутрішній клас.

  2. Дочірній клас повинен бути або final , sealed або non-sealed .

  3. Дозволений дочірній клас (permitted child class) має розширювати батьківський запечатаний клас.

    Тобто якщо запечатаний клас A допускає клас B, B повинен розширити A.

  4. Якщо запечатаний клас знаходиться в модулі, то дочірні класи повинні бути в тому ж модулі або в тому ж пакеті, якщо батьківський запечатаний клас знаходиться в безіменному модулі.

  5. Тільки безпосередньо дозволені класи можуть розширювати запечатаний клас. Тобто, якщо A є запечатаним класом, який дозволяє B розширювати його, B також є запечатаним класом, який дозволяє C.

    Тоді C може лише розширювати B, але може безпосередньо розширювати A.

Запечатані інтерфейси

Подібно до запечатаних класів, інтерфейси також можуть бути запечатані. Такий інтерфейс може дозволити вибирати свої дочірні інтерфейси чи класи, які можуть розширювати його за допомогою permits . Ось наочний приклад:
public sealed interface Device permits Electronic, Physical,
DeviceImpl {
}
Тут інтерфейс Device дозволяє інтерфейсам Electronic та Physical розширювати його та клас DeviceImpl для подальшої реалізації.

Запечатані записи

Задруковані класи можна використовувати із записами, представленими в Java 16. Запис не може розширювати звичайний клас, тому він може реалізувати лише закритий інтерфейс. Крім того, запис має на увазі наявність final . Таким чином, запис не може використовувати ключове слово permits , оскільки його не можна розділити на підкласи. Тобто існує лише однорівнева ієрархія із записами. Ось приклад:
public sealed interface Device permits Laptop {
}
public record Laptop(String brand) implement Device {
}

Підтримка Reflection

Java Reflection забезпечує підтримку запечатаних класів. Наступні два методи були додані в java.lang.Class :

1. getPermittedSubclasses()

Тут повертається масив java.lang.Class , що містить усі класи, дозволені цим об'єктом класу. Приклад:
Device c = new Device();
Class<? extends Device> cz = c.getClass();
Class<?>[] permittedSubclasses = cz.getPermittedSubclasses();
for (Class<?> sc : permittedSubclasses){
  System.out.println(sc.getName());
}
Висновок:
Комп'ютер Mobile

2. isSealed()

Тут повертається true якщо клас або інтерфейс, в якому він викликається, запечатаний. Це поки що все про запечатані (sealed) класи, додані в Java 17. Сподіваюся, стаття була інформативною.

4 способи реалізації Singleton

Джерело: Medium Сьогодні ви дізнаєтесь про декілька способів реалізації шаблону проектування Singleton. Шаблон проектування Singleton широко використовується у Java-проектах. Він забезпечує контроль доступу до ресурсів, наприклад до сокету або з'єднання з базою даних. Якось мене попросабо реалізувати синглтон під час співбесіди на посаду веб-розробника у велику компанію з виробництва чіпів. Це був мій перший раз, коли я проходив співбесіду на веб-позицію, і я особливо не готувався, тому вибрав найскладніше рішення: ліниве створення екземплярів (Lazy instantiation). Мій код був правильним лише на 90% і недостатньо ефективний, у підсумку я програв у фінальному раунді… Тож, сподіваюся, моя стаття буде вам корисною.

Раннє створення екземпляра

class Singleton {
    private Singleton() {}
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }
}
Оскільки об'єкт вже створений при ініціалізації, тут немає проблеми з безпекою потоку, але він витрачає ресурси пам'яті, якщо його ніхто не використовує.

Лінива реалізація

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
При використанні лінивого шаблону ініціалізації об'єкт створюється на вимогу. Однак цей спосіб має проблему з потокобезпекою: якщо два потоки запускаються в рядку 5 одночасно, то вони створять два екземпляри Singleton. Щоб цього уникнути, нам потрібно додати блокування:
class Singleton {
    private Singleton() {}
    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
Спосіб блокування з подвійною перевіркою (DCL): у рядку 6 немає блокування, тому цей рядок буде працювати дуже швидко, якщо об'єкт вже створений. Чому нам потрібно перевірити ще раз instance == null ? Тому що, можливо, є два потоки, введені в рядок 7: перший ініціював об'єкт, другий чекає на блокування Singleton.class . Якщо перевірки немає, тоді другий потік знову відтворюватиме об'єкт singleton. Тим не менш, цей метод, як і раніше, небезпечний для потоків. Рядок 9 може бути розділений на три рядки байтового коду:
  1. Виділити пам'ять (Allocate memory).
  2. Ініціювати об'єкт (Init object).
  3. Призначити об'єкт посилання екземпляра (Assign object to instance reference).
Оскільки JVM може працювати не по порядку, віртуальна машина може перед ініціалізацією присвоїти об'єкт на екземпляр. Інший потік вже бачить екземпляр != null , він почне його використовувати та викличе проблему. Тому нам потрібно додати volatile в екземпляр, тоді код стане таким:
class Singleton {
    private Singleton() {}
    private volatile static Singleton instance = null;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Використання статичного внутрішнього класу

public class Singleton {
  private Singleton() {}
  private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  public static final Singleton getInstance() {
    return SingletonHolder.INSTANCE;
  }
}
SingletonHolder — статичний внутрішній клас, він ініціалізується лише за виклику методу getInstance . Клас ініціалізації в JVM запустить <clinit> cmd , потім сама JVM подбає про те, щоб тільки один потік міг викликати <clinit> для цільового класу, інші потоки чекатимуть.

Enum у вигляді синглтона

public enum EnumSingleton {
    INSTANCE;
    int value;
    public int getValue() {
        return value;
    }
    public int setValue(int v) {
        this.value = v;
    }
}
За замовчуванням екземпляр enum є потокобезпечним, тому не потрібно турбуватися про блокування з подвійною перевіркою і досить простий у написанні.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ