Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Programming Language Guide



Programming Language Guide

Joshua Bloch

ADDISON- WESLEY

 

 

Java TM

         Эффективное программирование

     Джошуа Блох

 

 

                                   Издательство «Лори»

 

Благодарности

я благодарю Патрика Чана (Patrick Chan) за то, что он посоветовал мне написать эту книгу и подбросил идею Лайзе Френдли (Lisa Friendly), главному редактору серии, а также Тима Линдхолма (Tim Lindholm), технического редактора серии, и Майка Хендриксона (Mike Hendrickson), исполнительного редактора издательства Addison- Wesley Professional. Спасибо Лайзе, Тиму и Майку за их поддержку при реализации проекта, за сверхчеловеческое терпение и несгибаемую веру в то, что когда-нибудь я напишу эту книгу.

Я благодарю Джеймса Гослинга (James Gosling) и его незаурядную команду" за то, что они предоставили мне нечто значительное, о чем можно написать, а также многих разработчиков платформы Java, последователей ДжеЙмса. В особенности я благодарен моим коллегам по работе в компании Sun из Java Platform Тools and Libraries Group за понимание, одобрение и поддержку. В эту группу входят Эндрю Беннетт (Andrew Benriett), Джо Дарси Оое Darcy), Нил Гафтер (Neal Gafter), Айрис Гарсиа (Iris Garcia), Константин Кладко (Konstantin Кladko), Йена Литтл (Ian Little), Майк Маклоски (Mike McCloskey) и Марк Рейнхольд (Mark Reinhold). Среди бывших членов группы: Дзенгуа Ли (Zhenghua Li), Билл Мэддокс (Bill Maddox) и Нейвин Санджива (Naveen Sanjeeva).

Выражаю благодарность моему руководителю Эндрю Беннетту (Andrew Bennett) и директору Ларри Абрахамсу (Larry Abrahams) за полную и страстную поддержку этого проекта. Спасибо Ричу Грину (Rich Green), вице-президенту компании Java Software, за создание условий, при которых разработчики имеют возможность творить и публиковать свои труды.

Мне чрезвычайно повезло с самой лучшей, какую только можно вообразить, группой рецензентов, и я выражаю мои самые искренние благодарности каждому из них: Эндрю Беннетту (Andrew Bennett), Синди Блох (Cindy Вloch), Дэну Блох (Dan Вloch), Бет Ботос (Beth Bottos), Джо Баубиеру Оое Bowbeer), Джиладу Браче (Gilad Bracha), Мэри Кампьон (Mary Campione), Джо Дарси Оое Darcy), Дэвиду Экхардту (David Eckhardt), Джо Фьалли Оое Fialli), Лайзе Френдли (Lisa Friendly), Джеймсу Гослингу (James Gosling), Питеру Хаггеру (Peter Haggar), Брайену КеРl:lигану (Brian Kernighan), Константину Кладко (Konstantin Кladko), Дагу Ли (Doug Lea), Дзенгуа Ли (Zhenghua Li), Тиму Линдхолму (Tim Lindholm), Майку Маклоски (Mike McCloskey), Тиму Пейерлсу (Tim Peierls), Марку Рейнхолду (Mark Reinhold), Кену Расселу (Ken Russell), Биллу Шэннону (ВШ· Shannon), Питеру Стауту (Peter Stout), Филу Уодлеру (Phil Wadler), Давиду Холмсу (David Holmes) и двум анонимным рецензентам. Они внесли множество предложений, кото­рые позволили существенно улучшить книгу и избавили меня от многих затруднений. Все оставшиеся недочеты полностью лежат на моей совести.

Многие мои коллеги, работающие в компании Sun и вне ее, участвовали в техни­ческих дискуссиях, которые улучшили качество этой книги. Среди прочих: Бен Гомес (Ben Gomes), Стефен Грерап (Steffen Grarup), Питер Кесслер (Peter Kessler), Ричард Рода (Richard Roda), Джон Роуз (John Rose) и Дэвид Стаутэмайер (David Stoutamire). Особая благодарность Дагу Ли (Doug Lea), озвучившему многие идеи этой книги. Даг неизменно щедро делился своим временем и знаниями.

Я благодарен Джули Дайникола (Julie Dinicola), Джекки Дусетт (Jacqui Doucette), Майку Хендриксону (Mike Hendrickson), Хизер Ольщик (Heather Olszyk), Трейси Расс (Tracy Russ) и всем сотрудникам Addison-Wesley за их поддержку и Профессионализм. Даже будучи занятыми до предела, они всегда были дружелюбны и учтивы.

Я благодарю Гая Стила (Сиу Steele), написавшего предисловие. Его участие в этом проекте - большая честь для меня.

Наконец, спасибо моей жене Синди Блох (Cindy Вloch), которая своим ободре­нием, а подчас и угрозами помогла мне написать эту книгу. Благодарю за чтение каж­дой статьи в необработанном виде, за помощь при работе с программой Framemaker, за написание предметного указателя и за то, что терпела меня, пока я корпел над этой книгой.

 

 

XlV

 

Содержание

 

Предисловие

Предисловие автора

 

Введение

Классы и интерфейсы

 

12. Сводите к минимуму доступность классов и членов………………………57

13. Предпочтитайте постоянство………………………………………………….61

14. Предпочитайте компановку наследованию…………………………………69

15. Проектируйте и документируйте наследование либо запрещайте его…………………………………………………………………………………..75

16. Предпочитайте интерфейсы абстрактным классам……………………….80

17. Используйте интерфейсы только для определения типов……………….85

18. Предпочитайте статистические классы-члены нестатическим………….87

 

Методы

23. Проверяйте достоверность параметров……………………………………112

24. При необходимости создавайте резервные копии……………………….114

25. Тщательно проектируйте сигнатуру…………………………………………118

26. Перезагружая методы, соблюдайте осторожность……………………….120

27. Возвращайте массив нулевой длины, а не null……………………………125

28. Для всех открытых элементов АРI пишите dос-комментарии………….127

 

 

7. Общие вопросы программирования

29. Сводите к минимуму область видимости локальных переменных…….132

30. Изучите библиотеки и пользуйтесь ими…………………………………….135

31. Если требуются точные ответы, избегайте использования типов float и doubIe……………………………………………………………………………..139

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

33. При конкатенации строк опасайтесь потери производительности…………144

34. Для ссылки на объект используйте его интерфейс…………………………...145

35. Предпочитайте интерфейс отражению класса………………………………...147

36. Соблюдайте осторожность при использовании машинно-зависимых методов……………………………………………………………………………….150

37. Соблюдайте осторожность при оптимизации………………………………….151

38. выборе имен придерживайтесь общепринятых соглашений………………..154

 

Исключения

39. Используйте исключения лишь в исключительных ситуациях……………158

40. Применяйте обрабатываемые исключения для восстановления, для программных ошибок используйте исключения времени выполнения…………………………………………………………………………161

41. Избегайте ненужных обрабатываемых исключений…………………………163

42. Предпочитайте стандартные исключения……………………………………..165

43. Инициируйте исключения, соответствующие абстракции…………………..167

44. Для каждого метода документируйте все инициируемые исключения…………………………………………………………………………..170

45. В описание исключения добавляйте информацию о сбое…………………..171

46. Добивайтесь атомарности методов по отношению к сбоям…………………173

47. Не игнорируйте исключений………………………………………………………175

 

Потоки

48. Синхронизируйте доступ потоков к совместно используемым изменяемым данным………………………………………………………………………………..177

49. Избегайте избыточной синхронизации………………………………………….183

50. Никогда не вызывайте метод wait вне цикла…………………………………..188

51. Не попадайте в зависимость от планировщика потоков……………………..191

52. При работе с потоками документируйте уровень безопасности……………194

53. Избегайте группировки потоков…………………………………………………..197

 

Сериализация

 

54. Соблюдайте осторожность при реализации интерфейса SerializabIe…………………………………………………………………………...199

55. Рассмотрите возможность использования специализированной сериализованной формы…………………………………………………………..204

56. Метод readObject должен создаваться с защитой…………………………….210

57. При необходимости создавайте метод readResolve………………………….217

 

Литература

 

Предисловие

Если бы сослуживец сказал вам: "Моя супруга сегодня вечером готовит дома нечто необычное. Придешь?" (Spouse of me this night today manufactures the unusual meal in а home. You will join?), вам в голову, вероятно, пришли бы сразу три мысли: вас уже пригласили на обед; английский язык не является родным для вашего сослуживца; ну и прежде всего это слишком большое беспокойство.

Если вы сами когда-нибудь изучали второй язык, а затем пробовали пользоваться им за пределами аудитории, то вам известно, что есть три вещи, которые необходимо знать: каким образом структурирован язык (грамматика), как называется то, о чем вы хотите сказать (словарь), а также общепринятые и Эффективные варианты повсе­дневной речи (лексические обороты). В аудитории обычно ограничиваются изучением лишь первых двух из этих вещей, и вы обнаруживаете, что окружающие постоянно давятся от смеха, выслушивая, как вы пытаетесь говорить понятно.

Практически так же обстоит дело с языком программирования. Вы должны понимать суть языка: является ли он алгоритмическим, функциональным, объектно-ориентированным. Вам нужно знать словарь языка: какие структуры данных, опера­ции и возможности' предоставляют стандартные библиотеки. Кроме того, вам необхо­димо ознакомиться с общепринятыми и эффективными способами структурирования Кода. В книгах, посвященных языкам программирования, часто освещаются лишь первые два вопроса, приемы работы с языком, если и обсуждаются, то лишь кратко. Возможно, это происходит потому, что о первых двух вещах писать несколько проще. Грамматика и словарь - это свойства самого языка, тогда как способ его применения характеризует группу людей, пользующихся этим языком.

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

Есть и другой аспект. Программы, в отличие от произнесенных фраз и большин­ства изданных книг и журналов, имеют возможность меняться со временем. Недоста­точно создать программный код, который эффективно работает и без труда может быть понят другими людьми. Нужно еще организовать этот код таким образом, чтобы его можно было легко модифицировать для не которой задачи А существует десяток вариантов написания программного кода. Из этих десяти семь оказываются неуклю­жими, неэффективными или запутывающими читателя. Какой же из оставшихся трех вариантов будет представлять собой программный код, который потребуется в следую­щем году для новой версии программы, ,решающей задачу А'?

Существует много книг, по которым можно изучать грамматику языка програм­мирования Jаvа, в том числе книги "The Java Prograттiпg Laпguage" авторов Arnold, Gosling и Holmes [ArnoldOO] и "The Java Laпguage Spec if icatioп" авторов Gosling, Jоу, Bracha и вашего покорного слуги [JLS]. Немало книг посвящено библиотекам и при­кладным интерфейсам, связанным с Jаvа.

Эта книга посвящена третьей теме: общепринятым и эффективным приемам работы с языком Jаvа. На протяжении нескольких лет Джошуа Блох (Joshua Blосk) трудился в компании Sun Microsystems, работая с языком программирования Jаvа, занимаясь расширением и реализацией программного кода. Он изучил большое коли­чество программ, написанных многими людьми, в том числе и мною. 8 настоящей книге он дает дельные советы о том, каким образом структурировать код, чтобы он ра­ботал хорошо, чтобы его могли понять другие люди, чтобы последующие модификации и усовершенствования доставляли меньше головной боли и чтобы ваши программы были приятными, элегантными и красивыми.

Гай А. Стuл-младшuй (Сиу L. Steele Jr.) Берлингтон, шт. Массачусетс Апрель 2001

 

        Предисловие автора

 

 

в 1996г. я направился на запад, в компанию JavaSoft, как она тогда называлась, поскольку было очевидно, что именно там происходят главные события. На протяже­нии пяти лет я работал архитектором библиотек для платформы Java. Я занимался проектированием, разработкой и обслуживанием этих библиотек, а также давал консультации по многим другим библиотекам. Контроль над библиотеками в ходе становления платформы языка Java - такая возможность предоставляется раз в жизни. Не будет преувеличением сказать, что я имел честь трудиться бок о бок с великими разработчиками нашего времени. Я многое узнал о языке программирова­ния Java: что в нем работает, а что нет, как пользоваться языком и его библиотеками для получения наилучшего результата.

Эта книга является попыткой поделиться с вами моим опытом, чтобы вы смогли повторить мои успехи и избежать моих неудач. Оформление книги я позаимствовал из руководства Скотта Мейерса (Scott Meyers) "Effective С++" [Meyers98]; оно состоит из пятидесяти статей, каждая из которых посвящена одному конкретному правилу, направленному на улучшение программ и проектов. Я нашел такое оформле­ние необычайно эффективным, и надеюсь, вы тоже его оцените.

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

Эта книга предназначена не только для тех, кто занимается разработкой повтор­но используемых компонентов, тем не менее она неизбежно отражает мой опыт в написании таковых, накопленный за последние два десятилетия. Я привык думать в терминах прикладных интерфейсов (API) и предлагаю вам делать то же. Даже если вы не занимаетесь разработкой повторно используемых компонентов, примене­ние этих терминов поможет вам повысить качество ваших программ. Более того, нередко случается писать многократно используемые компоненты, не подозревая об этом: вы создали нечто полезное, поделились своим результатом с приятелем, и вскоре у вас будет уже с полдюжины пользователей. С этого момента вы лишае­тесь возможности свободно менять этот АР! и получаете благодарности за все те усилия, которые потратили на его разработку, когда писали программу в первый раз.

Мое особое внимание к разработке АР! может показаться несколько противо­естественным для ярых приверженцев новых облегченных методик создания програм­много обеспечения, таких как "Экстремальное программирование" [Beck99]. В этих методиках особое значение придается написанию самой простой программы, какая только сможет работать. Если вы пользуетесь одной из этих методик, то обнаружите, что внимание к АРI сослужит вам добрую службу в процессе последующей перестройки программы (refactoring). Основной задачей перестроения является усовер­шенствование структуры системы, а также исключение дублирующего программного кода. Этой цели невозможно достичь, если у компонентов системы нет хорошо спроектированного API.

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

Джошуа Блох

Купертино, шт. Калифорния

Апрель 2001

 

                       Глава 1

Введение

Эта книга писалась с той целью, чтобы помочь вам наиболее эффективно ис­пользовать язык программирования Jаvа ТМ и его основные библиотеки jаvа.lang, java.util и java.io. В книге рассматриваются и другие библиотеки, но мы не касаем­ся графического интерфейса пользователя и специализированных API.

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

Большинство статей сопровождается примерами программ. Главной особенностью этой книги является наличие в ней примеров программного кода, иллюстрирующих многие шаблоны (design pattern) и идиомы. Некоторые из них, такие как Singleton (статья 2), известны давно, другие появились недавно, например Finalizer Guardian (статья б) и Defensive readResolve (статья 57). Где это необходимо, шаблоны и идио­мы имеют ссылки на основные работы в данной области [Саmmа95].

Многие статьи содержат при меры программ, иллюстрирующие приемы, которых следует избегать. Подобные примеры, иногда называемые "антишаблонами", четко обозначены комментарием "// никогда не делайте так!" В каждом таком случае в статье дается объяснение, почему пример плох, и предлагается альтернатива.

Эта книга не предназначена для начинающих: предполагается, что вы уже хорошо владеете языком программирования Java. В противном случае обратитесь к одному из множества прекрасных изданий для начинающих [ArnoldOO, CampioneOO]. Книга построена так, чтобы быть доступной для любого, кто работает с этим языком, тем не менее она дает пищу для размышлений даже опытным программистам.

 

1

 

В основе большинства правил этой книги лежит несколько фундаментальных принципов. Ясность и простота имеют первостепенное значение. Функционирование модуля не должно вызывать удивление у него пользователя. Модули Должны быть на­столько компактны, насколько это возможно, но не более того. (В этой книге термин "модуль" относится к любому программному компоненту, который используется много раз: от отдельного метода до сложной системы, состоящей из нескольких пакетов.) Программный код следует использовать повторно, а не копировать. Взаимозависи­мость между модулями должна быть сведена к минимуму. Ошибку нужно выявлять как можно раньше, в идеале - уже на стадии компиляции.

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

Большая часть этой книги посвящена отнюдь не производительности программ.

Речь идет о написании понятных, прав ильных, полезных, надежных, гибких программ, которые удобно сопровождать. Если вы сможете сделать это, то добиться необхо­димой производительности будет несложно (статья 37). В некоторых статьях об­суждаются вопросы производительности, в ряде случаев приводятся показатели производительности. Эти данные, предваряемые выражением "на моей машине", следует рассматривать как приблизительные.

Для справки, моя машина - это старый компьютер домашней сборки с процес­сором 400 МГц Pentium® II и 128 Мбайт оперативной памяти под управлением Мiсrosоft Windows NT® 4.0, на котором установлен Java 2 Standard Edition Software Development Kit (SDK) компании Sun. В состав этого SDK входит Java HotSpot тм Client УМ компании Sun - финальная реализация виртуальной машины Java, пред­назначенной для клиентов.

При обсуждении особенностей языка программирования Jаvа и его библиотек иногда необходимо ссылаться на конкрет­ные версии для краткости в этой книге используются "рабочие", а не официальные номера версий. В таблице 1.1 показано со­ответствие между названиями версий и их рабочими номерами.

В некоторых статьях обсуждаются возможности, появившиеся в версии 1.4, однако в примерах программ, за редким 'исключением, я воздерживался от того, чтобы пользоваться ими. Эти примеры были проверены в версии 1.3. Большинство из них, если не все, без всякой переделки должны работать также с версией 1.2.

 

Официальное название версии Рабочий номер версии
JDK 1.1.x / JRE 1.1.x 1.1
Java 2 Platform, Standard Edition, v1.2 1.2
Java 2 Platform, Standard Edition, v1.3 1.3
Java 2 Platform, Standard Edition, v1.4 1.4

 

2

 

 

Примеры по возможности являются полными, однако предпочтение отдается не завершенности, а удобству чтения. В примерах широко используются классы пакеТ08 java.util и java. 10. Чтобы скомпилировать пример, вам потребуется добавить один или оба оператора import:

Import java.ut11.*;

Import java.io.*;

В примерах опущены детали. Полные версии всех примеров содержатся на web-сайте этой книги (http://java.sun.com/docs/books/effective). При желании любой из них можно скомпилировать и запустить.

Технические термины в этой книге большей частью используются в том виде, как они определены в "The J а v а Laпguage Speci f icatioп, Secoпd Editioп" [JLS]. Однако некоторые термины заслуживают отдельного упоминания. Язык Java поддерживает четыре группы типов: интерфейсы (interface), классы (class), массивы (аrrау) и простые типы (primitive). Первые три группы называются ссылочными типами (reference type). Экземпляры классов и массивов '- это объекты, значения простых типов таковыми не являются. К членам класса (members) относятся его поля (fields), методы (methods), классы-члены (member classes) и интерфейсы-члены (mernber interfaces). Сигнатура метода (signature) состоит из его названия и типов, которые имеют его формальные параметры. Т ил значения, возвращаемого методом, в сигнатуру не входит.

Некоторые термины в этой книге используются в ином значении, чем в "The J а v а .

Laпguage Speci/icatioп". Так, "наследование" (inheritance) применяется как синоним "образования подклассов" (subclassing). Вместо использования для интерфейсов тер­мина "наследование" в книге констатируется, что некий класс реализует (implement) интерфейс или что один интерфейс является расширением другого (extend) для описания' уровня доступа, который применяется, когда ничего больше не указано, в книге используется описательный термин "доступ только в пределах пакета" (package-private) вместо формально правильного термина "доступ по умолчанию" (default access) [JLS, 6.6.1].

      В этой книге встречается несколько технических терминов, которых нет в "The J а v а La n guage Speci f icatio n ". Термин "внешний АР!" (exported API), или просто АР I ”  относится к классам, интерфейсам, конструкторам, членам и сериализованным формам, с помощью которых программист получает доступ к классу, интерфейсу или пакету. (Термин API , являющийся сокращением от applicatio n programmi n g i n terface - программный интерфейс приложения, используется вместо термина "интерфейс" (interface). Это позволяет избежать путаницы с одноименной конструкцией языка Java.) Программист, который пишет программу, применяющую некий API, называется пользователем (user) указанного API. Класс, в реализации которого используется некий API, называется клиентом (client) этого API.

          

 

3

 

Классы, интерфейсы, конструкторы, члены и сериализованные формы называ­ются элементами АР/ (API element). Внешний API образуется из элементов API, которые доступны за пределами пакета, где этот  API был определен. Указанные элементы может использовать любой клиент, автор АР! берет на себя их поддержку. Неслучайно документацию именно к этим элементам генерирует утилита Javadoc при запуске в режиме по умолчанию. В общих чертах, внешний АР! пакета состоит из открытых (pubIic) и защищенных (protected) членов, а также из конструкторов всех открытых классов и интерфейсов в пакете.

 

 

4

 

 

 

Глава 2

Глава 3

Глава 4

Классы и интерфейсы

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

 

Предпочитайте постоянство

Неизменяемый класс - это такой класс, экземпляры которого нельзя поменять.

Вся информация, содержащаяся в любом его экземпляре, записывается в момент его создания и остается неизменной в течение всего времени существования этого объекта. В библиотеках для платформы ]ауа имеется целый ряд неизменяемых классов; в том числе String, простые классы-оболочки, Biglnteger и BigDecimal. На это есть много веских причин: по сравнению с изменяемыми классами, их проще проектировать, раз­рабатывать и и<;пользовать. Они менее подвержены ошибкам и более надежны.

Делая класс неизменяемым, выполняйте следующие пять правил:

1. Не создавайте каких-либо методов, которые модифицируют представленный объект (эти методы называются мутаторами  (mutator)).

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

и другие способы (см. ниже).

3. Сделайте все поля окончательными (final). Это ясно выразит

ваши намерения, причем в некоторой степени их будет поддерживать сама система. Это может понадобиться и для обеспечения правильного поведения программы в том случае, когда ссылка на вновь созданный экземпляр передается из одного потока в другой без выполнения синхронизации [Pugh01a] (как результат ведущихся работ

по ис~равлению модели памяти в ]ауа).

4. Сделайте все поля закрытыми (private). Это не позволит клиентам непосредственно менять значение полей. Хотя формально неизменяемые классы и могут иметь открытые поля с модификатором final, которые содержат либо значения простого типа, либо ссылки на неизменяемые объекты, делать это не рекомендуется, поскольку они будут препятствовать изменению в последующих версиях внутреннего представления класса (статья 12).

 

5. Убедитесь в монопольном доступе ко всем изменяемым компонентам. Если в вашем классе есть какие-либо поля, содержащие ссылки на изменяемые объекты, удостоверьтесь в том, что клиенты этого класса не смогут получить ссылок на эти объекты. Никогда не инициализируйте такое поле ссылкой на объект, полученной от клиента, метод доступа не должен возвращать хранящейся в этом поле ссылки на объект. При использовании конструкторов, методов доступа к полям и методов readObject (статья 56) создавайте резервные копии (defensive copies) (статья 24).

 

 

61

 

В при мерах из предыдущих статей многие классы были неизменяемыми. Так, класс PhoneNumber (статья 8) имеет метод доступа для каждого атрибута, но не имеет соответствующего мутатора. ГIредставим более сложный пример:

 

public final class Complex {

private final float rе;

pгivate final float im;

public Complex(float ге, float im) {

this. ге = ге;

this.im = im;

}

// Методы доступа без соответствующих мутаторо

 public float realPart() { return ге; }

 public float imaginaryPart() { return im; }

public Complex add(Complex с) {

return new Complex(re + С.ге, im + c,im);

}

public Complex subtract(Complex с) {

return new Complex(re - С.ге, im - c.im);

}

public Complex multiply(Complex с) {

return new Complex(re*c.гe - im*c.im, re*c.im + im*c. ге);

}

public Complex divide(Complex с) {

float tmp = с. ге*с. ге + c.im*c.im;

return new Complex((re*c. ге + im*c.im)/tmp, (im*c.re - re*c.im)/tmp);

}

public boolean equals(Object о) {

 if (о == this)

return true;

if (!(о instanceof Complex))

return false;

 

с = (Complex)o;

return (Float.floatTolntBits(re) ==Float.floatTolntBits(c.re)) && (Float.floatTolntBits(im) == Float.floatTolntBits(c.im));

}

 

// Чтобы понять,

// почему используется

// метод floatTolntBits

// см. статью 7.

 

public int hashCode() {

int result = 17 + Float.floatTolntBits(re);

result = 37*result + Float.floatTolntBits(im);

return result;

}

public String toString() {

return "(" + ге + " + " + im + "i)";

}

}

 

62

 

 

Данный класс представляет комплексное число (число с действительной и мнимой частями). Помимо обычных методов класса Object, он реализует методы доступа к действительной и мнимой частям числа, а также четыре основные арифметические операции: сложение, вычитание, умножение и деление. Обратите внимание на то, что представленные арифметические операции вместо того, чтобы менять данный экземпляр, генерируют и передают новый экземпляр класса Complex. Такой подход используется для большинства сложных неизменяемых классов. Называется это функциональным подходом (functiona! approach), поскольку рассматриваемые методы возвращают результат применения некоей функции к своему операнду, не изменяя при этом сам операнд. Альтернативой является более распространенный процедурный подход (procedura! approach), при котором метод выполняет для своего операнда некую процедуру, которая меняет его состояние.

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

 

 

63

 

Неизменяемые объекты по своей сути безопасны при работе с потоками (thread-safe): им не нужна синхронизация. Они не могут быть разрушены только из-за того, что одновременно к ним обращается несколько потоков. Несомненно, это самый простой способ добиться безопасности при работе с потоками. Действительно, ни один поток никогда не сможет обнаружить какого-либо воздействия со стороны другого потока через неизменяемый объект. По этой причине неизменяемые объекты можно свободно использовать для совместного доступа. Неизменяемые классы должны задействовать это преимущество, заставляя клиентов везде, где возможно, применять yжe существующие экземпляры. Один из простых приемов, позволяющих достичь этого: для часто используемых значений создавать константы типа publiC static final. Например, в классе Соmрех можно представить следующие константы:

publiC static final Complex ZERO = new Complex(0, 0);

 public static final Complex ONE = new Complex(1, 0);

 public static final Complex I = new Complex(0, 1);

Mожно сделать еще один шаг в этом направлении. В неизменяемом классе MO~HO предусмотреть статические методы генерации, которые кэшируют часто запрашивае­мые экземпляры вместо того, чтобы при каждом запросе создавать новые экземпляры, дублирующие уже имеющиеся. Подобные статические методы генерации есть в клас­сах 8iglnteger и 8oo1ean. Применение статических методов генерации заставляет клиентов совместно использовать уже имеющиеся экземпляры, а не создавать новые. Это снижает расход памяти и сокращает работу по ее освобождению.

Благодаря тому, что неизменяемые объекты можно свободно предоставлять для 'совместного доступа, не требуется создавать для них резервные копии (defensive copies) (статья 24). В действительности вам вообще не нужно делать никаких копий, поскольку они всегда будут идентичны оригиналу. Соответственно, для неизменяемого класса не надо, да и не следует создавать метод clone и конструктор копии (сору constructor) (статья 10). Когда платформа Java только появилась, еще не было четкого понимания этого обстоятельства, и потому класс String имеет конструктор копий. Лучше им не пользоваться (статья 4).

Можно совместно использовать не только неизменяемый объект, но и его содержимое. Например, класс 8iglnteger применяет внутреннее представление знак/модуль (sign/magnitude). Знак числа задается полем типа int, его модуль ­массивом int. Метод инвертирования negate создает новый экземпляр 8iglnteger с тем же модулем и с противоположным знаком. При этом нет необходимости копиро­вать массив, поскольку вновь созданный экземпляр 8iglnteger имеет внутри ссылку на тот же самый массив, что и исходный экземпляр.

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

 

 

64

 

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

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

Biglnteger mоbу = ...;

 Mоbу = moby.flipBit(0);

Метод flip8it создает новый экземпляр класса 8iglnteger длиной также в мил­лион битов, который отличается от своего оригинала только одним битом. Этой опера­ции требуются время и место, пропорциональные размеру экземпляра 8iglnteger. Противоположный подход использует java.util.BitSet. Как и Biglnteger, BitSet представляет последовательность битов произвольной длины, однако, в отличие от BigInteger, BitSet является изменяемым классом. В классе BitSet предусмотрен метод, позволяющий в экземпляре, содержащем миллионы битов, менять значение отдельного бита в течение фиксированного времени.

Проблема производительности усугубляется, когда вы выполняете многошаговую операцию, генерируя на каждом этапе новый объект, а в конце отбрасываете все эти объекты, оставляя только окончательный результат. Справиться с этой проблемой можно двумя способами. Во-первых, можно догадаться, какие многошаговые операции будут требоваться чаще всего, и представить их в качестве элементарных. Если много­шаговая операция реализована как элементарная (primitive), неизменяемый класс уже не обязан на каждом шаге создавать отдельный объект. Изнутри неизменяемый класс может быть сколь угодно хитроумным. Например, у класса Biglnteger есть изменяемый "класс-компаньон", который доступен только в пределах пакета и применяется для ускорения многошаговых операций, таких как возведение в степень по модулю. По всем перечисленным выше причинам использовать изменяемый класс-компаньон гораздо сложнее. Однако делать этого вам, к счастью, не надо. Разработчики класса Biglnteger уже выполнили за вас всю тяжелую работу.

Описанный прием будет работать превосходно, если вам удастся точно предска­зать, какие именно сложные многошаговые операции с вашим неизменяемым классом будут нужны клиентам. Если сделать это невозможно, самый лучший вариант ­создание открытого изменяемого класса-компаньона. В библиотеках для платформы Java такой подход демонстрирует класс String, для которого изменяемым классом­ Компаньоном является StringBuffer. В силу ряда причин BitSet вряд ли играет роль Изменяемого компаньона для Biglnteger.

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

 

65

 

 

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

Второй прием заключается в том, чтобы сделать все конструкторы неизменяемого класса закрытыми либо доступными только в пакете и вместо открытых конструкторов использовать открытые статические методы генерации (статья 1). Для пояснения представим, как бы выглядел класс Complex, если бы применялся такой подход:

// Неизменяемый класс со статическими методами генерации

// вместо конструкторов

publiC class Complex {

private final float ге;

  private final float im;

private Complex(float ге, float im) {

 this. ге = ге;

 this.im = im;

public static Соmрlех valueOf(float ге, float im) {

 return new Сотрlех(ге, im);}                       

// Остальное не изменилось

}

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

Как показано в статье 1, статические методы генерации объектов имеют множест­во преимуществ по сравнению с конструкторами. Предположим, что вы хотите создать механизм генерации комплексного числа, отталкиваясь от его полярных координат. Использовать здесь конструкторы плохо, поскольку окажется, что собст­венный конструктор класса Complex будет иметь ту же самую сигнатуру, которую мы только что применяли: Complex(float, float).

 

66

 

Со статическими методами генерации все проще - достаточно добавить второй статический метод генерации с таким назва­нием, которое четко обозначит его функцию:

public static Complex valueOfPolar(float г, float theta) {

return new Complex((float) (r * Math.cos(theta)), (float) (r * Math,sin(theta»);

}

 

 

Когда писались классы BigInteger и BigDecimal, не было согласия в том, что не­изменяемые классы должны быть фактически окончательными. Поэтому любой метод этих классов можно переопределить. К сожалению, исправить что-либо впоследствии уже было нельзя, не потеряв при этом совместимость версий снизу вверх. Поэтому, если вы пишите класс, безопасность которого зависит от неизменяемости аргумента с типом BigInteger или BigDecimal, полученного от ненадежного клиента, вы должны выполнить проверку и убедиться в том, что этот аргумент действительно является "настоящим" классом BigInteger или BigDecimal, а не экземпляром какого-либо ненадежного подкласса. Если имеет место последнее, необходимо создать резервную копию этого экземпляра, поскольку придется исходить из того, что он может оказать­ся изменяемым (статья 24):

public void foo(BigInteger b) {

if (b.getClass() != BigInteger.class)

b = new BigInteger(b.toByteArray());

}

 

Список правил для неизменяемых классов, представленный в начале статьи, гла­сит, что ни один метод не может модифицировать объект и все поля должны иметь модификатор final. Эти правила несколько строже, чем необходимо, и их можно ослабить с целью повышения производительности программы. Действительно, ни один метод не может произвести такое изменение состояния объекта, которое можно было бы увидеть извне. Вместе с тем, многие неизменяемые классы имеют одно или не­сколько избыточных полей без модификатора final, в которых они сохраняют однаж­ды полученные результаты трудоемких вычислений. Если в дальнейшем потребуется произвести те же самые вычисления, будет возвращено ранее сохраненное значение, ненужные вычисления выполняться не будут. Такая уловка работает надежно именно благодаря неизменяемости объекта: неизменность его состояния является гарантией того, что если вычисления выполнять заново, то они приведут опять к тому же результату.

Например, метод hashCode из класса PhoneNumbeT (статья 8) вычисляет хэш-код.

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

 

 

67

 

 

 Приведем общую идиому для кэширующей функции с отложенной инициализацией для неизменяемого объекта:

 

// Кэширующая функция с отложенной инициализацией

// для неизменяемого объекта

.private volatile Foo cachedFooVal = UNLIKELY_FOO_VALUE;

publlic Foo foo() {

Foo result = cachedFooVal;

if (result == UNLIKELY_FOO_VALUE)

result = cachedFooVal = fooValue();

return result;

}

// Закрытая вспомогательная функция, вычисляющая

// значение нашего объекта foo

private Foo fooVal() { ... }

 

Следует добавить одно предостережение, касающееся сериализуемости объектов. Если вы решили, что ваш неизменяемый класс должен реализовывать интерфейс Sеrializable, но при этом у него есть одно или несколько полей, которые ссылаются на изменяемые объекты, то вы обязаны предоставить явный метод readObject или readResolve, даже если для этого класса можно использовать сериализуемую форму, предоставляемую по умолчанию. Метод readObject, применяемый по умолчанию, позволил бы пользователю создать изменяемый экземпляр вашего во всех остальных ситуациях неизменяемого класса. Эта тема детально раскрывается в статье 56.

Подведем итоги. Не стоит для каждого метода get писать метод set. Классы должны оставаться неизменяемыми, если нет веской причины делать их изменяе­мыми. Неизменяемые классы имеют массу преимуществ, единственный же их недо­статок - возможные проблемы с производительностью при определенных условиях. Небольшие объекты значений, такие как PhoneNumber и Complex, всегда следует делать неизменяемыми. (В библиотеках для платформы Java есть несколько классов например java.util.Date и java.awt.Point, которые должны быть неизменяемыми, но та­ковыми не являются.) Вместе с тем вам следует серьезно подумать, прежде чем делать неизменяемыми более крупные объекты значений, такие как 5tring и Biglnteger. Создавать для вашего неизменяемого класса открытый изменяемый класс-компаньон следует, только если вы уверены в том, что это необходимо для получения приемле­мой производительности (статья 37) ..

Есть классы, которым неизменяемость не нужна, например классы-процессы Thread и TimerTask. Если класс нельзя сделать неизменяемым, вы должны огра­ничить его изменяемость, насколько это возможно. Чем меньше число состояний, в которых может находиться объект, тем проще рассматривать этот объект, тем меньше вероятность ошибки. По этой причине конструктор такого класса должен создавать полностью инициализированный объект, у которого все инварианты уже установлены. Конструктор не должен передавать другим методам класса объект, сформированный частично.

 

68

 

 Не создавайте открытый метод инициализации отдельно от конструктора, если только для этого нет чрезвычайно веской причины. Точно так же не следует создавать метод "повторной инициализации", который позволил бы использовать объект повторно, как если бы он был создан с другим исходным состоя­нием. Метод повторной инициализации обычно дает (если вообще дает) лишь неболь­шой выигрыш в производительности за счет увеличения сложности приложения.

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

Последнее замечание, которое нужно сделать в этой статье, касается класса Complex. Этот пример предназначался лишь для того, чтобы продемонстрировать свойство неизменяемости. Он не обладает достоинствами промышленной реализации класса комплексных чисел. для умножения и деления комплексных чисел он использует обычные формулы, для которых нет правильного округления и которые имеют скудную семантику для комплексных значений NaN и бесконечности [Kahan91, Smith62, Thomas94].

 

Глава 5

Аменяйте структуру классом

 

Конструкция struct языка С не была принята в языке программирования Java потому, что класс выполняет все то же самое, что может делать структура, и даже более того. Структура группирует несколько полей данных в один общий объект, тогда как класс связывает с полученным объектом операции, а также позволяет скрывать поля данных от пользователей объекта. Иными словами, класс может инкапсулировать

 

92

 

encapsulate) свои данные в объекте, доступ к которому осуществляется только через его методы. Тем самым у разработчика появляется возможность менять внутреннее представление объекта (статья 12).

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

 

// Вырожденные классы, подобные этому,

// не должны быть открытыми!

class Point {

public float х;

public float у;

}

 

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

 

// Класс с инкапсулированной структурой

class Point {

private float х;

private float у;

public Point(float х, float у) {

  this. х = х;

this.y = у;

}

public float getX() { return х; }

public float getY() { return у; }

public void setX(float х) { this.x = х; }

public void setY(float у) { this.y = у; }

}

 

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

 

93

 

 

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

Несколько классов в библиотеках для платформы Java нарушают совет, касающий­ся запрещения непосредственного доступа к полям открытого класса. В частности, это классы Point и Dimension из пакета java.awt. Не следует подражать этим классам, лучше рассматривать их как предупреждение. В статье 37 показано, как раскрытие внутреннего содержания класса Dimension привело к серьезным проблемам с произ­водительностью, которые нельзя было разрешить, не затрагивая клиентов.

 

 

Методы

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

 

Глава 7

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

 

 

Данная глава посвящена обсуждению основных элементов языка Java. В ней рассматриваются интерпретация локальных переменных, использование библиотек и различных типов данных, а также две выходящие за рамки языка возможности: отражение (reflection) и машинно-зависимые методы (native method). Наконец, обсуждаются оптимизация и соглашения по именованию.

 

Сводите к минимуму область видимости локальных переменных

 

Эта статья по своей сути схожа со статьей 12 "Сводите к минимуму доступность классов и членов". Сужая область видимости локальных переменных, вы повышаете удобство чтения и сопровождения вашего кода, сокращаете вероятность возникновения ошибок.

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

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

 

132

 

 

к расширению области видимости локальной переменной приводит не только слишком ранее, но и слишком позднее ее декларирование. Область видимости локаль­ной переменной начинается в том месте, где она декларируется, и заканчивается с завершением блока, содержавшего эту декларацию. Если переменная декларирована за пределами блока, где она используется, то она остается видимой и после того, как программа выйдет из этого блока. Если переменная случайно была использована до или после области, в которой она должна была применяться, последствия могут быть катастрофическими.

Почти каждая декларация локальной переменной должна содержать инициа­лизатор. Если у вас недостаточно информации для правильной инициализации пере­менной, вы должны отложить декларацию до той поры, пока она не появится. Исключение из этого правила связано с использованием операторов tгу / catch. Если для инициализации переменной применяется метод, инициирующий появление обраба­тываемого исключения, то инициализация переменной должна осуществляться внутри блока try. Если переменная должна использоваться за пределами блока try, деклари­ровать ее следует перед блоком try, там, где она еще не может быть "правильно ини­циализирована" (статья 35).

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

Представим, например, предпочтительную идиому для организации цикла по не­коей коллекции:

 

for (Iterator i = с.iterator(); i.hasNext(); ) {

doSomething(i.next()); }

 

для пояснения, почему данный цикл for предпочтительнее более очевидного цикла while, рассмотрим следующий фрагмент кода, в котором содержатся два цикла while и одна ошибка:

 

Iterator i = c.iterator();

 while (i.hasNext()) {

doSomething(i.next()); }

Iterator i2 = c2.1terator();

      while (i.hasNext()) { // Ошибка l

doSomethingElse(i2.next()); }

 

133

 

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

Если бы аналогичная ошибка копирования была допущена при применении цикла for, полученный код не был бы даже скомпилирован. для той области, где располага­ется второй цикл, переменная первого цикла уже была бы за пределами видимости:

 

for (Iterator i = с. iterator(); i. hasNext(); ) {

doSomething(i.next()); }

// Ошибка компиляции - символ i не может быть идентифицирован

for (Iterator i2 = c2.iterator(); i.hasNext();, ) {

doSomething(i2.next()); }

 

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

Идиома цикла for имеет еще одно преимущество перед идиомой цикла while, хотя и не столь существенное. Идиома цикла for короче на одну строку, что помогает при редактировании уместить содержащий ее метод в окне фиксированного размера и повышает удобство чтения.

Приведем еще одну идиом)\ цикла для про смотра списка, которая минимизирует область видимости локальных переменных:

 

// Высокопроизводительная идиома для просмотра списков

// с произвольным доступом

for (int i = 0, n = list.size(); i < n; i++) {

doSomething(list.get(i)); }

 

Эта идиома полезна для реализаций интерфейсов List с произвольным доступом, та­ких как ArrayList и Vector, поскольку для таких списков она, скорее всего, работает быстрее, чем приведенная выше "предпочтительная идиома". По поводу этой идиомы важно заметить, что в ней используются две переменные цикла: i и n, и обе имеют

 

134

 

 

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

Похожие идиомы есть и для других задач с циклами, например:

 

for (int i = 0, n = expensiveComputation(); i < n; i++) {

doSomething(i) ; }

 

И в этой идиоме применяются две переменные цикла. Вторая из них - n - служит для исключения' ненужных вычислений при повторных проходах цикла. Как правило, этой идиомой вы должны пользоваться в тех случаях, когда условие цикла содержит вызов некоего метода, но этот метод при каждом проходе гарантированно возвращает один и тот же результат.

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

 

Примеры типографских соглашений

 

Тип идентификатора Примеры
Пакет Com.sun.medialib, com.sun.jdi.event
Класс или интерфейс Timer, TimerTask, KeyFactorySpi, HttpServlet
Метод или поле Remove, ensureCapacity, getCrc
Поле-константа VALUES, NEGATIVE_INFINITY
Локальная переменная I, xref, houseNumber
   

 

 

Для методов, выполняющих какое-либо действие, в качестве названия использу­ются глаголы или глагольные конструкции, например append и drawlmage. для мето­дов, возвращающих булево значение, обычно применяются названия, в которых сначала идет слово "is", а потом существительное, именная конструкция' или любое слово (фраза), играющее роль прилагательного, например isDigit, isPrоbаblеРrime, isEmpty, isEnabled, isRunning.

Для именования методов, не связанных с булевыми операциями, а также методов, возвращающих атрибут объекта, для которого они были вызваны, обычно использу­ется существительное, именная конструкция либо глагольная конструкция, начинаю­щаяся с глагола "get", например size, hashCode, getТime. Отдельные пользователи требуют, чтобы применялась лишь третья группа (начинающаяся с "get"), но для по­добных претензий нет никаких оснований. Первые две формы обычно делают текст программы более удобным для чтения, например:

 

if (car.speed() > 2* SPEED_LIMIT)

generateAudibleAlert("Watch out for cops!");

 

Форма, начинающаяся с "get”, -обязательна, если метод принадлежит к классу Веа n [JavaBeans]. Ее можно также рекомендовать, если в будущем вы собираетесь превратить свой класс в Веаn. Наконец, серьезные основания для использования данной формы имеются в том случае, если в классе уже есть метод, присваивающий этому же атрибуту новое значение. При этом указанные методы следует назвать getAttribu t e и setAttribu t e.

Несколько названий методов заслуживают особого упоминания. Методы, кото­рые преобразуют тип объекта и возвращают независимый объект другого типа, часто называются toType, например toString, toArray. Методы, которые возвращают представление (статья 4), имеющее иной тип, чем сам объект, обычно называются asType, например asList. Методы, возвращающие простой тип с тем же значением,

 

 

156

 

что и у объекта, в котором они были вызваны, называются typeValue, например intValue. для статических методов генерации широко используются названия valueOf и getInstance (статья 1).

Грамматические соглашения для названий полей формализованы в меньшей степени и не играют такой большой роли, как в случае с классами, интерфейсами и методами, поскольку хорошо спроектированный API, если и предоставляет какое­ либо поля, то немного. Поля типа boolean обычно именуются так же, как логические методы доступа, но префикс "is" у них опускается, например 1п1 tialized, composite. Поля других типов, как правило, именуются с помощью существительного или имен­ной конструкции, например height, digits, bodyStyle. Грамматические соглашения для локальных переменных аналогичны соглашениям для полей, только их соблюде­ние еще менее обязательно.

Подведем итоги. Изучите стандартные соглашения по именованию и доведите их использование до автоматизма. Типографские соглашения просты и практически однозначны; грамматические соглашения более сложные и свободные. Как сказано в "The J ava La n guage Speci f icatioп" [JLS, 6.8], не нужно рабски следовать этим согла­шениям, если длительная практика их применения диктует иное решение. Пользуйтесь здравым смыслом.

 

 

157

 

 

Глава 8

Исключения

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

Не игнорируйте исключений

 

Этот совет кажется очевидным, но он нарушается настолько часто, что заслужи­вает повторения. Когда разработчики API декларируют, что некий метод инициирует исключение, этим они пытаются что-то вам скliзать. Не игнорируйте это! Игнориро­вать исключения легко: необходимо всего лишь Окружить вызов метода оператором try с пустым блоком catch:

 

// Пустой блок catch игнорирует исключение - крайне

// подозрительный код!

try {

} catch (SomeException е) { }

 

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

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

 

175

 

 

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

 

176

 

Глава 9

Потоки

Потоки позволяют выполнять одновременно несколько операций в пределах одной программы. Многопоточное программирование сложнее однопоточного, так что совет из статьи 30 здесь особенно актуален: если существует библиотечный класс, который может оградить вас от многопоточного программирования низкого уровня, во что бы то ни стало воспользуйтесь им. Одним из примеров таких классов является java.util.Тiтeг. Второй при мер - пакет util.concurrent Дага Ли (Doug Lea) [Lea01], содержащий целый набор утилит высокого уровня для управления потоками. Но, несмотря на наличие библиотек, вам все равно время от времени приходится писать или поддерживать программный код для многопоточной обработки. В этой главе содержатся советы, которые помогут вам создавать понятные, правильные и хорошо документированные программы для работы с потоками.

 

Глава 10

Сериализация

В этой главе описывается API сериализации объекта (object serialization), который формирует среду для представления объекта в виде потока байтов и, наоборот, для восстановления объекта из соответствующего потока байтов. Процедура представле­ния объекта в виде потока байтов называется сериализациеu объекта (serializing), обратный процесс называется его десериализациеu (deserializing). Как только объект сериализован, его представление можно передавать с одной работающей виртуальной машины Java на другую или сохранять на диске для последующей десериализации. Сериализация обеспечивает стандартное представление объектов на базовом уровне, которое используется для взаимодействия с удаленными машинами, а также как стан­дартный формат для сохранения данных при работе с компонентами JavaBeans ТМ.

 

Programming Language Guide

Joshua Bloch

ADDISON- WESLEY

 

 

Java TM

         Эффективное программирование

     Джошуа Блох

 

 

                                   Издательство «Лори»

 

Благодарности

я благодарю Патрика Чана (Patrick Chan) за то, что он посоветовал мне написать эту книгу и подбросил идею Лайзе Френдли (Lisa Friendly), главному редактору серии, а также Тима Линдхолма (Tim Lindholm), технического редактора серии, и Майка Хендриксона (Mike Hendrickson), исполнительного редактора издательства Addison- Wesley Professional. Спасибо Лайзе, Тиму и Майку за их поддержку при реализации проекта, за сверхчеловеческое терпение и несгибаемую веру в то, что когда-нибудь я напишу эту книгу.

Я благодарю Джеймса Гослинга (James Gosling) и его незаурядную команду" за то, что они предоставили мне нечто значительное, о чем можно написать, а также многих разработчиков платформы Java, последователей ДжеЙмса. В особенности я благодарен моим коллегам по работе в компании Sun из Java Platform Тools and Libraries Group за понимание, одобрение и поддержку. В эту группу входят Эндрю Беннетт (Andrew Benriett), Джо Дарси Оое Darcy), Нил Гафтер (Neal Gafter), Айрис Гарсиа (Iris Garcia), Константин Кладко (Konstantin Кladko), Йена Литтл (Ian Little), Майк Маклоски (Mike McCloskey) и Марк Рейнхольд (Mark Reinhold). Среди бывших членов группы: Дзенгуа Ли (Zhenghua Li), Билл Мэддокс (Bill Maddox) и Нейвин Санджива (Naveen Sanjeeva).

Выражаю благодарность моему руководителю Эндрю Беннетту (Andrew Bennett) и директору Ларри Абрахамсу (Larry Abrahams) за полную и страстную поддержку этого проекта. Спасибо Ричу Грину (Rich Green), вице-президенту компании Java Software, за создание условий, при которых разработчики имеют возможность творить и публиковать свои труды.

Мне чрезвычайно повезло с самой лучшей, какую только можно вообразить, группой рецензентов, и я выражаю мои самые искренние благодарности каждому из них: Эндрю Беннетту (Andrew Bennett), Синди Блох (Cindy Вloch), Дэну Блох (Dan Вloch), Бет Ботос (Beth Bottos), Джо Баубиеру Оое Bowbeer), Джиладу Браче (Gilad Bracha), Мэри Кампьон (Mary Campione), Джо Дарси Оое Darcy), Дэвиду Экхардту (David Eckhardt), Джо Фьалли Оое Fialli), Лайзе Френдли (Lisa Friendly), Джеймсу Гослингу (James Gosling), Питеру Хаггеру (Peter Haggar), Брайену КеРl:lигану (Brian Kernighan), Константину Кладко (Konstantin Кladko), Дагу Ли (Doug Lea), Дзенгуа Ли (Zhenghua Li), Тиму Линдхолму (Tim Lindholm), Майку Маклоски (Mike McCloskey), Тиму Пейерлсу (Tim Peierls), Марку Рейнхолду (Mark Reinhold), Кену Расселу (Ken Russell), Биллу Шэннону (ВШ· Shannon), Питеру Стауту (Peter Stout), Филу Уодлеру (Phil Wadler), Давиду Холмсу (David Holmes) и двум анонимным рецензентам. Они внесли множество предложений, кото­рые позволили существенно улучшить книгу и избавили меня от многих затруднений. Все оставшиеся недочеты полностью лежат на моей совести.

