Теория и практика Java : Это ваш окончательный ответ?

Руководство по эффективному использованию ключевого слова final

Зачастую ключевое слово final используется некорректно - слишком часто при декларировании классов и методов, и слишком редко при декларировании полей экземпляров (instance fields). В этом месяце практикующий специалист в области Java-технологий Брайан Гетц проводит исследование различных руководств по эффективному применению final.

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



29.01.2007

Так же как и подобное ключевое слово const в C, final может иметь несколько различных значений в зависимости от контекста. Ключевое слово final может применяться к классам, методам или полям. В применении к классу оно означает, что данный класс не может подразделяться на подклассы. В применении к методу оно означает, что данный метод не может быть заменен каким-либо подклассом. В применении к полю, оно означает, что в каждом конструкторе значение поля должно задаваться только один раз и после этого никогда не может изменяться.

Большинство Java-текстов верно описывают применение и результаты применения ключевого слова final , но мало что говорят о том, когда и как часто использовать final. По моему опыту, final слишком часто применяется к классам и методам (обычно потому, что разработчики ошибочно полагают, что это улучшит производительность), и недостаточно часто используется там, где наиболее востребовано - в объявлении переменных экземпляра класса.

Почему этот класс final?

Очень часто, особенно в проектах с открытым исходным кодом, разработчик объявляет класс как final, но не приводит никаких причин для принятия такого решения. Через некоторое время, особенно если изначальный разработчик больше не привлекается для обслуживания кода, другие разработчики неизбежно зададутся вопросом "Почему класс X был описан как final?" Чаще всего ответа никто не знает. А если кто-то знает или пытается предположить, то почти всегда дается ответ "для ускорения процесса". Довольно распространено представление, что описание классов или методов как final помогает компилятору подключать вызовы методов. Данное представление ошибочно (или, в весьма крайнем случае, сильно преувеличено).

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

Предварительная оптимизация

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

Подобно множеству мифов о производительности Java, общепринято и редко проверяется ошибочное представление о том, что объявление классов и методов как final приводит к лучшей производительности. Ведется спор о том, означает ли объявление метода или класса как final то, что компилятор сможет подключать вызовы методов более активно, потому что ему известно, что во время выполнения это версия именно того метода, который будет вызван. Но это просто ерунда. То, что класс X скомпилирован против final-класса Y, не означает, что во время выполнения загружена одна и та же версия класса Y. Итак, получается, что компилятор не может безопасно подключать вызовы методов разных классов, независимо от того, являются ли они final или нет. Компилятор сможет свободно подключать метод, только если он является частным (private), а в этом случае, ключевое слово final будет излишним.

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


Deja vu - Возвращение к ключевому слову register

Применение final для оптимизационных решений очень похоже на отмененное ключевое слово register в C. Создание ключевого слова register было обусловлено стремлением дать программисту возможность помочь оптимизатору, но, в действительности, оно оказалось не очень пригодным. Вопреки нашему убеждению, компиляторы обычно показывают лучшие результаты в кодовых оптимизационных решениях, чем человек, особенно на современных RISC-процессорах. Фактически, большинство C-компиляторов полностью игнорируют ключевое слово register. Ранние C-компиляторы игнорировали его, потому что они не выполняли никакой оптимизации, а современные компиляторы игнорируют его, потому что могут принимать лучшие решения по оптимизации и без него. В любом случае, использование ключевого слова register не приводит к большому приросту производительности, точно так же, как и ключевое слово final, применяемое к классам или методам Java. Если же вы хотите оптимизировать свой код, то используйте оптимизации, которые приведут к существенным результатам, например вместо выполнения лишних подсчетов применяйте эффективные алгоритмы - и предоставьте оптимизацию подсчета циклов компилятору и виртуальной машине Java.


Использование final для обеспечения неизменности

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

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

Если вы должны применить классы или методы final, задокументируйте причину

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


Поля final

