Java SE tutorial: перечисляемые типы enum

Как-то так сложилось, что я до сих пор не дружу с некоторыми вполне стандартными фичами в Джаве и постоянно вынужден их гуглить или читать документацию, когда встречаюсь с ними в задачах. И одной из таких тем стали 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 в Джаве: создание подобных конструкций, задание полей и методов.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s