- Enum Tricks: Two Ways to Extend Enum Functionality
- Introduction
- Mirror Enum
- EnumMap
- EnumMap and Java 8 Functional Interface
- Conclusion
- Перечисления в Java (java enum)
- Конструкция enum
- Перечисление — это класс
- Элементы перечисления — экземпляры enum -класса, доступные статически
- Название и порядковый номер элемента enum
- Декомпилированный enum-class с наследованием
- Перечисления и параметрический полиморфизм
Enum Tricks: Two Ways to Extend Enum Functionality
Join the DZone community and get the full member experience.
In my previous article, I explained how and why to use enums instead of the switch / case control structure in Java code. Here, I will demonstrate how to extend the functionality of existing enums .
Introduction
Java enum is a kind of a compiler magic. In byte code, any enum is represented as a class that extends the abstract class java.lang.Enum and has several static members. Therefore, enum cannot extend any other class or enum : there is no multiple inheritance.
Class cannot extend enum , as well. This limitation is enforced by the compiler.
This class tries to extend it:
class SubColor extends Color <>
This is the result of an attempt to compile class SubColor :
$ 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 cannot either extend or be extended. So, how is it possible to extend its functionality? The key word is «functionality.» Enum can implement methods. For example, enumColor may declare abstract method draw() and each member can override it:
enum Color < red < @Override public void draw() < >>, green < @Override public void draw() < >>, blue < @Override public void draw() < >>, ; public abstract void draw(); >
Popular usage of this technique is explained here. Unfortunately, it is not always possible to implement method in enum itself because:
- the enum may belong to a third-party library or another team in the company
- the enum is probably overloaded with other data and functions, so it becomes unreadable
- the enum belongs to a module that does not have dependencies required for implementation of method draw() .
This article suggests the following solutions for this problem.
Mirror Enum
We cannot modify enumColor ? No problem! Let’s create enumDrawableColor that has exactly the same elements as Color . This new enum will implement our method draw() :
enum DrawableColor < red < @Override public void draw() < >>, green < @Override public void draw() < >>, blue < @Override public void draw() < >>, ; public abstract void draw(); >
This enum is a kind of reflection of source enum Color , i.e. Color is its mirror.
But how doe we use the new enum ? All our code uses Color , not DrawableColor . The simplest way to implement this transition is using built-in enum methods name() and valueOf() as following:
Color color = . DrawableColor.valueOf(color.name()).draw();
Since name() method is final and cannot be overridden, and valueOf() is generated by a compiler. These methods are always a good fit for each other, so no functional problems are expected here. Performance of such transition is good also: method name() does not create a new String but returns a pre-initialized one (see source code of java.lang.Enum ). Method valueOf() is implemented using Map , so its complexity is O(1).
The code above contains obvious problem. If source enumColor is changed, the secondary enumDrawableColor does not know this fact, so the trick with name() and valueOf() will fail at runtime. We do not want this to happen. But how to prevent possible failure? We have to let DrawableColor know that its mirror is Color and enforce this preferably at compile time or at least at unit test phase. Here, we suggest validation during unit tests execution. Enum can implement a static initializer that is executed when enum is mentioned in any code. This actually means that if static initializer validates that enumDrawableColor fits Color , it is enough to implement a test like the following to be sure that the code will be never broken in production environment:
@Test public void drawableColorFitsMirror
Static initializer just has to compare elements of DrawableColor and Color and throw an exception if they do not match. This code is simple and can be written for each particular case. Fortunately, a simple open-source library named enumus already implements this functionality, so the task becomes trivial:
That’s it. The test will fail if source enum and DrawableColor do not fit it any more. Utility class Mirror has other methods that gets two arguments: classes of two enums that have to fit. This version can be called from any place in code and not only from enum that has to be validated.
EnumMap
Do we really have to define another enum that just holds implementation of one method? In fact, we do not have to. Here is an alternative solution. Let’s define interface Drawer as following:
The next examples assume that all elements of enum Color are statically imported.
Now, let’s create mapping between enum elements and implementation of interface Drawer :
Map drawers = new EnumMap<>(Color.class) >); put(green, new Drawer() < @Override public void draw()<>>) put(blue, new Drawer() < @Override public void draw()<>>) >>
EnumMap is chosen here as a Map implementation for better performance. Map guaranties that each enum element appears there only once. However, it does not guarantee that there is entry for each enum element. But it is enough to check that size of the map is equal to number of enum elements:
drawers.size() == Color.values().length
Enumus suggests convenient utility for this case also. The following code throws IllegalStateException with descriptive message if map does not fit Color :
EnumMapValidator.validateValues(Color.class, map, "Colors map");
It is important to call the validator from the code which is executed by unit test. In this case the map based solution is safe for future modifications of source enum.
EnumMap and Java 8 Functional Interface
In fact, we do not have to define special interface to extend enum functionality. We can use one of functional interfaces provided by JDK starting from version 8 ( Function , BiFunction , Consumer , BiConsumer , Supplier , etc.) The choice depends on parameters that have to be sent to the function. For example, Supplier can be used instead of Drawable defined in the previous example:
Map> drawers = new EnumMap>(Color.class) () < @Override public Void get() >); put(green, new Supplier() < @Override public Void get() >); put(blue, new Supplier() < @Override public Void get() >); >>
The previous code snippet can be simplified:
Map> drawers = new EnumMap>(Color.class) null); put(green, () -> null); put(blue, () -> null); >>;
Usage of this map is pretty similar to one from the previous example:
This map can be validated exactly as the map that stores instances of Drawable .
Conclusion
This article shows how powerful Java enums can be if we put some logic inside. It also demonstrates two ways to expand the functionality of enums that work despite the language limitations. The article introduces to user the open-source library named Enumus that provides several useful utilities that help to operate enums easier.
Opinions expressed by DZone contributors are their own.
Перечисления в Java (java enum)
Программируя мы часто сталкиваемся с необходимостью ограничить множество допустимых значений для некоторого типа данных. Так, например, день недели может иметь 7 разных значений, месяц в году – 12, а время года – 4. Для решения подобных задач во многих языках программирования со статической типизацией предусмотрен специальный тип данных – перечисление ( enum ). В Java перечисление появилось не сразу. Специализированная языковая конструкция enum была введена начиная с версии 1.5. До этого момента программисты использовали другие методы для реализации перечислений.
Конструкция enum
Season season = Season.SPRING; if (season == Season.SPRING) season = Season.SUMMER; System.out.println(season);
В результате выполнения которого на консоль будет выведено SUMMER. Думаю, что пример очевиден и в пояснениях не нуждается.
Перечисление — это класс
Объявляя enum мы неявно создаем класс производный от java.lang.Enum . Условно конструкция enum Season < . >эквивалентна class Season extends java.lang.Enum < . >. И хотя явным образом наследоваться от java.lang.Enum нам не позволяет компилятор, все же в том, что enum наследуется, легко убедиться с помощью reflection :
System.out.println(Season.class.getSuperclass());
Собственно наследование за нас автоматически выполняет компилятор Java. Далее давайте условимся называть класс, созданный компилятором для реализации перечисления — enum -классом, а возможные значения перечисляемого типа — элементами enum -a.
Элементы перечисления — экземпляры enum -класса, доступные статически
Элементы enum Season (WINTER, SPRING и т.д.) — это статически доступные экземпляры enum -класса Season . Их статическая доступность позволяет нам выполнять сравнение с помощью оператора сравнения ссылок == . Пример:
Season season = Season.SUMMER; if (season == Season.AUTUMN) season = Season.WINTER;
Название и порядковый номер элемента enum
Как уже было сказано ранее любой enum -класс наследует java.lang.Enum , который содержит ряд методов полезных для всех перечислений. Пример:
Season season = Season.WINTER; System.out.println("season.name()=" + season.name() + " season.toString()=" + season.toString() + " season.ordinal() lang-java line-numbers"> season.name()=WINTER season.toString()=WINTER season.ordinal()=0
Здесь показано использования методов name() , toString() и ordinal() . Семантика методов — очевидна. Следует обратить внимание, что данные методы enum -класс наследует из класса java.lang.Enum . Получение элемента enum по строковому представлению его имениДовольно часто возникает задача получить элемент enum по его строковому представлению. Для этих целей в каждом enum -классе компилятор автоматически создает специальный статический метод: public static EnumClass valueOf(String name) , который возвращает элемент перечисления EnumClass с названием, равным name . Пример использования:
String name = "WINTER"; Season season = Season.valueOf(name);
В результате выполнения кода переменная season будет равна Season.WINTER . Cледует обратить внимание, что если элемент не будет найден, то будет выброшен IllegalArgumentException, а в случае, если name равен null — NullPointerException. Об этом, кстати, часто забывают. Почему-то многие твердо уверенны, что если функция принимает один аргумент и при некоторых условиях выбрасывает IllegalArgumentException, то при передачи туда null , также будет непременно выброшен IllegalArgumentException. Но это не относится к делу. Продолжим. Получение всех элементов перечисления Иногда необходимо получить список всех элементов enum -класса во время выполнения. Для этих целей в каждом enum -классе компилятор создает метод public static EnumClass[] values() . Пример использования:
System.out.println(Arrays.toString(Season.values()));
[WINTER, SPRING, SUMMER, AUTUMN]
Обратите внимание, что ни метод valueOf() , ни метод values() не определен в классе java.lang.Enum . Вместо этого они автоматически добавляются компилятором на этапе компиляции enum -класса. Добавляем свои методы в enum -класс У Вас есть возможность добавлять собственные методы как в enum -класс, так и в его элементы: То же, но с полиморфизмом:
Последний пример демонстрирует использование наследования в enum . Об этом — далее. Наследование в enum С помощью enum в Java можно реализовать иерархию классов, объекты которой создаются в единственном экземпляре и доступны статически. При этом элементы enum могут содержать собственные конструкторы. Приведем пример:
Здесь объявляется перечисление Type с тремя элементами INT , INTEGER и STRING . Компилятор создаст следующие классы и объекты:
- Type — класс производный от java.lang.Enum
- INT — объект 1-го класса производного от Type
- INTEGER — объект 2-го класса производного от Type
- STRING — объект 3-го класса производного от Type
Три производных класса будут созданы с полиморфным методом Object parse(String) и конструктором Type(. boolean) . При этом объекты классов INT , INTEGER и STRING существуют в единственном экземпляре и доступны статически. В этом можно убедится:
System.out.println(Type.class); System.out.println(Type.INT.getClass() + " " + Type.INT.getClass().getSuperclass()); System.out.println(Type.INTEGER.getClass() + " " + Type.INTEGER.getClass().getSuperclass()); System.out.println(Type.STRING.getClass() + " " + Type.STRING.getClass().getSuperclass());
class Type class Type$1 class Type class Type$2 class Type class Type$3 class Type
Декомпилированный enum-class с наследованием
В подтверждение вышесказанному приведем еще результат декомпиляции перечисления Type из примера выше:
Перечисления и параметрический полиморфизм
У читателя может возникнуть вопрос: "почему вышеуказанное перечисление Type не использует генерики (generics)?". Дело в том, что в Java использование генериков в enum запрещено. Так следующий пример не скомпилируется: