Последнее изменение: 1 сентября 2007г.

Изменчивые постоянные или "что такое 122?"

В этой статье речь пойдет об использовании констант. При всей внешней простоте этот вопрос содержит немало тонкостей, как идеологического, так и чисто реализационного плана. Когда и почему стоит использовать константы? Когда этого можно не делать, и почему? На эти вопросы я хочу дать ответы. Сразу оговорю – речь идет в основном о числовых константах. Использование неизменяемых объектов вызвано несколько другими причинами.

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

Я бы хотел предложить следующее определение:

Константа есть отражение нашего предположения о том, что некоторая величина имеет заранее определенное постоянное значение.

Я не случайно употребил термин "предположение". В большинстве случаев постоянство используемых значений продиктовано не свойствами мира, а всего лишь постановкой задачи – бизнес-требованиями и т.п. Другими словами, это значение является постоянным лишь в текущий момент времени. Если сегодня размер почтового ящика для пользователя некой почтовой системы – 5 Мб, то завтра он может стать 10 Мб. А послезавтра – 25 Мб. С точки зрения программирования это значение – постоянно. С точки зрения устройства мира – нет. Именно этот факт и является одной из основных причин использования констант в программировании.

В принципе, можно в коде использовать и числа. Никто этого не может запретить. Но представьте ситуацию, когда какое-то число поменялось. В этом случае приходится перелопачивать весь код и менять значение на новое. А если несколько значений совпадают по величине, то изменение одного из них превращается в жуткую задачу. Ошибиться – проще простого. Найти ошибку – очень тяжело.

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

Итак, одной из причин использования констант является возможность их изменения. Разумеется, не всегда так. Часть постоянных значений действительно ПОСТОЯННА. Но тут уже появляется вторая причина.

Дело в том, что числа сами по себе говорят довольно немного. Если вообще говорят. Допустим, если в коде встретится 9.8 или, тем паче, 9.81 – в сознании что-то шевельнется. Эта константа въелась в память еще со школьной скамьи – ускорение свободного падения. Да и то, стопроцентной гарантии, что это именно оно, никто не даст.

А как быть, например, с числом 1.4? Что это? Корень из 2? Постоянная Больцмана, округленная до десятых и без указания степени? Коэффициент порчи экспериментального материала из закона Чизхолма? Число 1.4 не говорит абсолютно ни о чем. Для того, чтобы понять, что это такое, надо разбираться в коде. И иногда весьма долго. Даже в том случае, если это число встретилось в коде один раз.

В этой ситуации применение констант тоже может сильно облегчить жизнь. Более существенно здесь то, что константа имеет имя. И имя это можно выбрать так, что оно будет говорить о константе всё. Если 1.4 – корень из двух, то называться эта константа может, например, SQRT_2. Это имя говорит само за себя. И читаемость кода при этом сильно повышается.

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

Рассмотрим следующий фрагмент кодировщика BASE64 (возможно, этот код не столь оптимален, как мог бы быть, но речь сейчас не об оптимальности):

/**
 * This method splits group of 3 bytes into group of 4 6-bit values;
 *
 * @param src source data (3 bytes)
 *
 * @return array of 4 6-bit elements (two highest bits are guaranteed to be 0)
 */
private static int[] split(byte[] src){
    assert (src.length == 3) : "Source array length should be equal to 3";
    int result[] = new int[4];
    int _24bit = 0;
    for(int i=0; i<src.length ; i++){
        _24bit += (src[i] & MASK_8BIT) << (8*(2-i));
    }
    for(int i=0; i<result.length ; i++){
        result[i] = (_24bit >> (6*(3-i))) & MASK_6BIT;
    }
    return result;
}

Алгоритм BASE64 известен. Три последовательных байта объединяются в группу длиной 24 бита, после чего делятся на четыре 6-битовых значения. И потому – числа 3, 4, 6 и 8 в контексте данного метода вопросов не вызывают. Более того, использование вместо них констант может ухудшить читаемость кода. Во всяком случае, у меня это именно так. Кстати говоря, замена этих чисел константами ничего не даст, потому как для понимания, почему число (значение константы) именно такое, необходимо знание алгоритма.

Подведем черту под идеологией использования констант. Итак, в коде допустимы значения 0 и 1 в качестве инициализаторов переменных. В коде допустимо использование других чисел, если они являются неотъемлемой частью алгоритма, не вызывающей сомнений. Все остальные числа ОЧЕНЬ ЖЕЛАТЕЛЬНО заменять константами. Это сделает код более читаемым и облегчит его изменения. Стоит упомянуть, что использование констант считается обязательным в соглашениях по написанию кода компании Sun. Хотя у них допускаются только числа -1, 0 и 1.

* * *

С идеологией закончили, перейдем к практике. Тут тоже есть некоторые тонкие моменты. Прежде всего необходимо принять во внимание тот факт, что компилятор встраивает значения static final полей в тех местах, где они используются. Неважно, откуда берутся константы: из интерфейсов или из классов, наследуются ли они или используются через имя класса, – их значения встраиваются в код. Убедиться в этом можно на следующем простейшем примере:

