В традиционных языках программы загружаются целиком в процессе запуска. Далее следует инициализация, а затем программа начинает работу. Процесс инициализации в таких языках должен тщательно контролироваться, чтобы порядок инициализации статических объектов не создавал проблем. Например, в С++ могут возникнуть проблемы, когда один из статических объектов полагает, что другим статическим объектом уже можно пользоваться, хотя последний еще не был инициализирован.
В языке 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. Конструирование базового класса выполняется по тем же правилам и в том же порядке, что и для производного класса. После завершения работы конструктора базового класса инициализируются переменные, в порядке их определения. Наконец, выполняется оставшееся тело конструктора.
Резюме
Как наследование, так и композиция позволяют создавать новые типы на основе уже существующих. Композиция обычно применяется для повторного использования реализации в новом типе, а наследование — для повторного использования интерфейса. Так как производный класс имеет интерфейс базового класса, к нему можно применить восходящее преобразование к базовому классу; это очень важно для работы полиморфизма (см. следующую главу).
Несмотря на особое внимание, уделяемое наследованию в ООП, при начальном проектировании обычно предпочтение отдается композиции, а к наследованию следует обращаться только там, где это абсолютно необходимо. Композиция обеспечивает несколько большую гибкость. Вдобавок, применяя хитрости наследования к встроенным типам, можно изменять точный тип и, соответственно, поведение этих встроенных объектов во время исполнения. Таким образом, появляется возможность изменения поведения составного объекта во время исполнения программы.