Брюс Эккель - Философия Java3
Почему «восходящее преобразование»?
Термин возник по историческим причинам: традиционно на диаграммах наследования корень иерархии изображался у верхнего края страницы, а диаграмма разрасталась к нижнему краю страницы. (Конечно, вы можете рисовать свои диаграммы так, как сочтете нужным.) Для файла 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("fd2M). 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 в классе может быть разным для каждого объекта, и при этом оно сохраняет свою неизменность. Пример:
//: c06:BlankFinal .java // "Пустые" неизменные поля.
class Poppet {
private int i:
Poppet(int ii) { i = ii; }
}
public class BlankFinal {
private final int i = 0; // Инициализированная константа private final int j; // Пустая константа private final Poppet p; // Пустая константа-ссылка II Пустые константы НЕОБХОДИМО инициализировать // в конструкторе: public BlankFinalО {
j = 1; // Инициализация пустой константы р = new Poppet(l); // Инициализация пустой неизменной ссылки
}
public BlankFinal(int х) {
j = х; // Инициализация пустой константы р = new Poppet(x), // Инициализация пустой неизменной ссылки
}
public static void main(String[] args) { new BlankFinal О; new BlankFinal(47),
}
} ///:-
Значения неизменных (final) переменных обязательно должны присваиваться или в выражении, записываемом в точке определения переменной, или в каждом из конструкторов класса. Тем самым гарантируется инициализация полей, объявленных как final, перед их использованием.