Используйте постоянные типы для более безопасного и чистого кода

В этом руководстве мы расширяем идею нумерованных констант, описанную в книге Эрика Армстронга «Создание нумерованных констант в Java». Я настоятельно рекомендую прочитать эту статью, прежде чем вы погрузитесь в эту, поскольку я предполагаю, что вы знакомы с концепциями, связанными с перечисляемыми константами, и я подробно остановлюсь на некоторых примерах кода, которые представил Эрик.

Понятие констант

Имея дело с пронумерованными константами, я собираюсь обсудить нумерованную часть концепции в конце статьи. А пока мы просто сосредоточимся на постоянном аспекте. Константы - это в основном переменные, значение которых не может измениться. В C / C ++ ключевое слово constиспользуется для объявления этих постоянных переменных. В Java вы используете ключевое слово final. Однако представленный здесь инструмент - это не просто примитивная переменная; это реальный экземпляр объекта. Экземпляры объектов неизменяемы и неизменны - их внутреннее состояние не может быть изменено. Это похоже на шаблон singleton, где класс может иметь только один единственный экземпляр; однако в этом случае класс может иметь только ограниченный и предопределенный набор экземпляров.

Основные причины использования констант - ясность и безопасность. Например, следующий фрагмент кода не требует пояснений:

public void setColor (int x) {...} public void someMethod () {setColor (5); }

Из этого кода мы можем убедиться, что цвет задается. Но какой цвет обозначает 5? Если бы этот код был написан одним из тех редких программистов, которые комментируют его или ее работу, мы могли бы найти ответ в верхней части файла. Но, скорее всего, нам придется искать какие-то старые проектные документы (если они вообще есть) для объяснения.

Более ясное решение - присвоить значение 5 переменной с осмысленным именем. Например:

public static final int RED = 5; public void someMethod () {setColor (КРАСНЫЙ); }

Теперь мы можем сразу сказать, что происходит с кодом. Цвет устанавливается на красный. Это намного чище, но безопаснее? Что, если другой кодировщик запутается и объявит другие значения, например:

public static final int RED = 3; общедоступный статический финальный int GREEN = 5;

Теперь у нас две проблемы. Во-первых, REDбольше не установлено правильное значение. Во-вторых, значение красного представлено переменной с именем GREEN. Возможно, самое страшное в том, что этот код будет компилироваться без проблем, и ошибка может быть обнаружена только после отправки продукта.

Мы можем решить эту проблему, создав окончательный цветовой класс:

общедоступный класс Color {общедоступный статический окончательный int RED = 5; общедоступный статический финальный int GREEN = 7; }

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

public void someMethod () {setColor (Color.RED); }

Я говорю поощрять, потому что дизайн в этом листинге кода не позволяет нам заставить кодировщика подчиняться; код все равно будет компилироваться, даже если все не совсем в порядке. Таким образом, хотя это немного безопаснее, это не совсем безопасно. Хотя программисты должны использовать этот Colorкласс, они не обязаны это делать. Программисты могли очень легко написать и скомпилировать следующий код:

 setColor (3498910); 

Распознает ли setColorметод это большое число как цвет? Возможно нет. Так как же защититься от этих мошенников-программистов? Здесь на помощь приходят типы констант.

Начнем с переопределения сигнатуры метода:

 public void setColor (Color x) {...} 

Теперь программисты не могут передавать произвольное целочисленное значение. Они вынуждены предоставить действительный Colorобъект. Пример реализации этого может выглядеть так:

public void someMethod () {setColor (новый цвет ("Красный")); }

Мы по-прежнему работаем с чистым, читаемым кодом и намного ближе к достижению абсолютной безопасности. Но мы еще не совсем там. У программиста все еще есть место, чтобы сеять хаос, и он может произвольно создавать новые цвета, например:

public void someMethod () {setColor (новый цвет ("Привет, меня зовут Тед.")); }

Мы предотвращаем эту ситуацию, делая Colorкласс неизменяемым и скрывая создание экземпляра от программиста. Мы делаем каждый цвет (красный, зеленый, синий) одноэлементным. Это достигается за счет того, что конструктор становится закрытым, а открытые дескрипторы становятся доступными для ограниченного и четко определенного списка экземпляров:

публичный класс Color {частный цвет () {} публичный статический окончательный цвет КРАСНЫЙ = новый цвет (); общедоступный статический окончательный цвет ЗЕЛЕНЫЙ = новый цвет (); общедоступный статический окончательный цвет СИНИЙ = новый цвет (); }

В этом коде мы наконец достигли абсолютной безопасности. Программист не может создавать фальшивые цвета. Можно использовать только определенные цвета; иначе программа не скомпилируется. Вот так теперь выглядит наша реализация:

public void someMethod () {setColor (Color.RED); }

Упорство

Хорошо, теперь у нас есть чистый и безопасный способ работы с постоянными типами. Мы можем создать объект с атрибутом цвета и быть уверенным, что значение цвета всегда будет действительным. Но что, если мы хотим сохранить этот объект в базе данных или записать его в файл? Как сохранить значение цвета? Мы должны сопоставить эти типы со значениями.

В упомянутой выше статье JavaWorld Эрик Армстронг использовал строковые значения. Использование строк дает дополнительный бонус в виде возврата в toString()методе чего- то значимого , что делает вывод отладки очень понятным.

