Статьи

Два способа расширить функциональность enum

Предисловие

В моей предыдущей статье я объяснил, как и почему использовать enums вместо структуры управления enums switch/case в коде Java. Здесь я покажу, как расширить функциональность существующих enums .

Вступление

Java enum — это своего рода волшебство компилятора. В байтовом коде любое enum представлено как класс, который расширяет абстрактный класс java.lang.Enum и имеет несколько статических членов. Следовательно, enum не может расширять любой другой класс или enum: нет множественного наследования.

Класс также не может расширять перечисление. Это ограничение обеспечивается компилятором.

Вот простое enum :

1
enum Color {red, green, blue}

Этот класс пытается расширить его:

1
class SubColor extends Color {}

Это результат попытки скомпилировать класс SubColor:

1
2
3
4
5
6
7
8
$ javac SubColor.java
SubColor.java:1: error: cannot inherit from final Color
class SubColor extends Color {}
                       ^
SubColor.java:1: error: enum types are not extensible
class SubColor extends Color {}
^
2 errors

Enum не может ни расширяться, ни расширяться. Итак, как можно расширить его функциональность? Ключевое слово «функциональность». Enum может реализовать методы. Например, enum Color может объявить абстрактный метод draw() и каждый член может переопределить его:

1
2
3
4
5
6
7
enum Color {
    red { @Override public void draw() { } },
    green { @Override public void draw() { } },
    blue { @Override public void draw() { } },
    ;
    public abstract void draw();
}
Популярное использование этой техники объясняется здесь. К сожалению, не всегда возможно реализовать метод в самом enum, потому что:
  1. enum может принадлежать сторонней библиотеке или другой команде в компании
  2. Перечисление, вероятно, перегружено слишком многими другими данными и функциями, поэтому оно становится нечитаемым
  3. перечисление принадлежит модулю, у которого нет зависимостей, необходимых для реализации метода draw ().

В этой статье предлагаются следующие решения этой проблемы.

Зеркальное перечисление

Мы не можем изменить enum Color? Нет проблем! Давайте создадим enum DrawableColor, который имеет те же элементы, что и Color. Это новое перечисление будет реализовывать наш метод draw ():
1
2
3
4
5
6
7
enum DrawableColor {
    red { @Override public void draw() { } },
    green { @Override public void draw() { } },
    blue { @Override public void draw() { } },
    ;
    public abstract void draw();
}

Это перечисление является своего рода отражением исходного перечисления Color , т. Е. Color является его зеркалом. Но как использовать новое перечисление? Весь наш код использует Color , а не DrawableColor . Самый простой способ реализовать этот переход — использовать встроенные методы перечисления name () и valueOf () следующим образом:

1
2
Color color = ...
DrawableColor.valueOf(color.name()).draw();

Поскольку метод name() является окончательным и не может быть переопределен, а valueOf() генерируется компилятором, эти методы всегда соответствуют друг другу, поэтому здесь не ожидается никаких функциональных проблем. Производительность такого перехода также хороша: метод name () даже не создает новую String, но возвращает предварительно инициализированный (см. Исходный код java.lang.Enum ). Метод valueOf() реализован с использованием Map, поэтому его сложность составляет O (1).

Код выше содержит очевидную проблему. Если исходное перечисление Color изменено, вторичное перечисление DrawableColor не знает этого факта, поэтому трюк с name() и valueOf() будет выполнен во время выполнения. Мы не хотим, чтобы это произошло. Но как предотвратить возможный сбой?

Мы должны дать DrawableColor знать, что его зеркало — Color, и применять его предпочтительно во время компиляции или, по крайней мере, на этапе модульного тестирования. Здесь мы предлагаем проверку во время выполнения модульных тестов. Enum может реализовать статический инициализатор, который выполняется, когда enum упоминается в любом коде. Это на самом деле означает, что если статический инициализатор проверяет, что enum DrawableColor соответствует Color, то достаточно реализовать тест, подобный следующему, чтобы быть уверенным, что код никогда не будет нарушен в производственной среде:

