32

32


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).
Почему «восходящее преобразование»?
Термин возник по историческим причинам: традиционно на диаграммах насле­дования корень иерархии изображался у верхнего края страницы, а диаграмма разрасталась к нижнему краю страницы. (Конечно, вы можете рисовать свои диаграммы так, как сочтете нужным.) Для файла Wind.java диаграмма наследо­вания выглядит так:



Преобразование от производного типа к базовому требует движения вверх по диаграмме, поэтому часто называетсявосходящимпреобразованием. Восхо­дящее преобразование всегда безопасно, так как это переход от конкретного типа к более общему типу. Иначе говоря, производный класс является над­стройкой базового класса. Он может содержать больше методов, чем базовый класс, нообязанвключать в себя все методы базового класса. Единственное, что может произойти с интерфейсом класса при восходящем преобразовании, — по­теря методов, но никак не их приобретение. Именно поэтому компилятор все­гда разрешает выполнять восходящее преобразование, не требуя явных преоб­разований или других специальных обозначений.
Преобразование также может выполняться и в обратном направлении — так называемоенисходящее преобразование(downcasting). Но при этом возникает проблема, которая рассматривается в главе 1.1.
Снова о композиции с наследованием
В объектно-ориентированном программировании разработчик обычно упако­вывает данные вместе с методами в классе, а затем работает с объектами этого класса. Существующие классы также используются для создания новых клас­сов посредством композиции. Наследование на практике применяется реже. Поэтому, хотя во время изучения ООП наследованию уделяется очень много внимания, это не значит, что его следует без разбора применять всюду, где это возможно. Наоборот, пользоваться им следует осмотрительно — только там, где полезность наследования не вызывает сомнений. Один из хороших критериев выбора между композицией и наследованием — спросить себя, собираеесь ли вы впоследствии проводить восходящее преобразование от производного клас­са к базовому классу. Если восходящее преобразование актуально, выбирайте наследование, а если нет — подумайте, нельзя ли поступить иначе.
Ключевое слово final
В Java смысл ключевого слова final зависит от контекста, но в основном оно оз­начает:«Этонельзя изменить». Запрет на изменения может объясняться двумя причинами: архитектурой программы или эффективностью. Эти две причины основательно различаются, поэтому в программе возможно неверное употреб­ление ключевого слова final.
В следующих разделах обсуждаются три возможных применения final: для данных, методов и классов.
Неизменные данные
Во многих языках программирования существует тот или иной способ сказать компилятору, что частица данных является «константой». Константы полезны в двух ситуациях:
константа времени компиляции, которая никогда не меняется;
•        значение, инициализируемое во время работы программы, которое нель­зя изменять.
Компилятор подставляет значение константы времени компиляции во все выражения, где оно используется; таким образом предотвращаются некоторые издержки выполнения. В Java подобные константы должны относиться к при­митивным типам, а для их определения используется ключевое слово final. Зна­чение такой константы присваивается во время определения.
Поле, одновременно объявленное с ключевыми словами static и final, суще­ствует в памяти в единственном экземпляре и не может быть изменено.
При использовании слова final со ссылками на объекты его смысл не столь очевиден. Для примитивов final делает постояннымзначение, но для ссылки на объект постоянной становитсяссылка.После того как такая ссылка будет связана с объектом, она уже не сможет указывать на другой объект. Впрочем, сам объект при этом может изменяться; в Java нет механизмов, позволяющих сделать произвольный объект неизменным. (Впрочем, вы сами можете напи­сать ваш класс так, чтобы его объекты факически были константными.) Данное ограничение относится и к массивам, которые тоже являются объектами.
Следующий пример демонстрирует использование final для полей классов:
// reusing/Final Data java
// Действие ключевого слова final для полей.
import java util *,
import static net mindview.util Print.*;
class Value {
int i. // доступ в пределах пакета public Value(int i) { this i = i, }
}
public class FinalData {
private static Random rand = new Random(47); private String id,
public FinalData(String id) { this.id = id: } // Могут быть константами времени компиляции, private final int valueOne = 9. private static final int VALUE_TW0 = 99, // Типичная открытая константа, public static final int VALUE_THREE = 39: // He может быть константой времени компиляции: private final int i4 = rand.nextlnt(20); static final int INT_5 = rand nextlnt(20); private Value vl = new Value(ll): private final Value v2 = new Value(22), private static final Value VAL_3 = new Value(33): // Массивы.
private final int[] a = { 1. 2, 3. 4, 5, 6 }: public String toStringO {
return id + ": " + "i4 = " + i4 + ". INT_5 = " + INT_5;
}
public static void main(String[] args) {
FinalData fdl = new FinalDataCfdl"),
//! fdl.valueOne++, // Ошибка значение нельзя изменить
fdl.v2 i++, // Объект не является неизменным!
fdl.vl = new Value(9); // OK - не является неизменным
for(int i = 0, i < fdl.a.length; i++)
fdl a[i]++; // Объект не является неизменным! //! fdl v2 = new Value(O); // Ошибка: ссылку //! fdl VAL_3 = new Valued); // нельзя изменить //' fdl a = new int[3], print(fdl),
print("Создаем FinalData"); FinalData fd2 = new FinalData("fd2
M
). print(fdl), print(fd2):
}
} /* Output.
fdl i4 = 15. INT 5 = 18
Лпродолжение &Создаем Final Data fdl: i4 = 15. INT_5 = 18 fd2: i4 = 13. INT_5 = 18 *///:-
Так как valueOne и VALUE_TWO являются примитивными типами со значениями, заданными на стадии компиляции, они оба могут использоваться в качестве констант времени компиляции, и принципиальных различий между ними нет. Константа VALUE_THREE демонстрирует общепринятый способ определения по­добных полей: спецификатор public открывает к ней доступ за пределами пакета; ключевое слово static указывает, что она существует в единственном числе, а ключевое слово final указывает, что ее значение остается неизменным. Заметь­те, что примитивы final static с неизменными начальными значениями (то есть константы времени компиляции) записываются целиком заглавными буквами, а слова разделяются подчеркиванием (эта схема записи констант позаимствова­на из языка С).
Само по себе присутствие final еще не означает, что значение переменной из­вестно уже на стадии компиляции. Данный факт продемонстрирован на приме­ре инициализации i4 и INT_5 с использованием случайных чисел. Эта часть про­граммы также показывает разницу между статическими и нестатическими константами. Она проявляется только при инициализации во время исполне­ния, так как все величины времени компиляции обрабатываются компилято­ром одинаково (и обычно просто устраняются с целью оптимизации). Различие проявляется в результатах запуска программы. Заметьте, что значения поля i4 для объектов fdl и fd2 уникальны, но значение поля INT_5 не изменяется при создании второго объекта FinalData. Дело в том, что поле INT_5 объявлено как static, поэтому оно инициализируется только один раз во время загрузки класса.
Переменные от vl до VAL_3 поясняют смысл объявления ссылок с ключевым словом final. Как видно из метода main(), объявление ссылки v2 как final еще не означает, что ее объект неизменен. Однако присоединить ссылку v2 к новому объекту не получится, как раз из-за того, что она была объявлена как final. Именно такой смысл имеет ключевое слово final по отношению к ссылкам. Вы также можете убедиться, что это верно и для массивов, которые являются про­сто другой разновидностью ссылки. Пожалуй, для ссылок ключевое слово final обладает меньшей практической ценностью, чем для примитивов.
Пустые константы
В Java разрешается создаватьпустые константы —поля, объявленные как final, которым, однако, не было присвоено начальное значение. Во всех случаях пустую константуобязательнонужно инициализировать перед использовани­ем, и компилятор следит за этим. Впрочем, пустые константы расширяют сво­боду действий при использовании ключевого слова final, так как, например, поле final в классе может быть разным для каждого объекта, и при этом оно со­храняет свою неизменность. Пример:

Report Page