Многие мои коллеги, работающие в компании Sun и вне ее, участвовали в техни­ческих дискуссиях, которые улучшили качество этой книги. Среди прочих: Бен Гомес (Ben Gomes), Стефен Грерап (Steffen Grarup), Питер Кесслер (Peter Kessler), Ричард Рода (Richard Roda), Джон Роуз (John Rose) и Дэвид Стаутэмайер (David Stoutamire). Особая благодарность Дагу Ли (Doug Lea), озвучившему многие идеи этой книги. Даг неизменно щедро делился своим временем и знаниями.

Я благодарен Джули Дайникола (Julie Dinicola), Джекки Дусетт (Jacqui Doucette), Майку Хендриксону (Mike Hendrickson), Хизер Ольщик (Heather Olszyk), Трейси Расс (Tracy Russ) и всем сотрудникам Addison-Wesley за их поддержку и Профессионализм. Даже будучи занятыми до предела, они всегда были дружелюбны и учтивы.

Я благодарю Гая Стила (Сиу Steele), написавшего предисловие. Его участие в этом проекте - большая честь для меня.

Наконец, спасибо моей жене Синди Блох (Cindy Вloch), которая своим ободре­нием, а подчас и угрозами помогла мне написать эту книгу. Благодарю за чтение каж­дой статьи в необработанном виде, за помощь при работе с программой Framemaker, за написание предметного указателя и за то, что терпела меня, пока я корпел над этой книгой.

 

 

XlV

 

Содержание

 

Предисловие

Предисловие автора

 

Введение


Поделиться:



Последнее изменение этой страницы: 2019-04-11; Просмотров: 227; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.597 с.)
Главная | Случайная страница | Обратная связь