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. Конструирование базового класса выполняется по тем же правилам и в том же порядке, что и для производного класса. После за­вершения работы конструктора базового класса инициализируются перемен­ные, в порядке их определения. Наконец, выполняется оставшееся тело конст­руктора.
Резюме