1
2
3
4
@Test
public void drawableColorFitsMirror {
    DrawableColor.values();
}

Статический инициализатор просто должен сравнить элементы DrawableColor и Color и выдать исключение, если они не совпадают. Этот код прост и может быть написан для каждого конкретного случая. К счастью, простая библиотека с открытым исходным кодом с именем enumus уже реализует эту функциональность, поэтому задача становится тривиальной:

1
2
3
4
5
6
enum DrawableColor {
    ....
    static {
        Mirror.of(Color.class);
    }
}

Вот и все. Тест завершится неудачей, если source enum и DrawableColor больше не будут соответствовать ему. У служебного класса Mirror есть другой метод, который получает 2 аргумента: классы 2 перечислений, которые должны соответствовать. Эта версия может быть вызвана из любого места в коде, а не только из enum, который должен быть проверен.

EnumMap

Нужно ли нам определять другое перечисление, которое просто содержит реализацию одного метода? На самом деле мы не должны. Вот альтернативное решение. Давайте определим интерфейс Drawer следующим образом:

1
2
3
public interface Drawer {
    void draw();
}

Теперь давайте создадим отображение между элементами enum и реализацией интерфейса Drawer:

1
2
3
4
5
Map<Color, Drawer> drawers = new EnumMap<>(Color.class) {{
    put(red, new Drawer() { @Override public void draw();});
    put(green, new Drawer() { @Override public void draw();})
    put(blue, new Drawer() { @Override public void draw();})
}}

Использование простое:

1
drawers.get(color).draw();

EnumMap выбран здесь как реализация Map для лучшей производительности. Map гарантирует, что каждый элемент перечисления появляется там только один раз. Однако это не гарантирует наличия записи для каждого элемента enum . Но достаточно проверить, что размер карты равен количеству элементов enum :

1
drawers.size() == Color.values().length

Enumus предлагает удобную утилиту и для этого случая. Следующий код генерирует исключение IllegalStateException с описательным сообщением, если карта не соответствует цвету:

1
EnumMapValidator.validateValues(Color.class, map, "Colors map");

Важно вызвать валидатор из кода, который выполняется модульным тестом. В этом случае решение на основе карты безопасно для будущих модификаций исходного перечисления.

Функциональный интерфейс EnumMap и Java 8

На самом деле нам не нужно определять специальный интерфейс для расширения
Перечислите функциональность. Мы можем использовать один из функциональных интерфейсов, предоставляемых JDK, начиная с версии 8 ( Function,BiFunction,Consumer,BiConsumer,
Supplieretc
Function,BiFunction,Consumer,BiConsumer,
Supplieretc
Function,BiFunction,Consumer,BiConsumer,
Supplieretc
.) Выбор зависит от параметров, которые должны быть отправлены в функцию.
Например, Supplier может использоваться вместо Drawable определенного в предыдущем примере:

1
2
3
4
5
Map<Color, Supplier<Void>> drawers = new EnumMap<>(Color.class) {{
    put(red, new Supplier<Void>() { @Override public void get();});
    put(green, new Supplier<Void>() { @Override public void get();})
    put(blue, new Supplier<Void>() { @Override public void get();})
}}

Использование этой карты очень похоже на использование в предыдущем примере:

1
drawers.get(color).get();

Эта карта может быть проверена в точности как карта, которая хранит экземпляры
Drawable.

Выводы

В этой статье показано, насколько мощными могут быть enums Java, если мы добавим немного логики. Он также демонстрирует два способа расширения функциональности enums которые работают, несмотря на языковые ограничения. Статья знакомит пользователя с библиотекой с открытым исходным кодом enumus, которая предоставляет несколько полезных утилит, которые помогают упростить работу с enums .

Опубликовано на Java Code Geeks с разрешения Александра Радзина, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Два способа расширить функциональность enum

Мнения, высказанные участниками Java Code Geeks, являются их собственными.