Выбрать главу

//• 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).