29
В этой главе рассматривается процесс построения библиотек из классов; во-первых, механизм группировки классов внутри библиотеки и, во-вторых, механизм управления доступом к членам класса.
По оценкам проекты на языке С начинают «рассыпаться» примерно тогда, когда код достигает объема от 50 до 100 Кбайт, так как С имеет единое «пространство имен»; в системе возникают конфликты имен, создающие массу неудобств. В Java ключевое слово package, схема именования пакетов и ключевое слово import обеспечивают полный контроль над именами, так что конфликта имен можно легко избежать.
Существует две причины для ограничения доступа к членам класса. Первая — предотвращение использования клиентами внутренней реализации класса, не входящей во внешний интерфейс. Объявление полей и методов со спецификатором private только помогает пользователям класса, так как они сразу видят, какие члены класса для них важны, а какие можно игнорировать. Все это упрощает понимание и использование класса.
Вторая, более важная причина для ограничения доступа — возможность изменения внутренней реализации класса, не затрагивающего программистов- клиентов. Например, сначала вы реализуете класс одним способом, а затем выясняется, что реструктуризация кода позволит повысить скорость работы. Отделение интерфейса от реализации позволит сделать это без нарушения работоспособности существующего пользовательского кода, в котором этот класс используется.
Открытый интерфейс класса — это то, что фактическивидитего пользователь, поэтому очень важно «довести до ума» именно эту, самую важную, часть класса в процессе анализа и разработки. И даже при этом у вас остается относительная свобода действий. Даже если идеальный интерфейс не удалось построить с первого раза, вы можетедобавитьв него новые методы — без удаления уже существующих методов, которые могут использоваться программистами- клиентами.
[1]
Использовать Java-интерпретатор не обязательно. Существует несколько компиляторов, создающих единый исполняемый файл.
[2]
На самом деле доступ private или protected могут иметьвнутренние классы,но это особый случай (см. главу 8).
Повторное использование классов
Возможность повторного использования кода принадлежит к числу важнейших преимуществ Java. Впрочем, по-настоящему масштабные изменения отнюдь не сводятся к обычному копированию и правке кода.
Повторное использование на базе копирования кода характерно для процедурных языков, подобных С, но оно работало не очень хорошо. Решение этой проблемы в Java, как и многое другое, строится на концепции класса. Вместо того чтобы создавать новый класс «с чистого листа», вы берете за основу уже существующий класс, который кто-то уже создал и проверил на работоспособность.
Хитрость состоит в том, чтобы использовать классы без ущерба для существующего кода. В этой главе рассматриваются два пути реализации этой идеи. Первый довольно прямолинеен: объекты уже имеющихся классов просто создаются внутри вашего нового класса. Механизм построения новрго класса из объектов существующих классов называетсякомпозицией(composition). Вы просто используете функциональность готового кода, а не его структуру.
Второй способ гораздо интереснее. Новый класс создаетсякак специализацияуже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса. Этот механизм называетсянаследованием(inheritance), и большую часть работы в нем совершает компилятор. Наследование является одним из «краеугольных камней» объект- но-ориентированного программирования; некоторые из его дополнительных применений описаны в главе 8.
уиСинтаксис и поведение типов при использовании композиции и наследования нередко совпадают (что вполне логично, так как оба механизма предназначены для построения новых типов на базе уже существующих). В этой главе рассматриваются оба механизма повторного использования кода.
Синтаксис композиции
До этого момента мы уже довольно часто использовали композицию — ссылка на внедряемый объект просто включается в новый класс. Допустим, вам понадобился объект, содержащий несколько объектов String, пару полей примитивного типа и объект еще одного класса. Для не-примитивных объектов в новый класс включаются ссылки, а примитивы определяются сразу:
// reusing/SprinklerSystem java
// Композиция для повторного использования кода.
class WaterSource { private String s, WaterSourceO {
System out println( "WaterSourceO"); s = "сконструирован";
}
public String toStringO { return s; }
}
public class SprinklerSystem {
private String valvel. valve2, valve3, valve4, private WaterSource source = new WaterSourceO; private int i. private float f, public String toStringO { return
"valvel = " + valvel + " " + • "valve2 = " + valve2 + " " + "valve3 = " + valve3 + " " +
"valve4 = " + valve4 + "\n" +
••-j = - + -j + ■■ •• + -f = •• + f + •• " +
"source = " + source,
}
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem(), System out println(sprinklers);
}
} /* Output- WaterSourceO
valvel = null valve2 = null valve3 = null valve4 = null i = 0 f = 0.0 source = сконструирован *///•-
В обоих классах определяется особый метод toString(). Позже вы узнаете, что каждый не-примитивный объект имеет метод toString(), который вызывается в специальных случаях, когда компилятор располагает не объектом, а хочет получить его строковое представление в формате String. Поэтому в выражении из метода S р ri n klerSyste m.toStri n g ():
"source = " + source;
компилятор видит, что к строке "source = " «прибавляется» объект класса WaterSource. Компилятор не может это сделать, поскольку к строке можно «добавить» только такую же строку, поэтому он преобразует объект source в String, вызывая метод toString(). После этого компилятор уже в состоянии соединить две строки и передать результат в метод System.out.println() (или статическим методам print() и printnb(), используемым в книге). Чтобы подобное поведение поддерживалось вашим классом, достаточно включить в него метод toString().
Примитивные типы, определенные в качестве полей класса, автоматически инициализируются нулевыми значениями, как упоминалось в главе 2. Однако ссылки на объекты заполняются значениями null, и при попытке вызова метода по такой ссылке произойдет исключение. К счастью, ссылку null можно вывести без выдачи исключения.
Компилятор не создает объекты для ссылок «по умолчанию», и это логично, потому что во многих случаях это привело бы к лишним затратам ресурсов. Если вам понадобится проинициализировать ссылку, сделайте это самостоятельно:
• в точке определения объекта. Это значит, что объект всегда будет инициализироваться перед вызовом конструктора;
• в конструкторе данного класса;
• непосредственно перед использованием объекта. Этот способ часто называютотложенной инициализацией.Он может сэкономить вам ресурсы в ситуациях, где создавать объект каждый раз необязательно и накладно;
• с использованием инициализации экземпляров.
В следующем примере продемонстрированы все четыре способа:
//: reusing/Bath.java
// Инициализация в конструкторе с композицией.
import static net.mindview.util.Print.*:
class Soap {
private String s: SoapO {
printCSoapO"); s = "Constructed";
}
public String toStringO { return s: }
}
public class Bath {
private String // Инициализация в точке определения- si = "Счастливый", s2 = "Счастливый", s3. s4. private Soap castille; private int i; private float toy; public BathO {
print( В конструкторе BathO"), s3 = "Радостный"; toy = 3.14f;
ч
castille = new SoapO;
}
// Инициализация экземпляра-
{ i = 47; }
public String toStringO {
if(s4 == null) // Отложенная инициализация- s4 = "Радостный";
return
"si = " + si + "\n" + "s2 = " + s2 + "\n" + "s3 = " + s3 + "\n" + "s4 = " + s4 + "\n" +
H
i = " + i + "\n" + "toy = " + toy + "\n" + "castille = " + castille;
}
public static void main(String[] args) { Bath b = new Bath О; print(b);
}
} /* Output; В конструкторе Bath О SoapO
si = Счастливый s2 = Счастливый s3 = Радостный s4 = Радостный i = 47 toy = 3 14
castille = Сконструирован *///;-
Заметьте, что в конструкторе класса Bath команда выполняется до проведения какой-либо инициализации. Если инициализация в точке определения не выполняется, нет никаких гарантий того, что она будет выполнена перед отправкой сообщения по ссылке объекта — кроме неизбежных исключений времени выполнения.
При вызове метода toStringO в нем присваивается значение ссылке s4, чтобы все поля были должным образом инициализированы к моменту их использования.
Синтаксис наследования
Наследование является неотъемлемой частью Java (и любого другого языка ООП). Фактически оно всегда используется при создании класса, потому что, даже если класс не объявляется производным от другого класса, он автоматически становится производным от корневого класса Java Object.
Синтаксис композиции очевиден, но для наследования существует совершенно другая форма записи. При использовании наследования вы фактически говорите: «Этот новый класс похож на тот старый класс». В программе этот факт выражается перед фигурной скобкой, открывающей тело класса: сначала записывается ключевое слово extends, а затем имябазового(base)класса.Тем самым вы автоматически получаете доступ ко всем полям и методам базового класса. Пример:
//. reusing/Detergent.java
// Синтаксис наследования и его свойства
import static net mindview util Print.*.
class Cleanser {'
private String s = "Cleanser", public void append(String a) { s += a; } public void diluteO { append( dilutee)"), } public void applyO { appendC applyO"); } public void scrubO { appendC scrubO"): } public String toStringO { return s. } public static void main(String[] args) { Cleanser x = new CleanserO, x diluteO: x applyO, x scrubO; print(x);
}
}
public class Detergent extends Cleanser { II Изменяем метод- public void scrubO {
appendC' Detergent.scrubO").
super scrubO, // Вызываем метод базового класса
}
11 Добавляем новые методы к интерфейсу public void foamO { appendC foamO"), } // Проверяем новый класс, public static void main(String[] args) { Detergent x = new DetergentO, x.diluteO, x.applyO, x scrubO; x. foamO; print(x);
print("Проверяем базовый класс"); Cleanser main(args);
}
} /* Output
Cleanser diluteO applyO Detergent.scrub() scrubO foamO Проверяем базовый класс Cleanser diluteO applyO scrubO */// ~
Пример демонстрирует сразу несколько особенностей наследования. Во-первых, в методе класса Cleanser append() новые строки присоединяются к строке s оператором += — одним из операторов, специально «перегруженных» создателями Java для строк (String).
Во-вторых, как Cleanser, так и Detergent содержат метод main(). Вы можете определить метод main() в каждом из своих классов; это позволяет встраивать тестовый код прямо в класс. Метод main() даже не обязательно удалять после завершения тестирования, его вполне можно оставить на будущее.