Содержание
· 1 ИНИЦИАЛИЗАЦИЯ И ЗАВЕРШЕНИЕ
o 1.1 Конструктор гарантирует инициализацию
o 1.2 Перегрузка методов
o 1.3 Различение перегруженных методов
o 1.4 Перегрузка с примитивами
o 1.5 Перегрузка по возвращаемым значениям
o 1.6 Конструкторы по умолчанию
o 1.7 Ключевое слово this
o 1.8 Вызов конструкторов из конструкторов
o 1.9 Значение ключевого слова static
o 1.10 Очистка: финализация и сборка мусора
§ 1.10.1 Для чего нужен метод finalize()?
§ 1.10.2 Очистка — ваш долг
§ 1.10.3 Условие «готовности»
§ 1.10.4 Как работает сборщик мусора
o 1.11 Инициализация членов класса
§ 1.11.1 Явная инициализация
§ 1.11.2 Инициализация конструктором
§ 1.11.3 Порядок инициализации
§ 1.11.4 Инициализация статических данных
§ 1.11.5 Явная инициализация статических членов
§ 1.11.6 Инициализация нестатических данных экземпляра
o 1.12 Инициализация массивов
o 1.13 Списки аргументов переменной длины
o 1.14 Резюме
В ходе компьютерной революции выяснилось, что основной причиной чрезмерных затрат в программировании является «небезопасное» программирование. Основные проблемы с безопасностью относятся к инициализации и завершению. Очень многие ошибки при программировании на языке C обусловлены неверной инициализацией переменных. Это особенно часто происходит при работе с библиотеками, когда пользователи не знают, как нужно инициализировать компонент библиотеки, или забывают это сделать. Завершение — очень актуальная проблема; слишком легко забыть об элементе, когда вы закончили с ним работу и его дальнейшая судьба вас не волнует. В этом случае ресурсы, занимаемые элементом, не освобождаются, и в программе может возникнуть нехватка ресурсов (прежде всего памяти).
В C++ появилось понятие конструктора — специального метода, который вызывается при создании нового объекта. Конструкторы используются и в Java; к тому же в Java есть сборщик мусора, который автоматически освобождает ресурсы, когда объект перестает использоваться. В этой главе рассматриваются вопросы инициализации и завершения, а также их поддержка в Java.
Конструктор гарантирует инициализацию
Конечно, можно создать особый метод, назвать его initialize() и включить во все ваши классы. Имя метода подсказывает пользователю, что он должен вызвать этот метод, прежде чем работать с объектом. К сожалению, это означает, что пользователь должен постоянно помнить о необходимости вызова данного метода. В Java разработчик класса может в обязательном порядке выполнить инициализацию каждого объекта при помощи специального метода, называемого конструктором. Если у класса имеется конструктор, Java автоматически вызывает его при создании объекта, перед тем как пользователи смогут обратиться к этому объекту. Таким образом, инициализация объекта гарантирована.
Как должен называться конструктор? Здесь есть две тонкости. Во-первых, любое имя, которое вы используете, может быть задействовано при определении членов класса; так возникает потенциальный конфликт имен. Во-вторых, за вызов конструктора отвечает компилятор, поэтому он всегда должен знать, какой именно метод следует вызвать. Реализация конструктора в C++ кажется наиболее простым и логичным решением, поэтому оно использовано и в Java: имя конструктора совпадает с именем класса. Смысл такого решения очевиден — именно такой метод способен автоматически вызываться при инициализации. Рассмотрим определение простого класса с конструктором:
//: initialization/SimpleConstructor.java
// Demonstration of a simple constructor.
// Демонстрация простого конструктора
class Rock {
Rock() { // This is the constructor
System.out.print("Rock ");
}
}
public class SimpleConstructor {
public static void main(String[] args) {
for(int i = 0; i < 10; i++)
new Rock();
}
}
<spoiler text="Output:">
Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
</spoiler> Теперь при создании объекта:
new Rock( );
выделяется память и вызывается конструктор. Тем самым гарантируется, что объект будет инициализирован, прежде чем программа сможет работать с ним.
Заметьте, что стиль программирования, при котором имена методов начинаются со строчной буквы, к конструкторам не относится, поскольку имя конструктора должно точно совпадать с именем класса. Подобно любому методу, у конструктора могут быть аргументы, для того чтобы позволить вам указать, как создать объект. Предыдущий пример легко изменить так, чтобы конструктору при вызове передавался аргумент:
//: initialization/SimpleConstructor2.java
// Constructors can have arguments.
// Конструкторы могут получать аргументы
class Rock2 {
Rock2(int i) {
System.out.print("Rock " + i + " ");
}
}
public class SimpleConstructor2 {
public static void main(String[] args) {
for(int i = 0; i < 8; i++)
new Rock2(i);
}
}
<spoiler text="Output:">
Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7
</spoiler> В аргументах конструктора передаются параметры для инициализации объекта. Например, если у класса Tree (дерево) имеется конструктор, который получает в качестве аргумента целое число, обозначающее высоту дерева, то объекты Tree будут создаваться следующим образом:
Tree t = new Тrее(12), // 12-метровое дерево
Если Tree(int initialHeight) является единственным конструктором класса, то компилятор не позволит создавать объекты Tree каким-либо другим способом. Конструкторы устраняют большой пласт проблем и упрощают чтение кода. В предыдущем фрагменте кода не встречаются явные вызовы метода, подобного initialize(), который концептуально отделен от создания. В Java создание и инициализация являются неразделимыми понятиями — одно без другого невозможно.
Конструктор — не совсем обычный метод, так как у него отсутствует возвращаемое значение. Это ощутимо отличается даже от случая с возвратом значения void, когда метод ничего не возвращает, но при этом все же можно заставить его вернуть что-нибудь другое. Конструкторы не возвращают никогда и ничего (оператор new возвращает ссылку на вновь созданный объект, но сами конструкторы не имеют выходного значения). Если бы у них существовало возвращаемое значение и его можно было бы выбирать, то компилятору пришлось бы как-то объяснять, что же делать с этим значением.
Перегрузка методов
Одним из важнейших аспектов любого языка программирования является использование имен. Создавая объект, вы фактически присваиваете имя области памяти. Метод — имя для действия. Использование имен при описании системы упрощает ее понимание и модификацию. Работа программиста сродни работе писателя; в обоих случаях задача состоит в том, чтобы донести свою мысль до читателя.
Проблемы возникают при перенесении нюансов человеческого языка в языки программирования. Часто одно и то же слово имеет несколько разных значений — оно перегружено. Это полезно, особенно в отношении простых различий. Вы говорите «вымыть посуду», «вымыть машину» и «вымыть собаку». Было бы глупо вместо этого говорить «посудоМыть посуду», «машиноМыть машину» и «собакоМыть собаку» только для того, чтобы слушатель не утруждал себя выявлением разницы между этими действиями. Большинство человеческих языков несет избыточность, и даже при пропуске некоторых слов определить смысл не так сложно. Уникальные имена не обязательны — сказанное можно понять из контекста.
Большинство языков программирования (и в особенности C) требовали использования уникальных имен для всех функций. Иначе говоря, программа не могла содержать функцию print() для распечатки целых чисел и одноименную функцию для вывода вещественных чисел — каждая функция должна была иметь уникальное имя.
В Java (и в C++) также существует другой фактор, который заставляет использовать перегрузку имен методов: наличие конструкторов. Так как имя конструктора предопределено именем класса, оно может быть только единственным. Но что, если вы захотите создавать объекты разными способами? Допустим, вы создаете класс с двумя вариантами инициализации: либо стандартно, либо на основании из некоторого файла. В этом случае необходимость двух конструкторов очевидна: один из них не имеет аргументов (конструктор по умолчанию, также называемый конструктором без аргументов (no-arg)), а другой получает в качестве аргумента строку с именем файла. Оба они являются полноценными конструкторами, и поэтому должны называться одинаково — именем класса.
Здесь перегрузка методов (overloading) однозначно необходима, чтобы мы могли использовать методы с одинаковыми именами, но с разными аргументами. И хотя перегрузка методов обязательна только для конструкторов, она удобна в принципе и может быть применена к любому методу. Следующая программа показывает пример перегрузки как конструктора, так и обычного метода:
//: initialization/Overloading.java
// Demonstration of both constructor
// and ordinary method overloading.
// Демонстрация перегрузки конструкторов наряду
// с перегрузкой обычных методов
import static net.mindview.util.Print.*;
class Tree {
int height;
Tree() {
print("Planting a seedling");
height = 0;
}
Tree(int initialHeight) {
height = initialHeight;
print("Creating new Tree that is " +
height + " feet tall");
}
void info() {
print("Tree is " + height + " feet tall");
}
void info(String s) {
print(s + ": Tree is " + height + " feet tall");
}
}
public class Overloading {
public static void main(String[] args) {
for(int i = 0; i < 5; i++) {
Tree t = new Tree(i);
t.info();
t.info("overloaded method");
}
// Перегруженный конструктор:
new Tree();
}
}
<spoiler text="Output:">
Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling
</spoiler> Объект Tree (дерево) может быть создан или в форме ростка (без аргументов), или в виде «взрослого растения» с некоторой высотой. Для этого в классе определяются два конструктора; один используется по умолчанию, а другой получает аргумент с высотой дерева. Возможно, вы захотите вызывать метод info() несколькими способами. Например, вызов с аргументом-строкой info(String) используется при необходимости вывода дополнительной информации, а вызов без аргументов info() — когда дополнений к сообщению метода не требуется. Было бы странно давать два разных имени методам, когда их схожесть столь очевидна. К счастью, перегрузка методов позволяет использовать одно и то же имя для обоих методов.
Различение перегруженных методов
Если у методов одинаковые имена, как Java узнает, какой именно из них вызывается? Ответ прост: каждый перегруженный метод должен иметь уникальный список типов аргументов.
Если немного подумать, такой подход оказывается вполне логичным. Как еще различить два одноименных метода, если не по типу аргументов?
Даже разного порядка аргументов достаточно для того, чтобы методы считались разными (хотя описанный далее подход почти не используется, так как он усложняет сопровождение программного кода):
//: initialization/OverloadingOrder.java
// Overloading based on the order of the arguments.
// Перегрузка, основанная на порядке
// следования аргументов
import static net.mindview.util.Print.*;
public class OverloadingOrder {
static void f(String s, int i) {
print("String: " + s + ", int: " + i);
}
static void f(int i, String s) {
print("int: " + i + ", String: " + s);
}
public static void main(String[] args) {
f("String first", 11);
f(99, "Int first");
}
}
<spoiler text="Output:">
String: String first, int: 11
int: 99, String: Int first
</spoiler> Два метода f() имеют одинаковые аргументы с разным порядком следования, и это различие позволяет идентифицировать метод.
Перегрузка с примитивами
Простейший тип может быть автоматически приведен от меньшего типа к большему, и это в состоянии привнести немалую путаницу в перегрузку. Следующий пример показывает, что происходит при передаче примитивного типа перегруженному методу:
</spoiler> Если вы рассмотрите результат работы программы, то увидите, что константа 5 трактуется как int, поэтому если есть перегруженный метод, принимающий аргумент типа int, то он и используется. Во всех остальных случаях, если имеется тип данных, «меньший», чем требуется для существующего метода, то этот тип данных повышается соответственным образом. Только тип char ведет себя несколько иначе по той причине, что, если метода с параметром char нет, этот тип приводится сразу к типу int, а не к промежуточным типамbyte или short.
Что же произойдет, если ваш аргумент «больше», чем аргумент, требующийся в перегруженном методе? Ответ можно найти в модификации рассмотренной программы:
//: initialization/Demotion.java
// Demotion of primitives and overloading.
// Понижение примитивов и перегрузка.
import static net.mindview.util.Print.*;
public class Demotion {
void f1(char x) { print("f1(char)"); }
void f1(byte x) { print("f1(byte)"); }
void f1(short x) { print("f1(short)"); }
void f1(int x) { print("f1(int)"); }
void f1(long x) { print("f1(long)"); }
void f1(float x) { print("f1(float)"); }
void f1(double x) { print("f1(double)"); }
void f2(char x) { print("f2(char)"); }
void f2(byte x) { print("f2(byte)"); }
void f2(short x) { print("f2(short)"); }
void f2(int x) { print("f2(int)"); }
void f2(long x) { print("f2(long)"); }
void f2(float x) { print("f2(float)"); }
void f3(char x) { print("f3(char)"); }
void f3(byte x) { print("f3(byte)"); }
void f3(short x) { print("f3(short)"); }
void f3(int x) { print("f3(int)"); }
void f3(long x) { print("f3(long)"); }
void f4(char x) { print("f4(char)"); }
void f4(byte x) { print("f4(byte)"); }
void f4(short x) { print("f4(short)"); }
void f4(int x) { print("f4(int)"); }
void f5(char x) { print("f5(char)"); }
void f5(byte x) { print("f5(byte)"); }
void f5(short x) { print("f5(short)"); }
void f6(char x) { print("f6(char)"); }
void f6(byte x) { print("f6(byte)"); }
void f7(char x) { print("f7(char)"); }
void testDouble() {
double x = 0;
print("double argument:");
f1(x);f2((float)x);f3((long)x);f4((int)x);
f5((short)x);f6((byte)x);f7((char)x);
}
public static void main(String[] args) {
Demotion p = new Demotion();
p.testDouble();
}
}
<spoiler text="Output:">
double argument:
f1(double)
f2(float)
f3(long)
f4(int)
f5(short)
f6(byte)
f7(char)
</spoiler> Здесь методы требуют сужения типов данных. Если ваш аргумент «шире», необходимо явно привести его к нужному типу. В противном случае компилятор выведет сообщение об ошибке.
Перегрузка по возвращаемым значениям
Вполне логично спросить, почему при перегрузке используются только имена классов и списки аргументов? Почему не идентифицировать методы по их возвращаемым значениям? Следующие два метода имеют одинаковые имена и аргументы, но их легко отличить друг от друга:
void f() {}
int f() {}
Такой подход прекрасно сработает в ситуации, в которой компилятор может однозначно выбрать нужную версию метода, например: int х = f(). Однако возвращаемое значение при вызове метода может быть проигнорировано; это часто называется вызовом метода для получения побочного эффекта, так как метод вызывается не для естественного результата, а для каких-то других целей. Допустим, метод вызывается следующим способом:
f();
Как здесь Java определит, какая из версий метода f() должна выполняться? И поймет ли читатель программы, что происходит при этом вызове? Именно из-за подобных проблем перегруженные методы не разрешается различать по возвращаемым значениям.
Конструкторы по умолчанию
Как упоминалось ранее, конструктором по умолчанию называется конструктор без аргументов, применяемый для создания «типового» объекта. Если созданный вами класс не имеет конструктора, компилятор автоматически добавит конструктор по умолчанию. Например:
//: initialization/DefaultConstructor.java
class Bird {}
public class DefaultConstructor {
public static void main(String[] args) {
Bird b = new Bird(); // Default!
}
}
создает новый объект и вызывает конструктор по умолчанию, хотя последний и не был явно определен в классе. Без него не существовало бы метода для построения объекта класса из данного примера. Но если вы уже определили некоторый конструктор (или несколько конструкторов, с аргументами или без), компилятор не будет генерировать конструктор по умолчанию:
//: initialization/NoSynthesis.java
class Bird2 {
Bird2(int i) {}
Bird2(double d) {}
}
public class NoSynthesis {
public static void main(String[] args) {
//! Bird2 b = new Bird2(); // No default
Bird2 b2 = new Bird2(1);
Bird2 b3 = new Bird2(1.0);
}
}
Теперь при попытке выполнения new Bird2() компилятор заявит, что не может найти конструктор, подходящий по описанию. Получается так: если определения конструкторов отсутствуют, компилятор скажет: «Хотя бы один конструктор необходим, позвольте создать его за вас». Если же вы записываете конструктор явно, компилятор говорит: «Вы написали конструктор, а следовательно, знаете, что вам нужно; и если вы создали конструктор по умолчанию, значит, он вам и не нужен».
Ключевое слово this
Если у вас есть два объекта одинакового типа с именами а и b, вы, возможно, заинтересуетесь, каким образом производится вызов метода peel() для обоих объектов:
//: initialization/BananaPeel.java
class Banana { void peel(int i) { /* ... */ } }
public class BananaPeel {
public static void main(String[] args) {
Banana a = new Banana(),
b = new Banana();
a.peel(1);
b.peel(2);
}
}
Если существует только один метод с именем peel(), как этот метод узнает, для какого объекта он вызывается — а или b?
Чтобы программа могла записываться в объектно-ориентированном стиле, основанном на «отправке сообщений объектам», компилятор выполняет для вас некоторую тайную работу. При вызове метода peel() передается скрытый первый аргумент — не что иное, как ссылка на используемый объект. Таким образом, вызовы указанного метода на самом деле можно представить как:
Banana.рееl(a,1);
Banana.peel(b,2);
Передача дополнительного аргумента относится к внутреннему синтаксису. При попытке явно воспользоваться ею компилятор выдает сообщение об ошибке, но вы примерно представляете суть происходящего.
Предположим, во время выполнения метода вы хотели бы получить ссылку на текущий объект. Так как эта ссылка передается компилятором скрытно, идентификатора для нее не существует. Но для решения этой задачи существует ключевое слово — this. Ключевое словоthis может использоваться только внутри не-статического метода и предоставляет ссылку на объект, для которого был вызван метод. Обращаться с ней можно точно так же, как и с любой другой ссылкой на объект. Помните, что при вызове метода вашего класса из другого метода этого класса this вам не нужно; просто укажите имя метода. Текущая ссылка this будет автоматически использована в другом методе. Таким образом, продолжая сказанное:
//: initialization/Apricot.java
public class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ }
}
Внутри метода pit() можно использовать запись this.pick(), но в этом нет необходимости. Компилятор сделает это автоматически. Ключевое слово this употребляется только в особых случаях, когда вам необходимо явно сослаться на текущий объект. Например, оно часто применяется для возврата ссылки на текущий объект в команде return:
//: initialization/Leaf.java
// Simple use of the "this" keyword.
public class Leaf {
int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
}
<spoiler text="Output:">
i = 3
</spoiler> Так как метод increment() возвращает ссылку на текущий объект посредством ключевого слова this, над одним и тем же объектом легко можно провести множество операций. Ключевое слово this также может пригодиться для передачи текущего объекта другому методу:
//: initialization/PassingThis.java
class Person {
public void eat(Apple apple) {
Apple peeled = apple.getPeeled();
System.out.println("Yummy");
}
}
class Peeler {
static Apple peel(Apple apple) {
// ... remove peel
return apple; // Peeled
}
}
class Apple {
Apple getPeeled() { return Peeler.peel(this); }
}
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple());
}
}
<spoiler text="Output:">
Yummy
</spoiler> Класс Apple вызывает Peeler.peel() — вспомогательный метод, который по какой-то причине должен быть оформлен как внешний по отношению к Apple (может быть, он должен обслуживать несколько разных классов, и вы хотите избежать дублирования кода). Для передачи текущего объекта внешнему методу используется ключевое слово this.
Вызов конструкторов из конструкторов
Если вы пишете для класса несколько конструкторов, иногда бывает удобно вызвать один конструктор из другого, чтобы избежать дублирования кода. Такая операция проводится с использованием ключевого слова this. Обычно при употреблении this подразумевается «этот объект» или «текущий объект», и само слово является ссылкой на текущий объект. В конструкторе ключевое слово this имеет другой смысл: при использовании его со списком аргументов вызывается конструктор, соответствующий данному списку. Таким образом, появляется возможность прямого вызова других конструкторов:
//: initialization/Flower.java
// Calling constructors with "this"
import static net.mindview.util.Print.*;
public class Flower {
int petalCount = 0;
String s = "initial value";
Flower(int petals) {
petalCount = petals;
print("Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss) {
print("Constructor w/ String arg only, s = " + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//! this(s); // Can't call two!
this.s = s; // Another use of "this"
print("String & int args");
}
Flower() {
this("hi", 47);
print("default constructor (no args)");
}
void printPetalCount() {
//! this(11); // Not inside non-constructor!
print("petalCount = " + petalCount + " s = "+ s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.printPetalCount();
}
}
<spoiler text="Output:">
Constructor w/ int arg only, petalCount= 47
String & int args
default constructor (no args)
petalCount = 47 s = hi
</spoiler> Конструктор Flower(String s, int petals) показывает, что при вызове одного конструктора через this вызывать второй запрещается. Вдобавок вызов другого конструктора должен быть первой выполняемой операцией, иначе компилятор выдаст сообщение об ошибке.
Пример демонстрирует еще один способ использования this. Так как имена аргумента s и поля данных класса s совпадают, возникает неоднозначность. Разрешить это затруднение можно при помощи конструкции this.s, однозначно определяющей поле данных класса. Вы еще не раз встретите такой подход в различных Java-программах, да и в этой книге он практикуется довольно часто.
Метод printPetalCount() показывает, что компилятор не разрешает вызывать конструктор из обычного метода; это разрешено только в конструкторах.
Значение ключевого слова static
Ключевое слово this поможет лучше понять, что же фактически означает объявление статического (static) метода. У таких методов не существует ссылки this. Вы не в состоянии вызывать нестатические методы из статических (хотя обратное позволено), и статические методы можно вызывать для имени класса, без каких-либо объектов. Статические методы отчасти напоминают глобальные функции языка C, но с некоторыми исключениями: глобальные функции не разрешены в Java, и создание статического метода внутри класса дает ему право на доступ к другим статическим методам и полям.
Некоторые люди утверждают, что статические методы со своей семантикой глобальной функции противоречат объектно-ориентированной парадигме; в случае использования статического метода вы не посылаете сообщение объекту, поскольку отсутствует ссылка this. Возможно, что это справедливый упрек, и если вы обнаружите, что используете слишком много статических методов, то стоит пересмотреть вашу стратегию разработки программ. Однако ключевое слово static полезно на практике, и в некоторых ситуациях они определенно необходимы. Споры же о «чистоте ООП» лучше оставить теоретикам.
Очистка: финализация и сборка мусора
Программисты помнят и знают о важности инициализации, но часто забывают о значимости «приборки». Да и зачем, например, «прибирать» после использования обычной переменной int? Но при использовании программных библиотек «просто забыть» об объекте после завершения его работы не всегда безопасно. Конечно, в Java существует сборщик мусора, освобождающий память от ненужных объектов. Но представим себе необычную ситуацию. Предположим, что объект выделяет «специальную» память без использования оператора new. Сборщик мусора умеет освобождать память, выделенную new, но ему неизвестно, как следует очищать специфическую память объекта.
Для таких ситуаций в Java предусмотрен метод finalize(), который вы можете определить в вашем классе. Вот как он должен работать: когда сборщик мусора готов освободить память, использованную вашим объектом, он для начала вызывает метод finalize(). Hо только после этого освобождает занимаемую объектом память. Таким образом, метод finalize() позволяет выполнять завершающие действия во время работы сборщика мусора.
Все это может создать немало проблем для программистов, особенно для программистов на языке C++, так как они могут спутать метод finalize() с деструктором языка C++ — функцией, всегда вызываемой перед разрушением объекта. Но здесь очень важно понять разницу между Java и C++, поскольку в C++ объекты разрушаются всегда (в правильно написанной программе), в то время как в Java объекты удаляются сборщиком мусора не во всех случаях. Другими словами:
· 1. Ваши объекты могут быть и не переданы сборщику мусора.
· 2. Сборка мусора не является удалением.
Если программа завершает свою работу и сборщик мусора не удалил ни одного объекта и не освободил занимаемую память, то эта память будет возвращена операционной системе после завершения работы программы. Это хорошо, так как сборка мусора сопровождается весомыми издержками, и если сборщик не используется, то, соответственно, эти издержки не проявляются.