29

29


II: reusing/FinalArguments.java II Использование final с аргументами метода



class Gizmo {



public void spinO {}



}



public class Final Arguments { void with(final Gizmo g) {IIIg = new GizmoO;IIзапрещено -- g объявлено final



}



void without(Gizmo g) {



g = new GizmoO: II Разрешено -- g не является final g.spinO;



}



II void f(final int i) { i++, } II Нельзя изменять. II неизменные примитивы доступны только для чтения: int g(final int i) { return i + 1; } public static void main(String[] args) {



Final Arguments bf = new FinalArgumentsO;



bf .without(null):продолжение &bf with(niil 1),



}



} ///.-



Методы f() и g() показывают, что происходит при передаче методу примити­вов с пометкой final: их значение можно прочитать, но изменить его не удастся.



Неизменные методы



Неизменные методы используются по двум причинам. Первая причина — «бло­кировка» метода, чтобы производные классы не могли изменить его содержа­ние. Это делается по соображениям проектирования, когда вам точно надо знать, что поведение метода не изменится при наследовании.



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



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



Спецификаторы final и private



Любой закрытый (private) метод в классе косвенно является неизменным (final) методом. Так как вы не в силах получить доступ к закрытому методу, то не смо­жете и переопределить его. Ключевое слово final можно добавить к закрытому методу, но его присутствие ни на что не повлияет.



Это может вызвать недоразумения, так как при попытке переопределения закрытого (private) метода, также неявно являющегося final, все вроде бы рабо­тает и компилятор не выдает сообщений об ошибках:



//• reusi ng/Fi nalOverri di ngll1usi on.java



// Все выглядет так, будто закрытый (и неизменный) метод



// можно переопределить, но это заблуждение.



import static net mindview.util Print.*,



class WithFinals {



// To же, что и просто private:



private final void f() { printC'WithFinals f()
M
), }



// Также автоматически является final



