- •Глава 1 введение в объекты 17
- •Глава 2 всё является объектом 44
- •Глава 3 операторы 65
- •Глава 4 управляющие конструкции 97
- •Глава 5 инициализация и завершение 112
- •Глава 10 внутренние классы 236
- •Глава 11 контейнеры и хранение объектов 269
- •Глава 6 управление доступом 146
- •Глава 7 повторное использование классов 161
- •Глава 8 полиморфизм 190
- •Глава 9 интерфейсы 212
- •Глава 12 обработка ошибок и исключений 302
- •Глава 13 информация о типах 342
- •Глава 14 параметризация 386
- •Глава 15 массивы 442
- •Глава 16 система ввода/вывода java 472
- •Глава 17 параллельное выполнение 546
- •Глава 1 введение в объекты
- •Глава 2 всё является объектом
- •Глава 3 операторы
- •Глава 4 управляющие конструкции
- •Глава 5 инициализация и завершение
- •1. Ваши объекты могут быть и не переданы сборщику мусора.
- •2. Сборка мусора не является удалением.
- •3. Процесс сборки мусора относится только к памяти.
- •Глава 6 управление доступом
- •Глава 7 повторное использование классов
- •Глава 8 полиморфизм
- •Глава 9 интерфейсы
- •InterfaceCollіsion.Java.23 f() в c не может реализовать f() в i1; попытка использовать несовместимые возвращаемые типы обнаружено: int требуется- void
- •InterfaceCollіsi on java:24- интерфейсы і1 и i1 несовместимы; оба определяют f(), но с различными возвращаемыми типами
- •Глава 10 внутренние классы
- •Глава 11 контейнеры и хранение объектов
- •Глава 12 обработка ошибок и исключений
- •Глава 13 информация о типах
- •Instanceof и сравнение Class
- •Глава 14 параметризация
- •Глава 15 массивы
- •Глава 16 система ввода/вывода java
- •Глава 17 параллельное выполнение
1. Ваши объекты могут быть и не переданы сборщику мусора.
2. Сборка мусора не является удалением.
Если программа завершает свою работу и сборщик мусора не удалил ни одного объекта и не освободил занимаемую память, то эта память будет возвращена операционной системе после завершения работы программы. Это хорошо, так как сборка мусора сопровождается весомыми издержками, и если сборщик не используется, то, соответственно, эти издержки не проявляются.
Для чего нужен метод finalize()?
Итак, если метод finalize() не стоит использовать для проведения стандартных операций завершения, то для чего же он нужен? Запомните третье правило:
ВНИМАНИЕ ------------------------------------------
3. Процесс сборки мусора относится только к памяти.
Единственная причина существования сборщика мусора — освобождение памяти, которая перестала использоваться вашей программой. Поэтому все действия, так или иначе связанные со сбором мусора, особенно те, что записаны в методе finalize(), должны относиться к управлению и освобождению памяти.
Но значит ли это, что если ваш объект содержит другие объекты, то в finalize() они должны явно удаляться? Нет — сборщик мусора займется освобождением памяти и удалением объектов вне зависимости от способа их создания. Получается, что использование методаfinalize() ограничено особыми случаями, в которых ваш объект размещается в памяти необычным способом, не связанным с прямым созданием экземпляра. Но, если в Java все является объектом, как же тогда такие особые случаи происходят?
Похоже, что поддержка метода finalize() была введена в язык, чтобы сделать возможными операции с памятью в стиле C, с привлечением нестандартных механизмов выделения памяти. Это может произойти в основном при использовании методов, предоставляющих способ вызова He-Java-кода из программы на Java. C и C++ пока являются единственными поддерживаемыми языками, но, так как для них таких ограничений нет, в действительности программа Java может вызвать любую процедуру или функцию на любом языке. Во внешнем коде можно выделить память вызовом функций C, относящихся к семейству malloc(). Если не воспользоваться затем функцией free(), произойдет «утечка» памяти. Конечно, функция free() тоже принадлежит к C и C++, поэтому придется в методе finalize() провести вызов еще одного «внешнего» метода.
После прочтения этого абзаца у вас, скорее всего, сложилось мнение, что метод finalize() используется нечасто. И правда, это не то место, где следует проводить рутинные операции очистки. Но где же тогда эти обычные операции будут уместны?
Очистка — ваш долг
Для очистки объекта его пользователю нужно вызвать соответствующий метод в той точке, где эти завершающие действия по откреплению и должны осуществляться. Звучит просто, но немного противоречит традиционным представлениям о деструкторах C++. В этом языке все объекты должны уничтожаться. Если объект C++ создается локально (то есть в стеке, что невозможно в Java), то удаление и вызов деструктора происходит у закрывающей фигурной скобки, ограничивающей область действия такого объекта. Если же объект создается оператором new (как в Java), то деструктор вызывается при выполнении программистом оператора C++ delete (не имеющего аналога в Java). А когда программист на C++ забывает вызвать оператор delete, деструктор не вызывается и происходит «утечка» памяти, к тому же остальные части объекта не проходят необходимой очистки. Такого рода ошибки очень сложно найти и устранить, и они являются веским доводом в пользу перехода с C++ на Java. Java не позволяет создавать локальные объекты — все объекты должны быть результатом действия оператора new. Но в Java отсутствует аналог оператора delete, вызываемого для разрушения объекта, так как сборщик мусора и без того выполнит освобождение памяти. Значит, в несколько упрощенном изложении можно утверждать, что деструктор в Javaотсутствует из-за присутствия сборщика мусора. Но в процессе чтения книги вы еще не раз убедитесь, что наличие сборщика мусора не устраняет необходимости в деструкторах или их аналогах. (И никогда не стоит вызывать метод finalize() непосредственно, так как этот подход не решает проблему.) Если же потребуется провести какие-то завершающие действия, отличные от освобождения памяти, все же придется явно вызвать подходящий метод, выполняющий функцию деструктора C++, но это уже не так удобно, как встроенный деструктор. Помните, что ни сборка мусора, ни финализация не гарантированы. Если виртуальная машина Java (Java Virtual Machine, JVM) далека от критической точки расходования ресурсов, она не станет тратить время на освобождение памяти с использованием сборки мусора.
Условие «готовности»
В общем, вы не должны полагаться на вызов метода finalize() — создавайте отдельные «функции очистки» и вызывайте их явно. Скорее всего, finalize() пригодится только в особых ситуациях нестандартного освобождения памяти, с которыми большинство программистов никогда не сталкивается. Тем не менее существует очень интересное применение метода finalize(), не зависящее от того, вызывается ли он каждый раз или нет. Это проверка условия готовности объекта.
В той точке, где объект становится ненужным — там, где он готов к проведению очистки, — этот объект должен находиться в состоянии, когда освобождение закрепленной за ним памяти безопасно. Например, если объект представляет открытый файл, то он должен быть соответствующим образом закрыт, перед тем как его «приберет» сборщик мусора. Если какая-то часть объекта не будет готова к уничтожению, результатом станет ошибка в программе, которую затем очень сложно обнаружить. Ценность finalize() в том и состоит, что он позволяет вам обнаружить такие ошибки, даже если и не всегда вызывается. Единожды проведенная финализация явным образом укажет на ошибку, а это все, что вам нужно. Простой пример использования данного подхода:
//: initialization/TerminationCondition.java
// Использование finalize() для выявления объекта,
// не осуществившего необходимой финализации
class Book {
boolean checkedOut = false;
Book(boolean checkOut) {
checkedOut = checkOut;
}
void checkIn() {
checkedOut = false;
}
protected void finalize() {
if(checkedOut)
System.out.println("Error: checked out");
// Обычно это делается так-:
// super.finalize(); // Вызов версии базового класса
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Правильная очистка-:
novel.checkIn();
// Теряем ссылку, забыли про очистку:
new Book(true);
// запрос JVM на сборку мусора и финализация :
System.gc();
}
}
<spoiler text="Output:">
Error: checked out
</spoiler> «Условие готовности» состоит в том, что все объекты Book должны быть «сняты с учета» перед предоставлением их в распоряжение сборщика мусора, но в методе main() программист ошибся и не отметил один из объектов Book. Если бы в методе finalize() не было проверки на условие «готовности», такую оплошность было бы очень сложно обнаружить.
Заметьте, что для проведения принудительной финализации был использован метод System.gc(). Но даже если бы его не было, с высокой степенью вероятности можно сказать, что «утерянный» объект Book рано или поздно будет обнаружен в процессе исполнения программы (в этом случае предполагается, что программе будет выделено столько памяти, сколько нужно, чтобы сборщик мусора приступил к своим обязанностям).
Обычно следует считать, что версия finalize() базового класса делает что-то важное, и вызывать ее в синтаксисе super, как показано в Book.finalize(). В данном примере вызов закомментирован, потому что он требует обработки исключений, а эта тема нами еще не рассматривалась.
Как работает сборщик мусора
Если ранее вы работали на языке программирования, в котором выделение места для объектов в куче было связано с большими издержками, то вы можете предположить, что и в Java механизм выделения памяти из кучи для всех данных (за исключением примитивов) также обходится слишком дорого. Однако в действительности использование сборщика мусора дает немалый эффект по ускорению создания объектов. Сначала это может звучать немного странно — освобождение памяти сказывается на ее выделении — но именно так работают некоторые JVM, и это значит, что резервирование места для объектов в куче Java не уступает по скорости выделению пространства в стеке в других языках.
Представтьте кучу языка C++ в виде лужайки, где каждый объект «застолбил» свой собственный участок. Позднее площадка освобождается для повторного использования. В некоторых виртуальных машинах Java куча выглядит совсем иначе; она скорее похоже на ленту конвейера, которая передвигается вперед при создании нового объекта. А это значит, что скорость выделения хранилища для объекта оказывается весьма высокой. «Указатель кучи» просто передвигается вперед в «невозделанную» территорию, и по эффективности этот процесс близок к выделению памяти в стеке C++. (Конечно, учет выделенного пространства сопряжен с небольшими издержками, но их никоим образом нельзя сравнить с затратами, возникающими при поиске свободного блока в памяти.)
Конечно, использование кучи в режиме «ленты конвейера» не может продолжаться бесконечно, и рано или поздно память станет сильно фрагментирована (что заметно снижает производительность), а затем и вовсе исчерпается. Как раз здесь в действие вступает сборщик мусора; во время своей работы он компактно размещает объекты кучи, как бы смещая «указатель кучи» ближе к началу «ленты», тем самым предотвращая фрагментацию памяти. Сборщик мусора реструктуризует внутреннее расположение объектов в памяти и позволит получить высокоскоростную модель кучи для резервирования памяти.
Чтобы понять, как работает сборка мусора в Java, необходимо узнать, как устроены реализации сборщиков мусора (СМ) в других системах. Простой, но медленный механизм СМ называется подсчетом ссылок. С каждым объектом хранится счетчик ссылок на него, и всякий раз при присоединении новой ссылки к объекту этот счетчик увеличивается. Каждый раз при выходе ссылки из области действия или установке ее значения в null счетчик ссылок уменьшается. Таким образом, подсчет ссылок создает небольшие, но постоянные издержки во время работы вашей программы. Сборщик мусора перебирает объект за объектом списка; обнаружив объект с нулевым счетчиком, он освобождает ресурсы, занимаемые этим объектом. Но существует одна проблема — если объекты содержат циклические ссылки друг на друга, их счетчики ссылок не обнуляются, хотя на самом деле объекты уже являются «мусором». Обнаружение таких «циклических» групп является серьезной работой и отнимает у сборщика мусора достаточно времени. Подсчет ссылок часто используется для объяснения принципов процесса сборки мусора, но, судя по всему, он не используется ни в одной из виртуальных машин Java.
В более быстрых схемах сборка мусора не зависит от подсчета ссылок. Вместо этого она опирается на идею, что любой существующий объект прослеживается до ссылки, находящейся в стеке или в статической памяти. Цепочка проверки проходит через несколько уровней объектов. Таким образом, если начать со стека и статического хранилища, мы обязательно доберемся до всех используемых объектов. Для каждой найденной ссылки надо взять объект, на который она указывает, и отследить все ссылки этого объекта; при этом выявляются другие объекты, на которые они указывают, и так далее, пока не будет проверена вся инфраструктура ссылок, берущая начало в стеке и статической памяти. Каждый объект, обнаруженный в ходе поиска, все еще используется в системе. Заметьте, что проблемы циклических ссылок не существует — такие ссылки просто не обнаруживаются, и поэтому становятся добычей сборщика мусора автоматически.
В описанном здесь подходе работает адаптивный механизм сбора мусора, при котором JVM обращается с найденными используемыми объектами согласно определенному варианту действий. Один из таких вариантов называется ос- тановить-и-копировать. Смысл термина понятен: работа программы временно приостанавливается (эта схема не поддерживает сборку мусора в фоновом режиме). Затем все найденные «живые» (используемые) объекты копируются из одной кучи в другую, а «мусор» остается в первой. При копировании объектов в новую кучу они размещаются в виде компактной непрерывной цепочки, высвобождая пространство в куче {и позволяя удовлетворять заказ на новое хранилище простым перемещением указателя).
Конечно, когда объект перемещается из одного места в другое, все ссылки, указывающие на него, должны быть изменены. Ссылки в стеке или в статическом хранилище переопределяются сразу, но могут быть и другие ссылки на этот объект, которые исправляются позже, во время очередного «прохода». Исправление происходит по мере нахождения ссылок.
Существует два фактора, из-за которых «копирующие сборщики» обладают низкой эффективностью. Во-первых, в системе существует две кучи, и вы «перелопачиваете» память то туда, то сюда между двумя отдельными кучами, при этом половина памяти тратится впустую. Некоторые JVM пытаются решить эту проблему, выделяя память для кучи небольшими порциями по мере необходимости, а затем просто копируя одну порцию в другую.
Второй вопрос — копирование. Как только программа перейдет в фазу стабильной работы, она обычно либо становится «безотходной», либо производит совсем немного «мусора». Несмотря на это, копирующий сборщик все равно не перестанет копировать память из одного места в другое, что расточительно. Некоторые JVM определяют, что новых «отходов» не появляется, и переключаются на другую схему («адаптивная» часть). Эта схема называется пометить-и-убрать (удалить), и именно на ней работали ранние версии виртуальных машин фирмы Sun. Для повсеместного использования вариант «пометить-и-убрать» чересчур медлителен, но, когда известно, что нового «мусора» мало или вообще нет, он выполняется быстро.
Схема «пометить-и-убрать» использует ту же логику — проверка начинается со стека и статического хранилища, после чего постепенно обнаруживаются все ссылки на «живые» объекты. Однако каждый раз при нахождении объект помечается флагом, но еще продолжает существование. «Уборка» происходит только после завершения процесса проверки и пометки. Все «мертвые» объекты при этом удаляются. Но копирования не происходит, и если сборщик решит «упаковать» фрагментированную кучу, то делается это перемещением объектов внутри нее.
Идея «остановиться-и-копировать» несовместима с фоновым процессом сборки мусора; в начале уборки программа останавливается. В литературе фирмы Sun можно найти немало заявлений о том, что сборка мусора является фоновым процессом с низким приоритетом, но оказывается, что реализации в таком виде (по крайней мере в первых реализациях виртуальной машины Sun) в действительности не существует. Вместо этого сборщик мусора от Sun начинал выполнение только при нехватке памяти. Схема «пометить-и-убрать» также требует остановки программы.
Как упоминалось ранее, в описываемой здесь виртуальной машине память выделяется большими блоками. При создании большого объекта ему выделяется собственный блок. Строгая реализация схемы «остановиться-и-копировать» требует, чтобы каждый используемый объект из исходной кучи копировался в новую кучу перед освобождением памяти старой кучи, что сопряжено с большими перемещениями памяти. При работе с блоками памяти СМ использует незанятые блоки для копирования по мере их накопления. У каждого блока имеется счетчик поколений, следящий за использованием блока. В обычной ситуации «упаковываются» только те блоки, которые были созданы после последней сборки мусора; для всех остальных блоков значение счетчика увеличивается при создании внешних ссылок. Такой подход годится для стандартной ситуации — создания множества временных объектов с коротким сроком жизни. Периодически производится полная очистка — большие блоки не копируются (только наращиваются их счетчики), но блоки с маленькими объектами копируются и «упаковываются». Виртуальная машина постоянно следит за эффективностью сборки мусора и, если она становится неэффективной, потому что в программе остались только долгоживущие объекты, переключается на схему «пометить-и-убрать». Аналогично JVM следит за успешностью схемы «пометить-и-убрать», и, когда куча становится излишне фрагментированной, СМ переключается обратно к схеме «остановиться-и-копировать». Это и есть адаптивный механизм.
Существуют и другие способы ускорения работы в JVM. Наиболее важные — это действия загрузчика и то, что называется компиляцией «на лету» (Just-In-Time, JIT). Компилятор JIT частично или полностью конвертирует программу в «родной» машинный код, благодаря чему последний не нуждается в обработке виртуальной машиной и может выполняться гораздо быстрее. При загрузке класса (обычно это происходит при первом создании объекта этого класса) система находит файл .class, и байт-код из этого файла переносится в память. В этот момент можно просто провести компиляцию JIT для кода класса, но такой подход имеет два недостатка: во-первых, это займет чуть больше времени, что вместе с жизненным циклом программы может серьезно отразиться на производительности. Во-вторых, увеличивается размер исполняемого файла (байт-код занимает гораздо меньше места в сравнении с расширенным кодом JIT), что может привести к подкачке памяти, и это тоже замедлит программу. Альтернативная схема отложенного вычисления подразумевает, что код JIT компилируется только тогда, когда это станет необходимо. Иначе говоря, код, который никогда не исполняется, не компилируется JIT. Новая технология Java HotSpot, встроенная в последние версии JDK, делает это похожим образом с применением последовательной оптимизации кода при каждом его выполнении. Таким образом, чем чаще выполняется код, тем быстрее он работает.
Инициализация членов класса
Java иногда нарушает гарантии инициализации переменных перед их использованием. В случае с переменными, определенными локально, в методе, эта гарантия предоставляется в форме сообщения об ошибке. Скажем, при попытке использования фрагмента
void f() { int і;
і ++, // Ошибка - переменная і не инициализирована
}
вы получите сообщение об ошибке, указывающее на то, что переменная і не была инициализирована. Конечно, компилятор мог бы присваивать таким переменным значения по умолчанию, но данная ситуация больше похожа на ошибку программиста, и подобный подход лишь скрыл бы ее. Заставить программиста присвоить переменной значение по умолчанию — значит предотвратить ошибку в программе.
Если примитивный тип является полем класса, то и способ обращения с ним несколько иной. Как было показано в главе 2, каждому примитивному полю класса гарантированно присваивается значение по умолчанию. Следующая программа подтверждает этот факт и выводит значения:
//: initialization/InitialValues.java
// Вывод начальных значений, присваиваемых по умолчанию.
import static net.mindview.util.Print.*;
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues() {
print("Data type Initial value");
print("boolean " + t);
print("char [" + c + "]");
print("byte " + b);
print("short " + s);
print("int " + i);
print("long " + l);
print("float " + f);
print("double " + d);
print("reference " + reference);
}
public static void main(String[] args) {
InitialValues iv = new InitialValues();
iv.printInitialValues();
/* Тут возможен следующий вариант:
new InitialValues().printInitialValues();
*/
}
}
<spoiler text="Output:">
Data type Initial value
boolean false
char [ ]
byte 0
</spoiler> Присмотритесь — даже если значения явно не указываются, они автоматически инициализируются. (Символьной переменной char присваивается значение ноль, которое отображается в виде пробела.) По крайней мере, нет опасности случайного использования неинициализированной переменной.
Если ссылка на объект, определямая внутри класса, не связывается с новым объектом, то ей автоматически присваивается специальное значение null (ключевое слово Java).
Явная инициализация
Что делать, если вам понадобится придать переменной начальное значение? Проще всего сделать это прямым присваиванием этой переменной значения в точке ее объявления в классе. (Заметьте, что в C++ такое действие запрещено, хотя его постоянно пытаются выполнить новички.) В следующем примере полям уже знакомого класса InitialValues присвоены начальные значения:
//: initialization/InitialValues2.java
// Явное определение начальных значений переменных.
public class InitialValues2 {
boolean bool = true;
char ch = 'x';
byte b = 47;
short s = 0xff;
int i = 999;
long lng = 1;
float f = 3.14f;
double d = 3.14159;
}
Аналогичным образом можно инициализировать и не-примитивные типы. Если Depth является классом, вы можете добавить переменную и инициализировать ее следующим образом:
//: initialization/Measurement.java
class Depth {}
public class Measurement {
Depth d = new Depth();
// ...
}
Если вы попытаетесь использовать ссылку d, которой не задано начальное значение, произойдет ошибка времени исполнения, называемая исключением (исключения подробно описываются в главе 10). Начальное значение даже может задаваться вызовом метода:
//: initialization/MethodInit.java
public class MethodInit {
int i = f();
int f() { return 11; }
}
Конечно, метод может получать аргументы, но в качестве последних не должны использоваться неинициализированные члены класса. Например, так правильно:
//: initialization/MethodInit2.java
public class MethodInit2 {
int i = f();
int j = g(i);
int f() { return 11; }
int g(int n) { return n * 10; }
}
a так нет:
//: initialization/MethodInit3.java
public class MethodInit3 {
//! int j = g(i); // Illegal forward reference
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; }
// ...
}
переменной і сначала будет присвоено значение 0, а затем уже 7. Это верно для всех примитивных типов и ссылок на объекты, включая те, которым задаются явные значения в точке определения. По этим причинам компилятор не пытается заставить вас инициализировать элементы в конструкторе, или в ином определенном месте, или перед их использованием — инициализация и так гарантирована.
Порядок инициализации
Внутри класса очередность инициализации определяется порядком следования переменных, объявленных в этом классе. Определения переменных могут быть разбросаны по разным определениям методов, но в любом случае переменные инициализируются перед вызовом любого метода — даже конструктора. Например:
//: initialization/OrderOfInitialization.java
// Демонстрирует порядок инициализации.
import static net.mindview.util.Print.*;
// При вызове конструктора для создания объекта
// Window выводится сообщение:
class Window {
Window(int marker) { print("Window(" + marker + ")"); }
}
class House {
Window w1 = new Window(1); // Перед конструктором
House() {
// Показывает, что выполняется конструктор:
print("House()");
w3 = new Window(33); // Повторная инициализация w3
}
Window w2 = new Window(2); // После конструктора
void f() { print("f()"); }
Window w3 = new Window(3); // В конце
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f(); // Показывает, что объект сконструирован
}
}
<spoiler text="Output:">
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
</spoiler> В классе 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 f1(int marker) {
print("f1(" + marker + ")");
}
}
class Table {
static Bowl bowl1 = new Bowl(1);
Table() {
print("Table()");
bowl2.f1(1);
}
void f2(int marker) {
print("f2(" + marker + ")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard() {
print("Cupboard()");
bowl4.f1(2);
}
void f3(int marker) {
print("f3(" + marker + ")");
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
public static void main(String[] args) {
print("Creating new Cupboard() in main");
new Cupboard();
print("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}
<spoiler text="Output:">
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
</spoiler> Класс Bowl позволяет проследить за процессом создания классов; классы Table и Cupboard содержат определения статических объектов Bowl. Заметьте, что в классе Cupboard создается нестатическая переменная Bowl bowl3, хотя все остальные определения — статические.
Из выходных данных программы видно, что статическая инициализация происходит только в случае необходимости. Если вы не создаете объектов Table и никогда не обращаетесь к Table.bowl1 или Table.bowl2, то, соответственно, не будет и объектов static Bowl bowl1 иstatic Bowl bowl2. Они инициализируются только при создании первого объекта Table (или при первом обращении к статическим данным). После этого статические объекты повторно не переопределяются.
Сначала инициализируются static-члены, если они еще не были проинициализированы, и только затем нестатические объекты. Доказательство справедливости этого утверждения легко найти в результате работы программы. Для выполнения main() (а это статический метод!) загружается класс Staticlnitialization; затем инициализируются статические поля table и cupboard, вследствие чего загружаются эти классы. И так как все они содержат статические объекты Bowl, загружается класс Bowl. Таким образом, все классы программы загружаются до начала main(). Впрочем, эта ситуация нетипична, поскольку в рядовой программе не все поля объявляются как статические, как в данном примере.
Неплохо теперь обобщить знания о процессе создания объекта. Для примера возьмем класс с именем Dog:
Хотя ключевое слово static и не используется явно, конструктор в действительности является статическим методом. При создании первого объекта типа Dog или при первом вызове статического метода-обращения к статическому полю класса Dog, интерпретатор Javaдолжен найти класс Dog.class. Поиск осуществляется в стандартных каталогах, перечисленных в переменной окружения СLASSPATH.
После загрузки файла Dog.class (с созданием особого объекта Class, о котором мы узнаем позже) производится инициализация статических элементов. Таким образом, инициализация статических членов проводится только один раз, при первой загрузке объекта Class.
При создании нового объекта конструкцией new Dog() для начала выделяется блок памяти, достаточный для хранения объекта Dog в куче.
Выделенная память заполняется нулями, при этом все примитивные поля объекта Dog автоматически инициализируются значениями по умолчанию (ноль для чисел, его эквиваленты для типов boolean и char, null для ссылок).
Выполняются все действия по инициализации, происходящие в точке определения полей класса.
Выполняются конструкторы. Как вы узнаете из главы 7, на этом этапе выполняется довольно большая часть работы, особенно при использовании наследования.
Явная инициализация статических членов
Язык Java позволяет сгруппировать несколько действий по инициализации объектов static в специальной конструкции, называемой статическим блоком. Выглядит это примерно так:
//: initialization/Spoon.java
public class Spoon {
static int i;
static {
i = 47;
}
}
Похоже на определение метода, но на самом деле мы видим лишь ключевое слово static с последующим блоком кода. Этот код, как и остальная инициализация static, выполняется только один раз: при первом создании объекта этого класса или при первом обращении к статическим членам этого класса (даже если объект класса никогда не создается). Например:
//: initialization/ExplicitStatic.java
//Явная инициализация с использованием конструкции "static"
import static net.mindview.util.Print.*;
class Cup {
Cup(int marker) {
print("Cup(" + marker + ")");
}
void f(int marker) {
print("f(" + marker + ")");
}
}
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups() {
print("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
print("Inside main()");
Cups.cup1.f(99); // (1)
}
// static Cups cups1 = new Cups(); // (2)
// static Cups cups2 = new Cups(); // (2)
}
<spoiler text="Output:">
Inside main()
Cup(1)
Cup(2)
f(99)
</spoiler> Статический инициализатор класса Cups выполняется либо при обращении к статическому объекту cup1 в строке с пометкой (1), либо если строка (1) закомментирована — в строках (2) после снятия комментариев. Если же и строка (1), и строки (2) закомментированы, static-инициализация класса Cups никогда не выполнится. Также неважно, будут ли исполнены одна или обе строки (2) программы — static-инициализация все равно выполняется только один раз.
Инициализация нестатических данных экземпляра
В Java имеется сходный синтаксис для инициализации нестатических переменных для каждого объекта. Вот пример: .
//: initialization/Mugs.java
// "Инициализация экземпляра"
import static net.mindview.util.Print.*;
class Mug {
Mug(int marker) {
print("Mug(" + marker + ")");
}
void f(int marker) {
print("f(" + marker + ")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
print("mug1 & mug2 initialized");
}
Mugs() {
print("Mugs()");
}
Mugs(int i) {
print("Mugs(int)");
}
public static void main(String[] args) {
print("Inside main()");
new Mugs();
print("new Mugs() completed");
new Mugs(1);
print("new Mugs(1) completed");
}
}
<spoiler text="Output:">
Inside main()
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed
</spoiler> выглядит в точности так же, как и конструкция static-инициализации, разве что ключевое слово static отсутствует. Такой синтаксис необходим для поддержки инициализации анонимных внутренних классов (см. главу 9), но он также гарантирует, что некоторые операции будут выполнены независимо от того, какой именно конструктор был вызван в программе. Из результатов видно, что секция инициализации экземпляра выполняется раньше любых конструкторов.
Инициализация массивов
Массив представляет собой последовательность объектов или примитивов, относящихся к одному типу, обозначаемую одним идентификатором. Массивы определяются и используются с помощью оператора индексирования [ ]. Чтобы объявить массив, вы просто. указываете вслед за типом пустые квадратные скобки:
int[] al;
Квадратные скобки также могут размещаться после идентификатора, эффект будет точно таким же:
int al[];
Это соответствует ожиданиям программистов на C и C++, привыкших к такому синтаксису. Впрочем, первый стиль, пожалуй, выглядит более логично — он сразу дает понять, что имеется в виду «массив значений типа int». Он и будет использоваться в книге.
Компилятор не позволяет указать точный размер массива. Вспомните, что говорилось ранее о ссылках. Все, что у вас сейчас есть, — это ссылка на массив, для которого еще не было выделено памяти. Чтобы резервировать память для массива, необходимо записать некоторое выражение инициализации. Для массивов такое выражение может находиться в любом месте программы, но существует и особая разновидность выражений инициализации, используемая только в точке объявления массива. Эта специальная инициализация выглядит как набор значений в фигурных скобках. Выделение памяти (эквивалентное действию оператора new) в этом случае проводится компилятором. Например:
int[] al = { 1, 2, 3, 4, 5 };
Но зачем тогда вообще нужно определять ссылку на массив без самого массива?
int[] а2;
Во-первых, в Java можно присвоить один массив другому, записав следующее:
а2 = al;
В данном случае вы на самом деле копируете ссылку, как показано в примере:
//: initialization/ArraysOfPrimitives.java
// Массивы простейших типов.
import static net.mindview.util.Print.*;
public class ArraysOfPrimitives {
public static void main(String[] args) {
int[] a1 = { 1, 2, 3, 4, 5 };
int[] a2;
a2 = a1;
for(int i = 0; i < a2.length; i++)
a2[i] = a2[i] + 1;
for(int i = 0; i < a1.length; i++)
print("a1[" + i + "] = " + a1[i]);
}
}
<spoiler text="Output:">
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6
</spoiler> Массив a1 инициализируется набором значений, в то время как массив а2 — нет; присваивание по ссылке а2 присваивается позже — в данном случае присваивается другой массив.
Все массивы (как массивы примитивов, так и массивы объектов) содержат поле> которое можно прочитать (но не изменить!) для получения количества элементов в массиве. Это поле называется length. Так как в массивах Java, C и C++ . Нумерация элементов начинается с нуля, последнему элементу массива соответствует индекс length—1. При выходе за границы массива C и C++ не препятствуют «прогулкам в памяти» программы, что часто приводит к печальным последствиям. Но Java защищает вас от таких проблем — при выходе за рамки массива происходит ошибка времени исполнения (исключение, тема главы 10).
А если во время написания программы вы не знаете, сколько элементов вам понадобится в новом массиве? Тогда просто используйте new для создания его элементов. В следующем примере new работает, хотя в программе создается массив примитивных типов (операторnew неприменим для создания примитивов вне массива):
//: initialization/ArrayNew.java
// Создание массивов оператором new.
import java.util.*;
import static net.mindview.util.Print.*;
public class ArrayNew {
public static void main(String[] args) {
int[] a;
Random rand = new Random(47);
a = new int[rand.nextInt(20)];
print("length of a = " + a.length);
print(Arrays.toString(a));
}
}
<spoiler text="Output:">
length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
</spoiler> Размер массива выбирается случайным образом, с использованием метода Random.nextInt(), генерирующего число от нуля до переданного в качестве аргумента значения. Так как размер массива случаен, очевидно, что создание массива происходит во время исполнения программы. Вдобавок, результат работы программы позволяет убедиться в том, что элементы массивов простейших типов автоматически инициализируются «пустыми» значениями. (Для чисел и символов это ноль, а для логического типа boolean — false.)
Метод Arrays.toString(), входящий в стандартную библиотеку java.util, выдает печатную версию одномерного массива.
Конечно, в данном примере массив можно определить и инициализировать в одной строке:
int[] а = new int[rand.nextInt(20)];
Если возможно, рекомендуется использовать именно такую форму записи. При создании массива непримитивных объектов вы фактически создаете массив ссылок. Для примера возьмем класс-обертку Integer, который является именно классом, а не примитивом:
//: initialization/ArrayClassObj.java
// Создание массива непримитивных объектов.
import java.util.*;
import static net.mindview.util.Print.*;
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
print("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
a[i] = rand.nextInt(500); // Автоматическая упаковка
print(Arrays.toString(a));
}
}
<spoiler text="Output:"> (Sample)
length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
</spoiler> Здесь даже после вызова new для создания массива
Integer[] а = new Integer[rand nextlnt(20)];
мы имеем лишь массив из ссылок — до тех пор, пока каждая ссылка не будет инициализирована новым объектом Integer (в данном случае это делается посредством автоупаковки):
a[i] = rand.nextlnt(500);
Если вы забудете создать объект, то получите исключение во время выполнения программы, при попытке чтения несуществующего элемента массива. Массивы объектов также можно инициализировать списком в фигурных скобках. Существует две формы синтаксиса:
//: initialization/ArrayInit.java
// Инициализация массивов
import java.util.*;
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
new Integer(1),
new Integer(2),
3, // Autoboxing
};
Integer[] b = new Integer[]{
new Integer(1),
new Integer(2),
3, // Автоматическая упаковка
};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
}
<spoiler text="Output:">
[1, 2, 3]
[1, 2, 3]
</spoiler> В обоих случаях завершающая запятая в списке инициализаторов не обязательна (она всего лишь упрощает ведение длинных списков). Первая форма полезна, но она более ограничена, поскольку может использоваться только в точке определения массива. Вторая форма может использоваться везде, даже внутри вызова метода.
Списки аргументов переменной длины
Синтаксис второй формы предоставляет удобный синтаксис создания и вызова методов с эффектом, напоминающим списки аргументов переменной длины языка C. Такой список способен содержать неизвестное заранее количество аргументов неизвестного типа. Так как абсолютно все классы унаследованы от общего корневого класса Object, можно создать метод, принимающий в качестве аргумента массив Object, и вызывать его следующим образом:
//: initialization/VarArgs.java
// Использование синтаксиса массивов
// для получения переменного списка параметров.
class A {}
public class VarArgs {
static void printArray(Object[] args) {
for(Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[]{
new Integer(47), new Float(3.14), new Double(11.11)
});
printArray(new Object[]{"one", "two", "three" });
printArray(new Object[]{new A(), new A(), new A()});
}
}
<spoiler text="Output:"> (Sample)
47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f
</spoiler> Видно, что метод print() принимает массив объектов типа Object, перебирает его элементы и выводит их. Классы из стандартной библиотеки Java при печати выводят осмысленную информацию, однако объекты классов в данном примере выводят имя класса, затем символ @ и несколько шестнадцатеричных цифр. Таким образом, по умолчанию класс выводит имя и адрес объекта (если только вы не переопределите в классе метод toString() — см. далее).
До выхода Java SE5 переменные списки аргументов реализовывались именно так. В Java SE5 эта долгожданная возможность наконец-то была добавлена в язык — теперь для определения переменного списка аргументов может использоваться многоточие, как видно в определении метода printArray:
//: initialization/NewVarArgs.java
/ Создание списков аргументов переменной длины
// с использованием синтаксиса массивов.
public class NewVarArgs {
static void printArray(Object... args) {
for(Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
// Можно передать отдельные элементы:
printArray(new Integer(47), new Float(3.14),
new Double(11.11));
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
// Или массив:
printArray((Object[])new Integer[]{ 1, 2, 3, 4 });
printArray(); // Пустой список тоже возможен
}
}
<spoiler text="Output:"> (75% match)
47 3.14 11.11
47 3.14 11.11
one two three
A@1bab50a A@c3c749 A@150bd4d
1 2 3 4
</spoiler>
Резюме
Такой сложный механизм инициализации, как конструктор, показывает, насколько важное внимание в языке уделяется инициализации. Когда Бьерн Страуструп разрабатывал C++, в первую очередь он обратил внимание на то, что низкая продуктивность C связана с плохо продуманной инициализацией, которой была обусловлена значительная доля ошибок. Аналогичные проблемы возникают и при некорректной финализации. Так как конструкторы позволяют гарантировать соответствующие инициализацию и завершающие действия по очистке (компилятор не позволит создать объект без вызова конструктора), тем самым обеспечивается полная управляемость и защищенность программы.
В языке C++ уничтожение объектов играет очень важную роль, потому что объекты, созданные оператором new, должны быть соответствующим образом разрушены. В Java память автоматически освобождается сборщиком мусора, и аналоги деструкторов обычно не нужны. В таких случаях сборщик мусора Java значительно упрощает процесс программирования и к тому же добавляет так необходимую безопасность при освобождении ресурсов. Некоторые сборщики мусора могут проводить завершающие действия даже с такими ресурсами, как графические и файловые дескрипторы. Однако сборщики мусора добавляют издержки во время выполнения программы, которые пока трудно реально оценить из-за сложившейся исторически медлительности интерпретаторов Java. И хотя в последнее время язык Java намного улучшил свою производительность, проблема его «задумчивости» все-таки наложила свой отпечаток на возможность решения языком некоторого класса задач.
Так как для всех объектов гарантированно используются конструкторы, на последние возлагаются дополнительные обязанности, не описанные в этой главе. В частности, гарантия конструирования действует и при создании новых классов с использованием композиции или наследования, и для их поддержки требуются некоторые дополнения к синтаксису языка. Композиция и наследование, а также их влияние на конструкторы, рассматриваются в следующих главах.
