33

33


//: c06:BlankFinal .java // "Пустые" неизменные поля.
class Poppet {
private int i:
Poppet(int ii) { i = ii; }
}
public class BlankFinal {
private final int i = 0; // Инициализированная константа private final int j; // Пустая константа private final Poppet p; // Пустая константа-ссылка II Пустые константы НЕОБХОДИМО инициализировать // в конструкторе: public BlankFinalО {
j = 1; // Инициализация пустой константы р = new Poppet(l); // Инициализация пустой неизменной ссылки
}
public BlankFinal(int х) {
j = х; // Инициализация пустой константы р = new Poppet(x), // Инициализация пустой неизменной ссылки
}
public static void main(String[] args) { new BlankFinal О; new BlankFinal(47),
}
} ///:-
Значения неизменных (final) переменных обязательно должны присваивать­ся или в выражении, записываемом в точке определения переменной, или в ка­ждом из конструкторов класса. Тем самым гарантируется инициализация по­лей, объявленных как final, перед их использованием.
Неизменные аргументы
Java позволяет вам объявлять неизменными аргументы метода, объявляя их с ключевым словом final в списке аргументов. Это значит, что метод не может изменить значение, на которое указывает передаваемая ссылка:
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 {