private void g() { printC'WithFinals.g()"), }



class OverridingPrivate extends WithFinals {



private final void f() {



printC'OverridingPrivate fO").



}



private void g() {



printC'OverridingPrivate g()").



}



}



class 0verridingPrivate2 extends OverridingPrivate {



public final void f() {



print("0verridingPrivate2 f()").



}



public void g() {



print("0verridingPrivate2 g()").



}



public class FinalOverridingll1usion {



public static void main(String[] args) {



0verridingPrivate2 op2 = new 0verridingPrivate2();



op2 f();



op2.g();



// Можно провести восходящее преобразование-



OverridingPrivate op = op2;



// Но методы при этом вызвать невозможно.



//! op f().



//! op.g().



// И то же самое здесь- WithFinals wf = ор2. //! wf.fO, //! wf g();



}



} /* Output: 0verridingPrivate2.f()



0verridingPrivate2.g() */// ~



«Переопределение» применимо только к компонентам интерфейса базового класса. Иначе говоря, вы должны иметь возможность выполнить восходящее преобразование объекта к его базовому типу и вызвать тот же самый метод (это утверждение подробнее обсуждается в следующей главе). Если метод объявлен как private, он не является частью интерфейса базового класса; это просто неко­торый код, скрытый внутри класса, у которого оказалось то же имя. Если вы создаете в производном классе одноименный метод со спецификатором public, protected или с доступом в пределах пакета, то он никак не связан с закрытым методом базового класса. Так как privat-метод недоступен и фактически неви­дим для окружающего мира, он не влияет ни на что, кроме внутренней органи­зации кода в классе, где он был описан.



Неизменные классы






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



// reusing/Jurassic java



// Объявление неизменным всего класса



class SmallBrain {}



final class Dinosaur { int i = 7, int j = 1,



SmallBrain x = new SmallBrain(), void f() {}



}



//
1
class Further extends Dinosaur {}



// Ошибка Нельзя расширить неизменный класс Dinosaur



public class Jurassic {



public static void main(String[] args) { Dinosaur n = new DinosaurO; n.f(). n.i = 40. n.j++.



}



} ///-



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



Предостережение



На первый взгляд идея объявления неизменных методов (final) во время разра­ботки класса выглядит довольно заманчиво — никто не сможет переопределить ваши методы. Иногда это действительно так.



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



Хорошим примером служит стандартная библиотека Java. Класс vector Java 1.0/1.1 часто использовался на практике и был бы еще полезнее, если бы по со­ображениям эффективности (в данном случае эфемерной) все его методы не были объявлены как final. Возможно, вам хотелось бы создать на основе vector производный класс и переопределить некоторые методы, но разработчи­ки почему-то посчитали это излишним. Ситуация выглядит еще более парадок­сальной по двум причинам. Во-первых, класс Stack унаследован от Vector, и это значит, что StackестьVector, а это неверно с точки зрения логики. Тем не менее мы видим пример ситуации, в которой сами проектировщики Java используют наследование от Vector. Во-вторых, многие полезные методы класса Vector, та­кие как addElement() и elementAt(), объявлены с ключевым словом synchronized. Как вы увидите в главе 12, синхронизация сопряжена со значительными из­держками во время выполнения, которые, вероятно, сводят к нулю все преиму­щества от объявления метода как final. Все это лишь подтверждает теорию о том", что программисты не умеют правильно находить области для примене­ния оптимизации. Очень плохо, что такой неуклюжий дизайн проник в стан­дартную библиотеку Java. (К счастью, современная библиотека контейнеров Java заменяет Vector классом ArrayList, который сделан гораздо более аккуратно и по общепринятым нормам. К сожалению, существует очень много готового кода, написанного с использованием старой библиотеки контейнеров.)



Инициализация и загрузка классов



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



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



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



Инициализация с наследованием



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



// reusing/Beetle java



// Полный процесс инициализации



import static net mindview util Print *.



class Insect {



private int 1 =9. protected int j. InsectO {



System out println("i = " + i + ". j = " + j), J = 39,



}



private static int xl =



printlnitC"Поле static Insect xl инициализировано"), static int printlnit(String s) { print(s). return 47.












public class Beetle extends Insect {



private int k = рппШЩ"Поле Beetle k инициализировано"), public BeetleO {



prtC'k = " + k), prtC'j = " + j).



}



private static int x2 =



printInit("Пoлe static Beetle x2 инициализировано"), public static void main(String[] args) { print("Конструктор Beetle"). Beetle b = new BeetleO;



}



} /*



Поле static Insect.xl инициализировано Поле static Beetle x2 инициализировано Конструктор Beetle i = 9. j = 0



Поле Beetle k инициализировано k = 47



j = 39 */// ~



Запуск класса Beetle в Java начинается с выполнения метода Beetle.main() (статического), поэтому загрузчик пытается найти скомпилированный код класса Beetle (он должен находиться в файле Beetle.class). При этом загрузчик обнаруживает, что у класса имеется базовый класс (о чем говорит ключевое слово extends), который затем и загружается. Это происходит независимо от того, собираетесь вы создавать объект базового класса или нет. (Чтобы убе­диться в этом, попробуйте закомментировать создание объекта.)



Если у базового класса имеется свой базовый класс, этот второй базовый класс будет загружен в свою очередь, и т. д. Затем проводится static-инициали­зация корневого базового класса (в данном случае это Insect), затем следующе­го за ним производного класса, и т. д. Это важно, так как производный класс и инициализация его static-объектов могут зависеть от инициализации членов базового класса.






В этой точке все необходимые классы уже загружены, и можно переходить к созданию объекта класса. Сначала всем примитивам данного объекта при­сваиваются значения по умолчанию, а ссылкам на объекты задается значение null — это делается за один проход посредством обнуления памяти. Затем вызы­вается конструктор базового класса. В нашем случае вызов происходит автома­тически, но вы можете явно указать в программе вызов конструктора базового класса (записав его в первой строке описания конструктора Beetle()) с помо­щью ключевого слова super. Конструирование базового класса выполняется по тем же правилам и в том же порядке, что и для производного класса. После за­вершения работы конструктора базового класса инициализируются перемен­ные, в порядке их определения. Наконец, выполняется оставшееся тело конст­руктора.



Резюме