package test;

public interface Int{
    public static final int CONST = 1;
}
package test;

public class Cls implements Int{
    public static void main(String args[]){
        System.out.println("CONST="+CONST);
    }
}

Скомпилируйте, запустите. В консоль будет выведено CONST=1. Теперь поменяйте значение константы, скажем, на 2. Скомпилируйте ТОЛЬКО Int.java (предупреждение – практически все IDE отслеживают зависимости и перекомпилируют еще и Cls, потому всё это надо делать вручную, из командной строки). Запустите test.Cls. В консоль будет выведено всё то же CONST=1. Перекомпилируйте Cls.java, запустите test.Cls. Теперь в консоль будет выведено – CONST=2.

Что из этого следует? А следует из этого вот что: при изменении значения константы придется перекомпилировать весь код, который ее использует. В рамках одного проекта это сделать несложно. Гораздо больше проблем может возникнуть, если константа изменяется в библиотеке, которую используют другие приложения. Библиотека будет скомпилирована с новым значением, а в приложении останется старое, ибо это значение встроено в код. В результате такой код без перекомпиляции работать не будет. Если же библиотеку использует много приложений – перекомпилировать все будет затруднительно. Если приложений ОЧЕНЬ много... Масштаб катастрофы оцените сами.

Учитывая такое положение вещей, лично я при создании библиотек пользуюсь следующим подходом. Если константы с высокой вероятностью не будут меняться – они фигурируют в коде как константы. Если же постоянство этих значений – суть следствие, скажем, бизнес-требований – имеет смысл вынести их в отдельный класс. В нем они будут объявлены как private static final, а доступ к ним будет через методы getXXX(). Такой подход гарантирует, что при изменении константы весь использующий её код будет использовать новое значение.

Еще один тонкий момент в объявлении констант вот в чем: можно объявить константу в интерфейсе, а можно – в классе. Разница следующая. И в том, и в другом случае константы будут логически сгруппированы. Но поскольку интерфейсов можно реализовать несколько, при использовании констант может возникнуть путаница, т.к. принадлежность константы к той или иной группе очевидна не будет. В случае же использования класса – без указания имени класса не обойтись, что улучшает восприятие кода. Лучше в таком случае сделать класс final, но это уже дело вкуса.

В процессе написания этой статьи (вернее, после окончания, но до публикации) я наткнулся в форуме на javalobby.org на весьма любопытную идею относительно встраивания констант в код. При каких условиях это вообще происходит? Таких условий три:

  1. Константа должна быть примитивного типа
  2. Константа должна быть static final
  3. Константа должна быть инициализирована в момент объявления

Иначе говоря, константа из следующего фрагмента будет встроена компилятором в код:

package test;

public class Constants{
    public static final int CONST = 1;
}

Но! У нас есть возможность нарушить третье условие:

package test;

public class Constants{
    public static final int CONST;

    static{
        CONST = 1;
    }
}

И в ЭТОМ случае константа не будет встроена в код. Ее значение каждый раз будет браться из исходного класса.

Пользоваться этим подходом или нет – дело каждого. Мне лично он не нравится вот чем. Во-первых, нарушается читаемость. Если константа одна, как в приведенном примере – весь код можно схватить взглядом. Если же их много, либо эта константа объявлена в классе, содержащем много членов, так что блок статической инициализации будет далеко от объявления – значение константы узнать уже сложнее. Во-вторых, значение такой константы не будет отражено в документации. Я имею в виду javadoc. Несмотря на то, что значение константы определено, генератор документации его не найдет. Так что... Подход, конечно, любопытный, но лишь с теоретической точки зрения. Во всяком случае, для меня.

Еще одно замечание я хотел бы сделать в связи с новой возможностью языка версии 5.0 и выше, а именно – по поводу статического импорта. Это весьма удобная возможность, когда речь идет об импортировании методов, скажем, удобнее использовать функции типа sin, cos и т.п. без префикса Math. Однако в случае констант – эффект как раз обратный. Эта возможность позволяет импортировать и напрямую использовать константы даже в случае их объявления в классах. Как я уже говорил, я считаю, что это не лучшее решение, потому я бы рекомендовал очень обдуманно подходить к использованию статического импорта констант, тщательно взвешивая все "за" и "против" прежде всего с позиции качества кода.

* * *

На этом пока закончу. Ну и напоследок – ответ на вопрос из заголовка. Что такое 122? Представьте следующий фрагмент кода:

if (value > 122){
    throw new IllegalArgumentException("Illegal BASE64 character: "+value);
}

По этому фрагменту можно догадаться, что эта константа имеет отношение к кодировке BASE64. Но и только. Что именно это такое – придется вникать. Сравните этот код со следующим:

if (value > MAX_BASE64_CHAR_CODE){
    throw new IllegalArgumentException("Illegal BASE64 character: "+value);
}

Согласитесь, что этот фрагмент вызывает куда меньше вопросов, нежели первый. 122 – это код буквы z, максимальный из кодов всех используемых в BASE64 символов. О чем константа и говорит, причем недвусмысленно.