Джош Блох
.pdfГлава 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