Брюс Эккель - Философия Java3
Также обратите особое внимание на порядок вызова завершающих методов для базового класса и объектов-членов в том случае, если они зависят друг от друга. В основном нужно следовать тому же принципу, что использует компилятор С++ при вызове деструкторов: сначала провести завершающие действия для вашего класса в последовательности, обратной порядку их создания. (Обычно для этого требуется, чтобы элементы базовых классов продолжали существовать.) Затем вызываются завершающие методы из базовых классов, как и показано в программе.
Во многих случаях завершающие действия не являются проблемой; достаточно дать сборщику мусора выполнить свою работу. Но уж если понадобилось провести их явно, сделайте это со всей возможной тщательностью и вниманием, так как в процессе сборки мусора трудно в чем-либо быть уверенным. Сборщик мусора вообще может не вызываться, а если он начнет работать, то объекты будут уничтожаться в произвольном порядке. Лучше не полагаться на сборщик мусора в ситуациях, где дело не касается освобождения памяти. Если вы хотите провести завершающие действия, создайте для этой цели свой собственный метод и не полагайтесь на метод finalize().
Сокрытие имен
Если какой-либо из методов базового класса Java был перегружен несколько раз, переопределение имени этого метода в производном классе не скроет ни одну из базовых версий (в отличие от С++). Поэтому перегрузка работает вне зависимости от того, где был определен метод — на текущем уровне или в базовом классе:
//: reusing/Hide java
// Перегрузка имени метода из базового класса
// в производном классе не скроет базовую версию метода.
import static net.mindview.util.Print.*:
class Milhouse {}
class Bart extends Homer { void doh(Milhouse m) {
print("doh(Milhouse)");
}
}
public class Hide {
public static void main(String[] args) { Bart b = new BartO; b doh(l); b doh('x'); b.doh(l.Of); b doh(new MilhouseO);
}
} /* Output. doh(float) doh(char) doh(float) doh(Milhouse) *///:-
Мы видим, что все перегруженные методы класса Homer доступны классу Bart, хотя класс Bart и добавляет новый перегруженный метод (в С++ такое действие спрятало бы все методы базового класса). Как вы увидите в следующей главе, на практике при переопределении методов гораздо чаще используется точно такое же описание и список аргументов, как и в базовом классе. Иначе легко можно запутаться (и поэтому С++ запрещает это, чтобы предотвратить совершение возможной ошибки).
В Java SE5 появилась запись @0verride; она не является ключевым словом, но может использоваться так, как если бы была им. Если вы собираетесь переопределить метод, используйте @0verride, и компилятор выдаст сообщение об ошибке, если вместо переопределения будет случайно выполнена перегрузка:
//• reusing/Lisa java // {CompileTimeError} (Won't compile)
class Lisa extends Homer {
(^Override void doh(Milhouse m) {
System out println("doh(Milhouse)");
}
class Homer {
char doh(char с) {
print("doh(char)"); return 'd';
}
float doh(float f) {
print("doh(float)"); return l.Of,
Композиция в сравнении с наследованием
И композиция, и наследование позволяют вам помещать подобъекты внутрь вашего нового класса (при композиции это происходит явно, а в наследовании — опосредованно). Вы можете поинтересоваться, в чем между ними разница и когда следует выбирать одно, а когда — другое.
Композиция в основном применяется, когда в новом классе необходимо использовать функциональность уже существующего класса, но не его интерфейс. То есть вы встраиваете объект, чтобы использовать его возможности в новом классе, а пользователь класса видит определенный вами интерфейс, но не замечает встроенных объектов. Для этого внедряемые объекты объявляются со спецификатором private.
Иногда требуется предоставить пользователю прямой доступ к композиции вашего класса, то есть сделать встроенный объект открытым (public). Встроенные объекты и сами используют сокрытие реализации, поэтому открытый доступ безопасен. Когда пользователь знает, что класс собирается из составных частей, ему значительно легче понять его интерфейс. Хорошим примером служит объект Саг (машина):
// reusing/Car.java
// Композиция с использованием открытых объектов
// двигатель
class Engine {
public void startO {} // запустить public void rev() {} // переключить public void stopO {} // остановить
}
// колесо
class Wheel {
public void inflate(int psi) {} // накачать
}
// окно
class Window {
public void rollupO {} // поднять public void rolldownO {} // опустить
}
// дверь
class Door {
public Window window = new WindowO; // окно двери public void openО {} // открыть public void closeO {} // закрыть
}
// машина
public class Car {
public Engine engine = new EngineO; public Wheel[] wheel = new Wheel[4], public Door
left = new DoorO,
right = new DoorO: // двухдверная машина
public CarO {
for (int i =0; i <4; i++)
wheel[i] = new Wheel О;
}
public static void main(String[] args) { Car car = new CarO; car 1 eft.window.rollup(); car.wheel[0].inflate(72);
}
} /// -
Так как композиция объекта является частью проведенного анализа задачи (а не просто частью реализации класса), объявление членов класса открытыми (public) помогает программисту-клиенту понять, как использовать класс, и облегчает создателю класса написание кода. Однако нужно все-таки помнить, что описанный случай является специфическим и в основном поля класса следует объявлять как private.
При использовании наследования вы берете уже существующий класс и создаете его специализированную версию. В основном это значит, что класс общего назначения адаптируется для конкретной задачи. Если чуть-чуть подумать, то вы поймете, что не имело бы смысла использовать композицию машины и средства передвижения — машина не содержит средства передвижения, она сама есть это средство. Взаимосвязь «является» выражается наследованием, а взаимосвязь «имеет» описывается композицией.
protected
После знакомства с наследованием ключевое слово protected наконец-то обрело смысл. В идеале закрытых членов private должно было быть достаточно. В реальности существуют ситуации, когда вам необходимо спрятать что-либо от окружающего мира, тем не менее оставив доступ для производных классов.
Ключевое слово protected — дань прагматизму. Оно означает: «Член класса является закрытым (private) для пользователя класса, но для всех, кто наследует от класса, и для соседей по пакету он доступен». (В Java protected автоматически предоставляет доступ в пределах пакета.)
Лучше всего, конечно, объявлять поля класса как private — всегда стоит оставить за собою право изменять лежащую в основе реализацию. Управляемый доступ наследникам класса предоставляется через методы protected:
// reusing/Ore java
// Ключевое слово protected
import static net.mindview.util.Print.*;
class Villain {
private String name;
protected void set(String nm) { name = nm; } public Villain(String name) { this.name = name, } public String toStringO {
return "Я объект Villain и мое имя " + name;
}
public class Ore extends Villain { private int orcNumber, public Orc(String name, int orcNumber) { super(name);
this.orcNumber = orcNumber;
}
public void change(String name, int orcNumber) {
set(name); // Доступно, так как объявлено protected this.orcNumber = orcNumber;
}
public String toStringO {
return "Ore " + orcNumber + ": " + super.toString().
}
public static void main(String[] args) {
Ore ore = new ОгсС'Лимбургер". 12); print(orc);
огс.сЬапдеСБоб"* 19); print(orc);
}
} /* Output:
Ore 12: Я объект Villain и мое имя Лимбургер
Ore 19: Я объект Villain и мое имя Боб
*///-
Как видите, метод change() имеет доступ к методу set(), поскольку тот объявлен как protected. Также обратите внимание, что метод toString() класса Ore определяется с использованием версии этого метода из базового класса.
Восходящее преобразование типов
Самая важная особенность наследования заключается вовсе не в том, что оно предоставляет методы для нового класса, — наследование выражает отношения между новым и базовым классом. Ее можно выразить .следующим образом: «Новый класс имеет тип существующего класса».
Данная формулировка — не просто причудливый способ описания наследования, она напрямую поддерживается языком. В качестве примера рассмотрим базовый класс с именем Instrument для представления музыкальных инструментов и его производный класс Wind. Так как наследование означает, что все методы базового класса также доступны в производном классе, любое сообщение, которое вы в состоянии отправить базовому классу, можно отправить и производному классу. Если в классе Instrument имеется метод play(), то он будет присутствовать и в классе Wind. Таким образом, мы можем со всей определенностью утверждать, что объекты Wind также имеют тип Instrument. Следующий пример показывает, как компилятор поддерживает такое понятие:
//: reusing/Wind.java
// Наследование и восходящее преобразование.
class Instrument {
public void playO {} static void tune(Instrument i) { // ...
i .playO:
}
}
// Объекты Wind также являются объектами Instrument, // поскольку они имеют тот же интерфейс: public class Wind extends Instrument {
public static void main(String[] args) { Wind flute = new WindО.
Instrument.tune(flute); // Восходящее преобразование
}
} ///:-
Наибольший интерес в этом примере представляет метод tune(), получающий ссылку на объект Instrument. Однако в методе Wind.main() методу tune() передается ссылка на объект Wind. С учетом всего, что говорилось о строгой проверке типов в Java, кажется странным, что метод с готовностью берет один тип вместо другого. Но стоит вспомнить, что объект Wind также является объектом Instrument, и не существует метода, который можно вызвать в методе tune() для объектов Instrument, но нельзя для объектов Wind. В методе tune() код работает для Instrument и любых объектов, производных от Instrument, а преобразование ссылки на объект Wind в ссылку на объект Instrument называется восходящим преобразованием типов (upcasting).