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

Статья из группы Random

Запечатанные (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. Только непосредственно разрешенные классы (directly permitted classes) могут расширять запечатанный класс. То есть, если 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());
}
Вывод:
Computer 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;
    }
}
Способ блокировки с двойной проверкой (Double-Checked Locking, 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 является потокобезопасным, поэтому не нужно беспокоиться о блокировке с двойной проверкой и он довольно прост в написании.
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Artem Sokolov Уровень 26, Stavropol
30 июля 2022
"Отмечу, что для корректной работы данного варианта (Double Checked Locked) реализации обязательно одно из двух условий. Переменная INSTANCE должна быть либо final, либо volatile. "
fog Уровень 17
14 июля 2022
Поправочка: Использование модификатора volatile, в примере с ленивым синглтоном, является лишним. Так как освобождение и захват монитора - действия синхронизации, и освобождение монитора synchronizes-with с любым последующим захватом, то любые действия записи, произошедшие до освобождения монитора, в том числе действия записи полей инициализируемого объекта, будут видны всем действиям чтения, происходящим после последующего захвата монитора.