Брюс Эккель - Философия Java3
Явная инициализация
Что делать, если вам понадобится придать переменной начальное значение? Проще всего сделать это прямым присваиванием этой переменной значения в точке ее объявления в классе. (Заметьте, что в С++ такое действие запрещено, хотя его постоянно пытаются выполнить новички.) В следующем примере полям уже знакомого класса InitialValues присвоены начальные значения:
//• initialization/InitialValues2.java
// Явное определение начальных значений переменных
public class Ini ti alValues2 {
boolean bool = true; char ch = 'x'; byte b = 47; short s = Oxff; int i = 999; long Ing = 1, float f = 3.14f; double d = 3.14159; } ///:-
Аналогичным образом можно инициализировать и не-примитивные типы. Если Depth является классом, вы можете добавить переменную и инициализировать ее следующим образом:
//: initialization/Measurement.java class Depth {}
public class Measurement { Depth d = new DepthO; // ... } ///:-
Если вы попытаетесь использовать ссылку d, которой не задано начальное значение, произойдет ошибка времени исполнения, называемая исключением (исключения подробно описываются в главе 10).
Начальное значение даже может задаваться вызовом метода:
II: initialization/Methodlnit.java public class Methodlnit { int i = f(); int f() { return 11; } } ///:-
Конечно, метод может получать аргументы, но в качестве последних не должны использоваться неинициализированные члены класса. Например, так правильно:
II: initialization/Methodlnit2.java public class MethodInit2 { int i = f(), int j = g(i); int f() { return 11; } int g(int n) { return n * 10; } } ///-
а так нет:
II: initialization/MethodInit3 java public class MethodInit3 { //! int j = g(i); 11 Недопустимая опережающая ссылка int i = f(); int f() { return 11; } int g(int n) { return n * 10, } } ///
Это одно из мест, где компилятор на полном основании выражает недовольство преждевременной ссылкой, поскольку ошибка связана с порядком инициализации, а не с компиляцией программы.
Описанный подход инициализации очень прост и прямолинеен. У него есть ограничение — все объекты типа InitialValues получат одни и те же начальные значения. Иногда вам нужно именно это, но в других ситуациях необходима большая гибкость.
Инициализация конструктором
Для проведения инициализации можно использовать конструктор. Это придает большую гибкость процессу программирования, так как появляется возможность вызова методов и выполнения действия по инициализации прямо во время работы программы. Впрочем, при этом необходимо учитывать еще одно обстоятельство: оно не исключает автоматической инициализации, происходящей перед выполнением конструктора. Например, в следующем фрагменте
//: initialization/Counter.java public class Counter { int i;
Counter О {i=7, } // .. } ///-
переменной i сначала будет присвоено значение 0, а затем уже 7. Это верно для всех примитивных типов и ссылок на объекты, включая те, которым задаются явные значения в точке определения. По этим причинам компилятор не пытается заставить вас инициализировать элементы в конструкторе, или в ином определенном месте, или перед их использованием — инициализация и так гарантирована.
Порядок инициализации
Внутри класса очередность инициализации определяется порядком следования переменных, объявленных в этом классе. Определения переменных могут быть разбросаны по разным определениям методов, но в любом случае переменные инициализируются перед вызовом любого метода — даже конструктора. Например:
II- initialization/OrderOflnitialization java // Демонстрирует порядок инициализации import static net mindview util.Print.*,
// При вызове конструктора для создания объекта // Window выводится сообщение class Window {
Window(int marker) { print("Window(" + marker + ")"); }
}
class House { Window wl = new Window(l); // Перед конструктором HouseO {
// Показывает, что выполняется конструктор print(" HouseO"):
w3 = new Window(33), 11 Повторная инициализация w3
}
Window w2 = new Window(2). // После конструктора
void f() { printC'fO"). }
Window w3 = new Window(3). // В конце
}
public class OrderOflnitialization { public static void main(String[] args) { House h = new HouseO;
h fO. // Показывает, что объект сконструирован
}
} /* Output Window(l) Window(2) Window(3) HouseO Window(33) fO */// -
В классе House определения объектов Window намеренно разбросаны, чтобы доказать, что все они инициализируются перед выполнением конструктора или каким-то другим действием. Вдобавок ссылка w3 заново проходит инициализацию в конструкторе.
Из результатов программы видно, что ссылка w3 минует двойную инициализацию, перед вызовом конструктора и во время него. (Первый объект теряется, и со временем его уничтожит сборщик мусора.) Поначалу это может показаться неэффективным, но такой подход гарантирует верную инициализацию — что произошло бы, если бы в классе был определен перегруженный конструктор, который не инициализировал бы ссылку w3, а она при этом не получала бы значения по умолчанию?
Инициализация статических данных
Данные статических полей всегда существуют в единственном экземпляре, независимо от количества созданных объектов. Ключевое слово static не может применяться к локальным-переменным, только к полям. Если статическое поле относится к примитивному типу, при отсутствии явной инициализации ему присваивается значение по умолчанию. Если это ссылка на объект, то ей присваивается значение null.
Если вы хотите провести инициализацию в месте определения, она выглядит точно так же, как и у нестатических членов класса.
Следующий пример помогает понять, когда инициализируется статическая память:
//• initialization/StaticInitialization.java // Указание значений по умолчанию в определении класса, import static net mindview util Print *;
class Bowl {
Bowl(int marker) {
print("Bowl(" + marker + ")"),
}
void fl(int marker) {
print("fl(" + marker + ")");
}
}
class Table {
static Bowl bowll = new Bowl(l); TableO {
print("Table()"); bowl 2.f1(1),
}
void f2(iлt marker) {
print("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3), static Bowl bowl4 = new Bowl(4). CupboardO {
printC'CupboardO"). bowl4 fl(2).
}
void f3(int marker) {
print(Mf3(" + marker + ")"),
}
static Bowl bowl5 = new Bowl(5);
}
public class Staticlnitialization {
public static void main(String[] args) {
print ("Создание нового объекта Cupboard в mainO"), new CupboardO;
print ("Создание нового объекта Cupboard в mainO"), new CupboardO; table.f2(l). cupboard f3(l),
}
static Table table = new TableO, static Cupboard cupboard = new CupboardO; } /* Output; Bowl(1) Bowl(2) TableO fl(l) Bowl(4) Bowl(5) Bowl(3) Cupboard() fl(2)
Создание нового объекта Cupboard в mainO Bowl(3) CupboardO fl(2)
Создание нового объекта Cupboard в mainO Bowl(3)
CupboardО fl(2) f2(l) f3(l) *///:-
Класс Bowl позволяет проследить за процессом создания классов; классы Table и Cupboard содержат определения статических объектов Bowl. Заметьте, что в классе Cupboard создается нестатическая переменная Bowl bowl3, хотя все остальные определения — статические.
Из выходных данных программы видно, что статическая инициализация происходит только в случае необходимости. Если вы не создаете объектов Table и никогда не обращаетесь к Table.bowll или Table.bowl2, то, соответственно, не будет и объектов static Bowl bowll и static Bowl bowl2. Они инициализируются только при создании первого объекта Table (или при первом обращении к статическим данным). После этого статические объекты повторно не переопределяются.
Сначала инициализируются static-члены, если они еще не были проинициали-зированы, и только затем нестатические объекты. Доказательство справедливости этого утверждения легко найти в результате работы программы. Для выполнения main() (а это статический метод!) загружается класс Staticlnitialization; затем инициализируются статические *поля table и cupboard, вследствие чего загружаются эти классы. И так как все они содержат статические объекты Bowl, загружается класс Bowl. Таким образом, все классы программы загружаются до начала main(). Впрочем, эта ситуация нетипична, поскольку в рядовой программе не все поля объявляются как статические, как в данном примере.
Неплохо теперь обобщить знания о процессе создания объекта. Для примера возьмем класс с именем Dog:
• Хотя ключевое слово static и не используется явно, конструктор в действительности является статическим методом. При создании первого объекта типа Dog или при первом вызове статического метода-обращения к статическому полю класса Dog, интерпретатор Java должен найти класс Dog.class. Поиск осуществляется в стандартных каталогах, перечисленных в переменной окружения С LASS PATH.
• После загрузки файла Dog.class (с созданием особого объекта Class, о котором мы узнаем позже) производится инициализация статических элементов. Таким образом, инициализация статических членов проводится только один раз, при первой загрузке объекта Class.
• При создании нового объекта конструкцией new Dog() для начала выделяется блок памяти, достаточный для хранения объекта Dog в куче.
• Выделенная память заполняется нулями, при этом все примитивные поля объекта Dog автоматически инициализируются значениями по умолчанию (ноль для чисел, его эквиваленты для типов boolean и char, null для ссылок).
• Выполняются все действия по инициализации, происходящие в точке определения полей класса.
• Выполняются конструкторы. Как вы узнаете из главы 7, на этом этапе выполняется довольно большая часть работы, особенно при использовании наследования.