Константы, перечисления (enum), и static import’ы в Java


Это статья в помощь начинающим изучать Яву, но уже с небольшим опытом программирования. Я постарался написать доступно и при этом раскрыть побольше нюансов.

Константа — это, грубо говоря, переменная значение которой не меняется во время работы программы. Классический пример константы — число Пи (3,14…). Константы используются для борьбы с Магическими числами, т.е. непонятно что означающими числами или строками.

В Java нет специальной директивы для объявления констант как например const в Pascal/Delphi или в C/C++. Т.е. не выйдет написать такое:

const double PI = 3.14;

Но в замен есть более гибкий модификатор final:

final double PI = 3.14;

В этом примере была объявлена финальная переменная с названием PI и ей было сразу же задано значение 3.14. Финальным переменным значения можно задать только один раз и больше его нельзя менять. Любую попытку поменять значение финальной переменной (или поля) компилятор будет воспринимать как ошибку, т.е. всё так же как и с константами.

Разница между финальным переменной и константой в том что инициализацию можно отложить:

final String someFinalVariable; // Объявляем финальную переменную, но мы её не проинициализировали значением!
System.out.println(someFinalVariable); // Ошибка компиляции: значение не задано. javac: variable not have been initialized
someFinalVariable = "Some value"; // Наконец задаём значение
System.out.println(someFinalVariable); // Выводим содержимое переменной
someFinalVariable = "New value"; // Ошибка компиляции: значение переменной уже задано. javac: variable someFinalVariable already have been assigned

Такая отложенная инициализация например часто используется для создания неизменяемых объектов (англ. Immutable objects).

Так вот, константами принято называть публичные, статические, финальные и сразу же проинициализированные поля:

public static final double PI = 3.14;

Статическое поле (модификатор static) принадлежит классу и для доступа к нему не нужно создавать конкретный экземпляр объекта:

public class Math {
    public static final double PI = 3.14;
}

public class Test {
    public static void main(String[] args) {
        double d = 3 * Math.PI; // Мы не создавали объект класса Math а обратились к полю самого класса!
        System.out.println(d);
    }
}

Обратите внимание что в Яве, как и во всех Си подобных языках, принято имена констант писать БОЛЬШИМИ_БУКВАМИ_РАЗДЕЛЯЯ_ИХ_ПОДЧЁРКИВАНИЕМ.

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

static final double SECONDS_IN_DAY = 60 * 60 * 24; // Сколько секунд в дне

При этом всё вычисляется на этапе компиляции и не нужно заменять 60 * 60 * 24 на менее читаемое и очевидное уже вычисленное вами 86400 (к тому же возможно неправильно рассчитанное).

Группирование констант через перечисления

Очень часто константы используются в нескольких классах и их хочется их все держать в одном модуле. В Яве ООП принудительное, поэтому всё таки придётся объявить класс без методов и все константы поместить в него. Например класс констант для модификаторов элементов языка Java:

public class Modifier {
    public static final int PUBLIC = 1;
    public static final int PROTECTED = 2;
    public static final int PRIVATE = 3;
    public static final int ABSTRACT = 4;
    public static final int STATIC = 5;
    public static final int FINAL = 6;
    public static final int TRANSIENT = 7;
    public static final int VOLATILE = 8;
    public static final int SYNCHRONIZED = 9;
    public static final int NATIVE = 10;
    public static final int STRICTFP = 11;
}

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

  • Небезопасность типа (type unsafe): в методе, где требуется передать значение перечисления, можно передать любое число, а не только значения от 1 до 11;
  • Неинформативность: например, при отладке значение 3 не скажет нам ни о чем. А хотелось бы увидеть PRIVATE;
  • Подверженность ошибкам: например, при добавлении нового элемента или при изменении последовательности существующих. Например можно не уследить, и у PUBLIC и PROTECTED поставить одинаковое значение 1. Т.е. отсутствует контроль со стороны компилятора как за уникальностью значений констант, так и за возможностью случайного присваивания переменным значений, не соответствующих ни одной из этих констант.

Такие сгруппированные константы называются перечислениями и в Java для них есть специальная конструкция enum.

