Выбрать главу

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

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

Резюме

Как наследование, так и композиция позволяют создавать новые типы на осно­ве уже существующих. Композиция обычно применяется для повторного ис­пользования реализации в новом типе, а наследование — для повторного исполь­зования интерфейса. Так как производный класс имеет интерфейс базового класса, к нему можно применить восходящее преобразование к базовому клас­су; это очень важно для работы полиморфизма (см. следующую главу).

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