Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 8 Общие вопросы программирования

Запустив программу, вы выясните, что можете позволить себе три конфеты и у вас останется еще $ 0 .39999999999999999 . Но это неправильный ответ! Правильный путь решения задачи

заключается в п р и м е н ен и и для д е н е ж н ы х расчетов типов Big-

Decimal, int или long. Представим простое преобразование преды­ дущей программы, которое позволяет использовать тип BigDecimal

вместо double:

public static void main(String[] args) {

final BigDecimal TEN_CENTS = new BigDecimal( “.10"): int itemsBought = 0;

BigDecimal funds = new BigDecimal(“1.00”); for (BigDecimal price = TEN_CENTS;

funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) { itemsBought++;

funds = funds.subtract(price);

}

System.out.println(itemsBought + “ items bought.”); System.out.println("Money left over: $” + funds);

}

Запустив исправленную программу, вы обнаружите, что можете позволить себе четыре конфеты и у вас останется $0,00. Это верный ответ. Однако тип BigDecimal имеет два недостатка: он не столь удо­ бен и медленнее, чем простой арифметический тип. Последнее можно считать несущественным, если вы решаете единственную маленькую задачу, а вот неудобство может раздражать.

Вместо BigDecimal можно использовать int или long (в за ­ висимости от обрабатываемых величин) и самостоятельно отсле­ живать положение десятичной точки. В нашем примере расчеты лучше производить не в долларах, а в центах. Продемонстрируем этот подход:

public static void main(String[] args) { int itemsBought - 0;

300

С татья 49

int funds = 100;

for (int price = 10; funds >= price; price += 10) { funds -= price;

itemsBought++;

}

System.out.println(itemsBought + " items bought.”); System, out. println(‘‘Money left over: $” + funds);

}

Подведем итоги. Для вычислений, требующих точного резуль­ тата, не используйте типы float и double. Если вы хотите, что­ бы система сама отслеживала положение десятичной точки, и вас не пугают неудобства, связанные с отказом от простого типа, ис­ пользуйте BigDecimal. Применение этого класса имеет еще то пре­ имущество, что он дает вам полный контроль над округлением: для любой операции, завершающейся округлением, предоставляется на выбор восемь режимов округления. Это пригодится, если вы бу­ дете выполнять экономические расчеты с жестко заданным алго­ ритмом округления. Если для вас важна производительность, вас не пугает необходимость самостоятельно отслеживать положение десятичной точки, а обрабатываемые значения не слишком велики, используйте тип int или long. Если значения содержат не более де­ вяти десятичных цифр, можете применить тип int. Если в значении не больше восемнадцати десятичных цифр, используйте тип long. Если же в значении более восемнадцати цифр, вам придется рабо­

тать с BigDecimal.

Отдавайте предпочтение использованию обычных примитивных типов,

ане упакованных примитивных типов

Уязыка Java есть система, состоящая из двух частей, — прими­

тивные типы, такие как int, double и Boolean, и типы ссылок, такие K a K S t r i n g H L i s t . Каждый примитивный тип обладает соответству­ ющим типом ссылок, называемых упакованными примитивными ти­

301

Глава 8 Общие вопросы программирования

пами. Упакованные примитивные типы, соответствующие простым

примитивным типам int, double и Boolean, являются Intefer, Double

иBoolean.

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

Между обычными и упакованными примитивными типами су­ ществует три главных отличия. Во-первых, у обычных примитив­ ных типов есть только их значения, в то время как у упакованных примитивных типов есть еще и идентичность, отличающаяся от их значения. Другими словами, экземпляры двух упакованных прими­ тивных типов могут иметь одно и то же значение, но разные иден­ тичности. Во-вторых, у обычных примитивных типов есть только функциональные значения, в то время как упакованные примитив­ ные типы имеют только одно нефункциональное значение, пи11,

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

Рассмотрим следующий компаратор, созданный для представ­ ления в возрастающем порядке значения Integer. (Вспоминаем, что метод компаратора Compare возвращает число — отрицательное, по­ ложительное или ноль, в зависимости от того, является ли первый ар­ гумент меньше, равен или больше второго.). На практике вам не нуж­ но писать компаратор, так как он реализует натуральный порядок

вInteger, который можно получить и без компаратора, но пример

получается интересный:

302

С татья 49

// Нерабочий компаратор - можете ли найти, где ошибка?

Comparator<Integer> naturalOrder = new Comparator<Integer>() { public int compare(Integer first, Integer second) {

return first < second ? -1 (first == second ? 0 : 1);

}

};

Этот компаратор кажется нормальным и пройдет много тестов. Например, его можно использовать с Collections, sort, чтобы пра­ вильно отсортировать список из миллионов элементов, вне зави­ симости от того, содержатся ли в списке дублирующие элементы. Но у этого компаратора существенные недостатки. Чтобы убедиться в этом, просто напечатайте значения Natural. Order. compare(new In-

teger(42), newlnteger(42)). Оба экземпляра Integer представляют

одно и то же значение (42), так что значение этого выражения долж­ но быть 0, но оно на самом деле 1, что отражает, что первое значение Integer больше, чем второе.

Итак, в чем же проблема? Первая проверка в naturalOrder сра­ батывает нормально. Оценка выражения first < second приводит к тому, что экземпляр Integer, на который ссылаются first и second, становится автоматически распакован; т.е. он извлекает свои прими­ тивные значения. Проверка продолжает проверять, является ли пер­ вый результат значения int меньше второго. Но предположим, что нет. Тогда следующая проверка проверяет выражение first == second, который выполняет сравнение идентичностей двух ссылок на объект. Если first и second ссылаются на различные экземпляры Integer, представляющие различные значения int, то это сравнение вернет значение false и компаратор ошибочно выведет значение 1, которое означает, что первое значение Integer больше второго. И с­ пользование оператора == на упакованных примитивных типах почти всегда неверно.

Самый понятный способ решения проблемы — это добавить две локальные переменные для хранения примитивных значений int, со­ ответствующих first и second, и выполнять все сравнения на этих

303

Глава 8 Общие вопросы программирования

переменных. Это поможет избежать ошибочного сравнения их иден­ тичностей:

Comparator<Integer> naturalOrder = new Comparator<Integer>() { public int compare(Integer first, Integer second) {

int f = first; // Auto-unboxing int s = second; // Auto-unboxing

return f < s ? -1 : (f == s ? 0 : 1); //No unboxing

}

};

Теперь рассмотрим эту небольшую программу:

public class Unbelievable { static Integer i;

public static void main(String[] args) { if (i == 42)

System.out.println(“Unbelievable”);

}

}

Нет, она не печатает Unbelievable — но что она делает, почти так же странно. Она выдает ошибку NullPointerException при сравне­ нии выражения (I == 42). Проблема в том, что i — это Integer, а не int и, как и все поля ссылки на объекты, его начальное значение null. Когда программа проверяет выражение (i == 42), она сравнивает Integer и int. Почти в любом случае, когда вы смешиваете обыч­ ные и упакованные примитивные типы одной операцией, упакован­ ный примитивный тип автоматически распаковывается, и это не вы­ зывает ошибки. Если нулевая ссылка на объект распаковывается, то вы получите NullPointerException. То, что демонстрирует данная программа, может случиться где угодно. Исправить программу так же просто, как объявить, что i является int, а не Integer.

Наконец, рассмотрим программу (статья 5):

// Ужасно медленная программа! Можете найти, где создается объект? public static void main(String[] args) {

Long sum = OL;

304

С татья 49

for (long i = 0; i < Integer.MAX_VALUE; i++) { sum += i;

}

System.out.println(sum);

}

Эта программа намного медленнее, чем должны быть, потому что она случайно декларирует, что локальная переменная (sum) является упакованным примитивным типом Long, а не обычным примитивным типом long. Программа компилируется без ошибок или предупреж­ дений, и переменная постоянно упаковывается и распаковывается, снижая производительность.

У всех трех программ, которые мы обсудили, одна и та же про­ блема: программист проигнорировал различия между обычными при­ митивными типами и упакованными примитивными типами, что при­ вело к неприятным последствиям. В первых двух случаях это привело к ошибкам при запуске, в третьем случае — серьезному снижению производительности

Итак, когда же надо использовать упакованные примитивные типы? У них есть несколько разрешенных видов использования. Во-первых, в качестве элементов, ключей и значений коллекций. Вы не можете поместить обычные примитивные типы в коллекцию, поэтому вам придется использовать упакованные примитивные типы. Это частный случай более общего. Вы должны использовать упако­ ванные примитивные типа в качестве параметров типа в типах с па­ раметрами (глава 5), потому что язык не позволяет вам использовать обычные примитивные типы. Например, вы не можете объявить пе­ ременную, чтобы она имела тип ThreadLocal<int>, вместо этого вам нужно использовать ThreadLocal<Integer>. Наконец, вы должны ис­ пользовать упакованные примитивные типы при запуске рефлектив­ ных методов (статья 53).

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

305

Глава 8 Общие вопросы программирования

использовать упакованные примитивные типы, будьте осторожны!

Автоупаковка уменьшает многословность, но не опасность ис­ пользования упакованных п р и м и т и в н ы х типов. Когда ваша про­

грамма сравнивает два упакованных примитивных типа оператором ==, то происходит сравнение идентичностей, что, скорее всего, совсем не то, что вам нужно. Когда программа выполняет расчеты и с обыч­ ными и с упакованными примитивными типами, она выполняет рас­ паковку, а когда ваша программа выполняет распаковку, она может вывести ошибку NullPointerException. Наконец, когда программа упаковывает примитивные значения, это может привести к затратно­ му и ненужному созданию объектов.

Не используйте строку там, где более уместен иной тип

Тип String создавался для того, чтобы представлять текст, и де­ лает он это прекрасно. Поскольку строки широко распространены и имеют хорошую поддержку в языке Java, возникает естественное желание использовать строки для решения тех задач, для которых они не предназначались. В этой статье обсуждается несколько опе­ раций, которые не следует проделывать со строками.

С т р о к и — плохая замена другим типам значений. Когда дан­

ные попадают в программу из файла, сети или с клавиатуры, они ча­ сто имеют вид строки. Естественным является стремление оставить их в том же виде, однако это оправданно лишь тогда, когда данные по своей сути являются текстом. Если получены числовые данные, они должны быть приведены к соответствующему числовому типу, такому как int, float или Big Intege г. Ответ на вопрос «да/нет» следует преобразовать в boolean. В общем случае, если есть соот­ ветствующий тип значения (примитивный либо ссылка на объект), вы обязаны им воспользоваться. Если такового нет, вы должны его написать. Этот совет кажется очевидным, но его часто нарушают.

306

С татья 50

Строки — плохая замена перечислениям. Как говорилось в статье 30, и перечисления типов, и перечисления целых чисел лучше представлять в виде констант перечисления, а не в виде строк.

Строки — плохая замена составным типам (aggregate type).

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

// Неправомерное использование строки в качестве составного типа String compoundKey = className + “#" + i.nextQ;

Такой подход имеет множество недостатков. Если в одном из по­ лей встретится символ, используемый для разделения, может возник­ нуть беспорядок. Для получения доступа к отдельным полям вы должны выполнить разбор строки, а это медленная, трудоемкая и подверженная ошибкам операция. У вас нет возможности создать методы equals, toString и compareTo, и вы вынуждены принять решение, предлагаемое классом St ring. Куда лучше написать класс, представляющий составной тип, часто это бывает закрытый статический класс-член (статья 22).

Строки — плохая замена мандатам. Иногда строки использу­ ются для обеспечения доступа к неким функциональным возможно­ стям. Например, рассмотрим механизм создания переменных, привя­ занных к определенному потоку. Этот механизм позволяет создавать переменные, в которых каждый поток может хранить собственное значение. У библиотек Java с версии 1.2 есть возможность использо­ вания локально-потоковых переменных, но до этого программистам приходилось выкручиваться самим. Несколько лет назад, столкнув­ шись с необходимостью реализации такого функционала, несколько человек независимо друг от друга пришли к одному и тому же ре­ шению, при котором доступ к подобной переменной осуществляется через строку-ключ, предоставляемую клиентом:

// Ошибка: неправомерное использование класса String в качестве мандата public class ThreadLocal {

307

Глава 8 Общие вопросы программирования

private ThreadLocalO { } //Не порождает экземпляров

//Заносит в именованную переменную значение,

//соответствующее текущему потоку

public static void set(String key, Object value);

//Извлекает из именованной переменной значение,

//соответствующее текущему потоку

public static Object get(String key);

}

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

A PI можно исправить, если эту строку заменить неподделываемым ключом (его иногда называют мандатом (capability)):

public class ThreadLocal

{

private ThreadLocalO

{ } //He порождает экземпляров

public static class Key { KeyО { }

}

// Генерирует уникальный, неподделываемый ключ public static Key getKeyO {

return new Key();

}

public static void set(Key key, Object value); public static Object get(Key key);

}

Это решает обе проблемы с API, использующим строки, но можно сделать еще лучше. В действительности статические

308

С татья 50

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

в ThreadLocal:

public final class ThreadLocal { private ThreadLocal();

public void set(Object value); public Object get();

}

Этот API не является безопасным, потому что необходимо пере­ давать значение от Object его актуальному типу, когда вы извлекае­ те его из привязанной к потоку локальной переменной. Невозможно сделать безопасным оригинальный API на основе строки, и сложно сделать безопасным API на основе ключа, но очень просто сделать A PI безопасным, обобщив класс ThreadLocal (статья 26):

public final class ThreadLocal<T> { public ThreadLocal();

public void set(T value); public T get();

}

Такой A PI реализует java. util.ThreadLocal. Помимо того, что этот интерфейс разрешает проблемы API, применяющего строки, он быстрее и элегантнее любого API, использующего ключи.

Подведем итоги. Не поддавайтесь естественному желанию пред­ ставить объект в виде строки, если для него имеется или может быть написан более приемлемый тип данных. Если строки используются неправильно, они оказываются более громоздкими, менее гибкими, более медленными и подверженными ошибкам, чем другие типы. Как правило, строки ошибочно применяются вместо простых и составных типов, перечислений.

309

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]