Перепишем этот класс как перечисление:

enum Modifier {
    PUBLIC,
    PROTECTED,
    PRIVATE,
    ABSTRACT,
    STATIC,
    FINAL,
    TRANSIENT,
    VOLATILE,
    SYNCHRONIZED,
    NATIVE,
    STRICTFP;
}

И использовать его можно как обычную константу:

Modifier variableModifier = Modifier.PUBLIC;

Обратите внимание что перечисления не нужно сравнивать через equals, достаточно просто сравнить по ссылке:

// Так не делайте!
if (modifier.eqals(Modifier.PUBLIC)) {
    // ....
}
// Просто сравнивайте по ссылке 😉
if (modifier == Modifier.PUBLIC) {
    // ....
}

Вообще тут нужно отметить что раньше, до пятой версии Java перечислений не было, вместо них приходилось использовать классы со статическими константами. Это я говорю к тому, что многие классы с которыми вы столкнётесь остались написанными «дедовским» способом. Причём в стандартных классах их не меняли, например java.awt.Color.

Технически перечисления представляют собой полноценный класс который наследуется от java.lang.Enum, т.е. запись public enum Modifier равноценна abstract class Modifier extends java.lang.Enum.

А если это класс, значит в него можно добавлять произвольное количество полей и методов:

enum Modifier {
    PUBLIC,
    PROTECTED,
    PRIVATE,
    ABSTRACT,
    STATIC,
    FINAL,
    TRANSIENT,
    VOLATILE,
    SYNCHRONIZED,
    NATIVE,
    STRICTFP;

    /**
    * Возвращает это имя модификатора в нижнем регистре.
    */
    @Override
    public String toString() {
        String lowercase = name().toLowerCase(java.util.Locale.US);
        return lowercase;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Modifier variableModifier = Modifier.PUBLIC;
        System.out.println(variableModifier);
    }
}

При запуске этого примера в вывод будет напечатано public а не 1! У каждого enum’а хранится поле name с его именем (в нашем случае PUBLIC). Мы переопределили (@Override) метод toString() внутри которого приводим имя к нижнему регистру.

Это особенно сносит мозг тем кто раньше программировал в других языках программирования в которых перечисления являются по сути сгруппированными обычными целочисленными константами, т.е. типами значения (содержат непосредственно значения, и при присвоении переменной одного типа значений другому создается копия присваиваемого значения, в отличие от переменных ссылочного типа, при присвоении которых копируются ссылки на объекты, но не сами объекты).

Ещё нужно отметить, что хотя класс перечисления вроде как не финальный (т.е. объявлен без модификатора final и можно его наследовать), дальше расширять наш класс мы уже не можем — этот запрет реализован на уровне компилятора.

Использование статических импортов

Перечисления естественно подходят не для всех случаев. Например константа натурального логарифма e и константу π нельзя объединять в перечисление. Давайте посмотрим на класс содержащий эту константы:

public class Math {
    public static final double E = 2.71;
    public static final double PI = 3.14;
}

Все его поля public static final. Чтобы каждый раз не писать эти модификаторы многие программисты объявляют константы в интерфейсе, потому что все его поля итак уже public static final:

interface Math {
    double E = 2.71;
    double PI = 3.14;
}

И вот кому то стрельнула идея: «А зачем каждый раз писать полное имя интерфейса с константами, если можно его заимплементить и получить к ним доступ напрямую?». И такие конструкции

public class Test {
    public static void main(String[] args) {
        double d = 3 * Math.PI; 
        System.out.println(d);
    }
}

превратились в такой кошмар

public class Test implements Math {
    public static void main(String[] args) {
        double d = 3 * PI; // Math. можно уже не писать 
        System.out.println(d);
    }
}

Так вот, так делать нельзя, это антипаттерн. Фактически это настолько плохая идея, что для нее есть свое название: Constant Interface Antipattern (см. Effective Java, 17 статья. Эта книга вообще обязательная для прочтения всем Ява разработчикам). Дело в том, что использование статических членов класса другим классом всего лишь деталь реализации. Когда же класс реализует интерфейс, его члены становится частью публичного АРI этого класса. Детали реализации не должны становиться публичным программным интерфейсом. Вместо этого используйте статические импорты.

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

import static Math.PI;

или все целиком:

import static Math.*;

Однажды импортированный статический член может быть использован без указания имени класса:

double d = 3 * PI; // Math. можно не писать 

Объявление статического импорта аналогично объявлению обычного импорта. При объявлении обычного импорта классы импортируются из пакетов, что позволяет их использовать без указания имени пакета перед именем класса. При объявлении статического импорта статические члены импортируются из классов, что позволяет им быть использованными без указания имени содержащего их класса.

Так когда же следует использовать статический импорт? Только в некоторых случаях! Используйте его только, если иначе вы вынуждены объявлять локальные копии констант или при неправильном использовании наследования. Другими словами, использование его оправданно, когда требуется постоянное использование статических членов одного класса из одного или двух других классов.

Чрезмерное использование статического импорта может сделать вашу программу нечитаемой и тяжелой в поддержке ввиду того, что пространство имен увеличится из-за всех статических членов из вашего импорта. Тот, кто будет читать ваш код (включая вас, спустя несколько месяцев после написания кода) не будут знать какому из статически импортированных классов принадлежит тот или иной член.

Импортирование всех статических методов из класса может быть частично вредно для читабельности; если вы нуждаетесь только в одном или двух методах, импортируйте их по-отдельности. Использованный умело, статический импорт может сделать вашу программу более наглядной, исключая из программного кода нунжные повторения имен классов.

Как правильно объявить константный класс?

Если всё таки очень надо, то объявляйте константный класс как обычный класс, а не интерфейс. Только делайте его ещё и финальным (final), чтобы от него нельзя было унаследоваться. А ещё было бы неплохо сделать его абстрактным (abstract), чтобы нельзя было создать его экземпляр, ведь все константы у нас статические и мы обращаемся к полям класса а не объекта.

Проблема в том что классу нельзя одновременно указать модификаторы final abstract, потому что для компилятора такое объявление абсурдно. Но этого можно добиться просто объявив дефолтный конструктор как приватный. Как известно, конструктор по умолчанию создается только если класс не содержит никаких явных конструкторов. Поэтому, определив у класса приватный конструктор без параметров мы обеспечим запрет на его инстанциирование из кода вне класса:

public final class Math {
    public static final double E = 2.71;
    public static final double PI = 3.14;
    private Math() {
    }
}

Заключение

Как видите, есть различные нюансы использования констант в Яве, которые не всегда очевидны для новичков. К сожалению мною остались ещё не освящены темы констант инлайнинга, хотелось бы ещё написать про имитацию сетов через операцию сложения +, показать как создавать константные коллекции (это я расскажу в отдельной статье).

Смотрите также ещё одну прекрассную статью Кучука https://github.com/qcha/JBook/blob/master/start/enum.md


Не поленитесь, и почитайте ещё несколько очень важных моих статей для начинающих программистов, студентов и джуниоров.
Также не забудьте вступить в группу IT Juniors куда я пытаюсь собирать ссылки на другие полезные статьи для вас и анонсы курсов и интернатуры в компаниях.

Один комментарий

  1. Аноним

    Извиняюсь за лажу с сообщением выше, это я проверял, нужно ли вводить сопутствующую инфу. Автор, всё классно, но хватит с темы на тему перескакивать. Тут enum, перегрузка стандартных(а их то не все знают начинающие) методов(ты бы ещё equal с хэшкодом перегрузил дабы окончательно запутать). И зачем объединять две такие разные темы? Статические импорты и перечисляемые типы? У них общего нет ни копейки

    • stokito

      Та, норм. Дело в том что как раз эти темы немного в связке идут, если не сразу поймут то поймут чуть позже. Так кстати даже лучше запомнится — не до конца раскрытая информация остаётся в мозгу на обработке.
      Насколько я помню эту статью я писал сразу после того как джуну объяснил энумы, так сказать по горячим следам.

  2. Уведомление: Возможности Java: статический импорт | stokito on software
  3. Уведомление: Возможности Java: статический импорт | stokito on software

Оставьте комментарий