57

57


}

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

Почему «восходящее преобразование»?

Термин возник по историческим причинам: традиционно на диаграммах насле­дования корень иерархии изображался у верхнего края страницы, а диаграмма разрасталась к нижнему краю страницы. (Конечно, вы можете рисовать свои диаграммы так, как сочтете нужным.) Для файла Wind.java диаграмма наследо­вания выглядит так:

Преобразование от производного типа к базовому требует движения вверх по диаграмме, поэтому часто называетсявосходящимпреобразованием. Восхо­дящее преобразование всегда безопасно, так как это переход от конкретного типа к более общему типу. Иначе говоря, производный класс является над­стройкой базового класса. Он может содержать больше методов, чем базовый класс, нообязанвключать в себя все методы базового класса. Единственное, что может произойти с интерфейсом класса при восходящем преобразовании, — по­теря методов, но никак не их приобретение. Именно поэтому компилятор все­гда разрешает выполнять восходящее преобразование, не требуя явных преоб­разований или других специальных обозначений.

Преобразование также может выполняться и в обратном направлении — так называемоенисходящее преобразование(downcasting). Но при этом возникает проблема, которая рассматривается в главе 1.1.

Снова о композиции с наследованием

В объектно-ориентированном программировании разработчик обычно упако­вывает данные вместе с методами в классе, а затем работает с объектами этого класса. Существующие классы также используются для создания новых клас­сов посредством композиции. Наследование на практике применяется реже. Поэтому, хотя во время изучения ООП наследованию уделяется очень много внимания, это не значит, что его следует без разбора применять всюду, где это возможно. Наоборот, пользоваться им следует осмотрительно — только там, где полезность наследования не вызывает сомнений. Один из хороших критериев выбора между композицией и наследованием — спросить себя, собираеесь ли вы впоследствии проводить восходящее преобразование от производного клас­са к базовому классу. Если восходящее преобразование актуально, выбирайте наследование, а если нет — подумайте, нельзя ли поступить иначе.

Ключевое слово final

В Java смысл ключевого слова final зависит от контекста, но в основном оно оз­начает:«Этонельзя изменить». Запрет на изменения может объясняться двумя причинами: архитектурой программы или эффективностью. Эти две причины основательно различаются, поэтому в программе возможно неверное употреб­ление ключевого слова final.

В следующих разделах обсуждаются три возможных применения final: для данных, методов и классов.

Неизменные данные

Во многих языках программирования существует тот или иной способ сказать компилятору, что частица данных является «константой». Константы полезны в двух ситуациях:

константа времени компиляции, которая никогда не меняется;

•        значение, инициализируемое во время работы программы, которое нель­зя изменять.

Компилятор подставляет значение константы времени компиляции во все выражения, где оно используется; таким образом предотвращаются некоторые издержки выполнения. В Java подобные константы должны относиться к при­митивным типам, а для их определения используется ключевое слово final. Зна­чение такой константы присваивается во время определения.

Поле, одновременно объявленное с ключевыми словами static и final, суще­ствует в памяти в единственном экземпляре и не может быть изменено.