Однако хранить струны может быть дорого. Целое число требует 32 бита для хранения его значения, в то время как строка требует 16 бит на символ (из-за поддержки Unicode). Например, число 49858712 можно сохранить в 32-битном формате, но для строки TURQUOISEпотребуется 144 бита. Если вы храните тысячи объектов с атрибутами цвета, эта относительно небольшая разница в битах (в данном случае от 32 до 144) может быстро увеличиться. Так что давайте вместо этого будем использовать целые числа. Какое решение этой проблемы? Мы сохраним строковые значения, потому что они важны для представления, но мы не собираемся их хранить.

Версии Java, начиная с версии 1.1, могут автоматически сериализовать объекты, если они реализуют Serializableинтерфейс. Чтобы предотвратить хранение в Java посторонних данных, вы должны объявить такие переменные с помощью transientключевого слова. Итак, чтобы хранить целочисленные значения без сохранения строкового представления, мы объявляем строковый атрибут временным. Вот новый класс и методы доступа к целочисленным и строковым атрибутам:

открытый класс Color реализует java.io.Serializable {частное значение int; частное временное имя строки; общедоступный статический окончательный цвет КРАСНЫЙ = новый цвет (0, «Красный»); общедоступный статический окончательный цвет СИНИЙ = новый цвет (1, «Синий»); public static final Color GREEN = new Color (2, «Зеленый»); частный цвет (значение типа int, имя строки) {this.value = значение; this.name = имя; } public int getValue () {возвращаемое значение; } публичная строка toString () {возвращаемое имя; }}

Теперь мы можем эффективно хранить экземпляры постоянного типа Color. Но как насчет их восстановления? Это будет немного сложно. Прежде чем идти дальше, давайте расширим это до фреймворка, который справится со всеми вышеупомянутыми ловушками за нас, позволяя нам сосредоточиться на простом вопросе определения типов.

Каркас постоянного типа

With our firm understanding of constant types, I can now jump into this month's tool. The tool is called Type and it is a simple abstract class. All you have to do is create a very simple subclass and you've got a full-featured constant type library. Here's what our Color class will look like now:

public class Color extends Type { protected Color( int value, String desc ) { super( value, desc ); } public static final Color RED = new Color( 0, "Red" ); public static final Color BLUE = new Color( 1, "Blue" ); public static final Color GREEN = new Color( 2, "Green" ); } 

The Color class consists of nothing but a constructor and a few publicly accessible instances. All of the logic discussed to this point will be defined and implemented in the superclass Type; we'll be adding more as we go along. Here's what Type looks like so far:

public class Type implements java.io.Serializable { private int value; private transient String name; protected Type( int value, String name ) { this.value = value; this.name = name; } public int getValue() { return value; } public String toString() { return name; } } 

Back to persistence

With our new framework in hand, we can continue where we left off in the discussion of persistence. Remember, we can save our types by storing their integer values, but now we want to restore them. This is going to require a lookup -- a reverse calculation to locate the object instance based on its value. In order to perform a lookup, we need a way to enumerate all of the possible types.

In Eric's article, he implemented his own enumeration by implementing the constants as nodes in a linked list. I'm going to forego this complexity and use a simple hashtable instead. The key for the hash will be the integer values of the type (wrapped in an Integer object), and the value of the hash will be a reference to the type instance. For example, the GREEN instance of Color would be stored like so:

 hashtable.put( new Integer( GREEN.getValue() ), GREEN ); 

Of course, we don't want to type this out for each possible type. There could be hundreds of different values, thus creating a typing nightmare and opening the doors to some nasty problems -- you might forget to put one of the values in the hashtable and then not be able to look it up later, for instance. So we'll declare a global hashtable within Type and modify the constructor to store the mapping upon creation:

 private static final Hashtable types = new Hashtable(); protected Type( int value, String desc ) { this.value = value; this.desc = desc; types.put( new Integer( value ), this ); } 

But this creates a problem. If we have a subclass called Color, which has a type (that is, Green) with a value of 5, and then we create another subclass called Shade, which also has a type (that is Dark) with a value of 5, only one of them will be stored in the hashtable -- the last one to be instantiated.

In order to avoid this, we have to store a handle to the type based on not only its value, but also its class. Let's create a new method to store the type references. We'll use a hashtable of hashtables. The inner hashtable will be a mapping of values to types for each specific subclass (Color, Shade, and so on). The outer hashtable will be a mapping of subclasses to inner tables.

This routine will first attempt to acquire the inner table from the outer table. If it receives a null, the inner table doesn't exist yet. So, we create a new inner table and put it into the outer table. Next, we add the value/type mapping to the inner table and we're done. Here's the code:

 private void storeType( Type type ) { String className = type.getClass().getName(); Hashtable values; synchronized( types ) // avoid race condition for creating inner table { values = (Hashtable) types.get( className ); if( values == null ) { values = new Hashtable(); types.put( className, values ); } } values.put( new Integer( type.getValue() ), type ); } 

And here's the new version of the constructor:

 protected Type( int value, String desc ) { this.value = value; this.desc = desc; storeType( this ); } 

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

Благодаря нашей организации хеш-таблиц, невероятно просто раскрыть функциональность перечисления, предлагаемую реализацией Эрика. Единственное предостережение: сортировка, предлагаемая Эриком, не гарантируется. Если вы используете Java 2, вы можете заменить отсортированную карту внутренними хэш-таблицами. Но, как я сказал в начале этой колонки, сейчас меня интересует только версия JDK 1.1.

Единственная логика, необходимая для перечисления типов, - это получить внутреннюю таблицу и вернуть ее список элементов. Если внутренняя таблица не существует, мы просто возвращаем null. Вот весь метод: