Частенько в коде встречаются перечисления, дополнительно хранящие некие значения (в виде private final
поля обычно).
Чтобы в дальнейшем можно было, например, при сериализации в json это значение подставлять автоматом (@JsonValue
у Jackson). Тогда возникает обычно и обратная задача — десериализовать (распарсить) значение обратно в Enum (@JsonCreator
у Jackson).
Вот мне и надоело копипастить туда-сюда все эти методы (сериализации/десериализации) между классами Enum. Решил сделать один раз утилитный метод и в проекте им пользоваться. Благо, время позволило.
В принципе, подобный метод есть в недрах Apache Commons EnumUtils, но он работает только со строковым параметром String enumName
, плюс выкидывает стандартное исключение. А обычно надо выкинуть некое кастомное, принятое на проекте. Так и родился свой костылёчек, как оно зачастую и бывает.
Получить класс Enum по значению
Импорты копировать не буду, полагаю, IDE предложит их подставить по выбору, если автоматически не сможет этого сделать. Сам утилитный метод примитивен:
public final class EnumUtil {
private EnumUtil() {}
public static <T extends ValuedEnum<V>, V> T fromValue(Class<T> enumType, V value) {
return Arrays.stream(enumType.getEnumConstants())
.filter(it -> it.getValue().equals(value))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(String.format(
"Wrong value [%s] for enum type [%s]", value, enumType.getSimpleName())
));
}
}
Разве что — в реальных условиях он выбрасывает не IllegalArgumentException
, а кастомное исключение.
ValuedEnum
— это интерфейс, который реализуют все Enum со значением. Вот он:
public sealed interface ValuedEnum<V> permits
ComplexValue,
NumberValue,
StringValue
{
V getValue();
ValuedEnum<V> fromValue(V value);
}
Сделан sealed
, чтобы при имплементации (методом копипасты) — был дополнительный «маячок», заставляющий обратить внимание — это именно Enum со значением.
Пример работы и тесты
Набор тестовых Enum
Для демонстрации — три штуки тестовых классов Enum. Значения — Map.Entry
, Number
и String
соответсвенно.
Значение — составной тип
public enum ComplexValue implements ValuedEnum<Entry<String, Number>> {
COMPLEX_VALUE(Map.entry("One", BigDecimal.ONE));
private final Entry<String, Number> value;
ComplexValue(Entry<String, Number> value) {
this.value = value;
}
@Override
public Entry<String, Number> getValue() {
return this.value;
}
@Override
public ValuedEnum<Entry<String, Number>> fromValue(Entry<String, Number> value) {
return EnumUtil.fromValue(ComplexValue.class, value);
}
}
Значение — число
public enum NumberValue implements ValuedEnum<Number> {
NUMBER_VALUE(42);
private final Number value;
NumberValue(Number value) {
this.value = value;
}
@Override
public Number getValue() {
return this.value;
}
@Override
public ValuedEnum<Number> fromValue(Number value) {
return EnumUtil.fromValue(NumberValue.class, value);
}
}
Значение — строка (банально)
public enum StringValue implements ValuedEnum<String> {
STRING_VALUE("String value");
private final String value;
StringValue(String value) {
this.value = value;
}
@Override
public String getValue() {
return this.value;
}
@Override
public ValuedEnum<String> fromValue(String value) {
return EnumUtil.fromValue(StringValue.class, value);
}
}
Все три ничего особенного из себя не представляют, просто демонстрируют, что это работает для различных случаев.
Юнит тесты
Здесь проще привести всё целиком, включая импорты. JUnit пятой версии используется.
import org.assertj.core.api.AssertionsForClassTypes;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.math.BigDecimal;
import java.util.Map;
import java.util.stream.Stream;
@Tag("unit")
class EnumUtilTest {
private static Stream<Arguments> enumFromValueGoodParams() {
return Stream.of(
Arguments.arguments(ComplexValue.class, Map.entry("One", BigDecimal.ONE), ComplexValue.COMPLEX_VALUE),
Arguments.arguments(NumberValue.class, 42, NumberValue.NUMBER_VALUE),
Arguments.arguments(StringValue.class, "String value", StringValue.STRING_VALUE)
);
}
private static Stream<Arguments> enumFromValueBadParams() {
return Stream.of(
Arguments.arguments(ComplexValue.class, Map.entry("TEN", BigDecimal.TEN)),
Arguments.arguments(NumberValue.class, 42.42d),
Arguments.arguments(StringValue.class, "Not found String value")
);
}
@DisplayName("Success parse value to a Enum")
@ParameterizedTest
@MethodSource("enumFromValueGoodParams")
<T extends ValuedEnum<V>, V> void fromValueSuccess(Class<T> enumClass, V enumValue, T expected) {
var fromValue = EnumUtil.fromValue(enumClass, enumValue);
Assertions.assertEquals(expected, fromValue);
}
@DisplayName("Thrown when parse wrong value to a Enum")
@ParameterizedTest
@MethodSource("enumFromValueBadParams")
<T extends ValuedEnum<V>, V> void fromValueError(Class<T> enumClass, V enumBadValue) {
AssertionsForClassTypes.assertThatThrownBy(() -> EnumUtil.fromValue(enumClass, enumBadValue))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining(
String.format("Wrong value [%s] for enum type [%s]", enumBadValue, enumClass.getSimpleName())
);
}
}
А результат выполнения этих тестов вынесен в начало заметки — видно, что они проходят 🙂