
Как-то так сложилось, что я до сих пор не дружу с некоторыми вполне стандартными фичами в Джаве и постоянно вынужден их гуглить или читать документацию, когда встречаюсь с ними в задачах. И одной из таких тем стали enum, которые я изучал ещё в универе десять лет назад, но так и не запомнил всех нюансов. Поэтому я подумал, что было бы неплохо закрепить знания написанием статьи – может, она ещё кому-нибудь пригодится.
public static final int STATUS_MARRIED = 0;
public static final int STATUS_SINGLE = 1;
public static final int STATUS_NOT_SPECIFIED = 2;
Такой подход не очень удобный, потому что названия получались довольно длинными (первое слово обычно значит критерий, а остальные слова – название конкретного варианта), а правильность работы программы зависела лишь от добросовестности программиста. Компилятор не остановит вас от суммирования разных статусов и нарушения целостности информации.
int STATUS_UNKNOWN = STATUS_SINGLE + STATUS_NOT_SPECIFIED;
Раньше программист также мог бы что-нибудь напутать и подменить параметр функции, ожидающей статус гражданина, на что-нибудь другое. Например, компилятор бы ничего не заподозрил, если в функцию, сохраняющую в базу статус вы бы передали пол гражданина:
public static final int GENDER_M = 0;
public static final int GENDER_F = 1;
public static final int GENDER_NOT_SPECIFIED = 2;
public void saveMaritalStatusToDB(int MaritalStatus) {
//сохраняем инфомрацию в базу данных
}
public void processData() {
saveMaritalStatusToDB(GENDER_F);
}
Сохранение целостности данных с помощью enum
К счастью, в языке со временем появился инструмент для создания подобных перечисляемых типов – enum. Теперь оперировать этими значениями, словно это обычные целые числа, уже стало нельзя.
public enum MaritalStatus {
MARRIED, SINGLE, NOT_SPECIFIED;
}
За кулисами данные в таком enum хранятся так же – в виде final полей. Каждый элемент внутри перечисления доступен извне, но пользователь не может создавать новые элементы и не может создавать переменные перечисляемого типа. Таким образом, следующие попытки программиста не увенчаются успехом:
new MaritalStatus();
MaritalStatus.NOT_SPECIFIED + MaritalStatus.SINGLE;
Получается, что enum очень похож на шаблон Singleton. Программист теперь так же не сможет нечаянно заменить аргумент любой функции на другой перечисляемый тип. Подобный код просто не скомпилируется:
public enum Gender {
M, F, NOT_SPECIFIED;
}
public void saveMaritalStatusToDB(MaritalStatus MaritalStatus) {
//сохраняем инфомрацию в базу данных
}
public void processData() {
saveMaritalStatusToDB(Gender.F);
}
Методы и поля в enum
В отличие от подобных типов в других языках, в Джаве также можно добавлять в них свои методы и поля. Например, вы пишете игру по типу гоночек. И у каждой машины есть свойства «масса» и «мощность», а исходя из них вы также можете вычислить максимальную скорость.
public enum GameCar {
LIGHT_CAR(2, 1),
MEDIUM_CAR(2.5, 2),
HEAVY_CAR(4, 2.5);
private final double mass;
private final double power;
private final double maxSpeed;
private final double basicSpeed = 100;
private final double coEff = 20;
GameCar(double mass, double power) {
this.mass = mass;
this.power = power;
//эту формулу я выдумал на ходу, поэтому она не отражает законы физики,
//но иллюстрирует возможности enum типов
this.maxSpeed = power * basicSpeed - mass * coEff;
}
public double mass() { return mass; }
public double power() { return power; }
public double maxSpeed() { return maxSpeed; }
}
Поля можно сделать и публичными, но лучше, конечно же, как и во всех других классах, использовать приватные и давать пользователям класса способ получить нужные данные.
Каждый enum предоставляет пользователю статический метод Values, который возвращает массив возможных значений в порядке, в котором они были объявлены. Кроме этого вы также можете воспользоваться методом toString() – по умолчанию он вернёт название элемента, как вы его написали (в моём случае, LIGHT_CAR капсом и т. д.). Но вы всегда можете переопределить метод toString(), как и в любом другом классе.
И если вы хотите, чтобы у каждого элемента были не только свои значения полей, но и особое поведение, это также возможно сделать. Рассмотри пример с калькулятором, в котором заранее заданы четыре основные операции.
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
MULTIPLY("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
}
В данном примере у каждой операции есть своё символьное представление, которое задаётся при создании типа через конструктор. Это же значение возвращается методом toString(), который здесь переопределён. У каждого элемента должен быть метод apply, который изначально мы задали абстрактным, но определили для каждого элемента – таким образом, каждая операция подразумевает уникальное поведение, но предоставляет пользователю схожий интерфейс. Всё получается, как в обычных классах.
Если вы переопределили toString(), которая превращает конкретный элемент в строку, то стоит подумать и над обратным методом, который превращает строку в элемент перечисляемого типа.
Для этого нужно бы создать поле, в котором будет храниться «словарь» в виде мапы.
private static Map<String, Operation> stringMap = recordStrings();
private static Map<String, Operation> recordStrings() {
Map<String, Operation> map = new HashMap<>();
for (Operation op : values()) {
map.put(op.toString(), op);
}
return map;
}
public static Operation fromString(String symbol) {
return stringMap.get(symbol);
}
Ну или можно сократить происходящее до следующего варианта.
private static Map<String, Operation> stringMap =
Stream.of(values()).collect(
Collectors.toMap(Object::toString, e -> e));
public static Operation fromString(String symbol) {
return stringMap.get(symbol);
}
Теперь можно использовать этот класс следующим образом:
Operation op = Operation.fromString("+");
System.out.println(op.apply(2,2));
Enum со значениями по умолчанию
Напоследок добавлю ещё один пример, в котором представлю поведение по умолчанию. В следующем примере енум символизирует методы оплаты за разные дни. В списке все семь дней недели и праздничный день. Каждому дню соответствует свой метод оплаты, который описан в приватном, вложенном классе PayType.
По умолчанию за будние дни мы платим обычную сумму. Эта стратегия оплаты задаётся через конструктор по умолчанию. В остальных случаях метод оплаты задаётся явно. Для расчёта используется длительность Shift – смены.
enum PayDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND), HOLIDAY(PayType.HOLIDAY);
private final PayType payType;
PayDay(PayType payType) { this.payType = payType; }
PayDay() { this(PayType.WEEKDAY); }
int pay(Shift shift) {
int regularPay = shift.regularHours() * this.payType.payPerHour;
int overtimePay = shift.overtimeHours() * PayType.OVERTIME.payPerHour;
return regularPay + overtimePay;
}
private enum PayType {
WEEKDAY (10),
WEEKEND (12),
HOLIDAY (14),
OVERTIME (15);
private final int payPerHour;
PayType(int payPerHour) { this.payPerHour = payPerHour; }
}
}
enum Shift {
SHORT(4), FULL(8), EXTRA(12);
private int hoursOfWork;
private final int normalDay = 8;
Shift(int hours) {
this.hoursOfWork = hours;
}
public int overtimeHours() {
int overtime = hoursOfWork - normalDay;
return overtime > 0 ? overtime : 0;
}
public int regularHours() {
return hoursOfWork > normalDay ? normalDay : hoursOfWork;
}
}
С таким набором можно посчитать, сколько же человек заработал за неделю, если он работал пять дней, но в понедельник был в офисе полдня, но зато отработал дополнительные часы в четверг.
int week = 0;
week += PayDay.MONDAY.pay(Shift.SHORT);
week += PayDay.WEDNESDAY.pay(Shift.FULL);
week += PayDay.THURSDAY.pay(Shift.EXTRA);
week += PayDay.FRIDAY.pay(Shift.FULL);
week += PayDay.SATURDAY.pay(Shift.FULL);
System.out.println("You earned for this week: " + week);
На этом сегодня всё. В этой статье я разобрал основные принципы работы с enum в Джаве: создание подобных конструкций, задание полей и методов.