Поля final так отличаются от классов и методов final, что едва ли правомерно именовать их одним и тем же ключевым словом. Поле final является полем, предназначенным только для чтения, и гарантируется, что его параметры задаются только один раз во время построения (или во время инициализации класса для static (статических) final полей.) Как было описано ранее, при работе с классами и методами final Вам всегда следует задавать себе вопрос, действительно ли необходимо применить final. Что касается полей final, то Вам следует задавать прямо противоположным вопрос - действительно ли необходимо , чтобы поле было изменяемым? Ответ, на удивление, довольно часто оказывается отрицательным.

Значение документирования

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

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

Почему же поля final так редко используют? Одной из причин является то, что, они могут быть слишком громоздкими, чтобы их можно было использовать правильно, особенно для ссылок на объекты, чьи конструкторы могут создавать исключительные ситуации. Из-за того, что поле final должно быть инициализировано в каждом конструкторе только один раз, если конструкция ссылки на объект final может создать исключительные ситуации, то компилятор может указывать на то, что возможно поле будет неинициализировано. Компилятор обычно достаточно умен, чтобы понять, что при наличии инициализации на каждой из двух различных ветвей кода, например, как в блоке if...else , фактически выполняется только одна инициализация, но часто менее снисходителен к блокам try...catch . Например, большинство компиляторов Java не примут код в листинге 1:

 public class Foo { private
                                                final Thingie thingie; public Foo() { try { thingie
                                                = new Thingie(); } catch
                                                (ThingieConstructionException e) { thingie =
                                                Thingie.getDefaultThingie(); } } }

Но они примут код в листинге 2:

 public class Foo { private
                                                final Thingie thingie; public Foo() { Thingie
                                                tempThingie; try { tempThingie = new Thingie(); }
                                                catch (ThingieConstructionException e) { tempThingie
                                                = Thingie.getDefaultThingie(); } thingie =
                                                tempThingie; } }

Ограничения полей final

Однако у полей final есть некоторые серьезные ограничения. Хотя ссылка на массив и может быть объявлена как final, но для элементов массива это недопустимо. Это значит, что классы, которые определяют public final-поля массива или возвратные ссылки на эти поля через их методы, например класс DangerousStates в листинге 3, являются неизменяемыми. Аналогично, хотя объектная ссылка и может быть объявлена как поле final, но объект, на который она указывает, однако, может быть изменяемым. Если Вы хотите создать неизменяемые объекты с помощью полей final, то Вы не должны допустить, чтобы ссылки на массивы или изменяемые объекты покинули Ваш класс. Это можно легко сделать, не прибегая к многократному клонированию массива, превратив массивы в Lists (списки), такие как в классе SafeStates, показанном в листинге 3.

 // Не неизменяемый - массив
                                                состояний может быть изменен злоумышленным
                                                вызывающим оператором public-класс DangerousStates {
                                                private final String[] states = new String[] {
                                                "Alabama",
                                                "Alaska", ... }; public String[]
                                                getStates() { return states; } } // Неизменяемый -
                                                возвращает неизменяемый List вместо public-класс
                                                SafeStates { private final String[] states = new
                                                String[] { "Alabama",
                                                "Alaska", ... }; private final
                                                List statesAsList = new AbstractList() { public
                                                Object get(int n) { return states[n]; } public int
                                                size() { return states.length; } }; public List
                                                getStates() { return statesAsList; } }

Почему final не был расширен для применения к массивам и ссылочным объектам, подобно использованию const в C и C++? Семантика и применение const в C++ довольно запутанны, и имеет различные значения в зависимости от того, в какой части выражения используется. Разработчики архитектуры Java попытались спасти нас от такой путаницы, но, к сожалению, они снабдили весь процесс новыми трудностями.


Несколько слов в заключение

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

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=192683
ArticleTitle=Теория и практика Java : Это ваш окончательный ответ?
publish-date=01292007