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

В классе Soup2 использован другой подход — в программе всегда создается не более одного объекта этого класса. Объект Soup2 создается как статическая приватная переменная, пэтому он всегда существует только в одном экземпляре и его невозможно получить без вызова открытого метода access().

Резюме

В любых отношениях важно установить ограничения, которые соблюдаются всеми сторонами. При создании библиотеки вы устанавливаете отношения с пользователем библиотеки (программистом-клиентом), который создает про­граммы или библиотеки более высокого уровня с использованием ваших биб­лиотек.

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

В этой главе рассматривается процесс построения библиотек из классов; во-первых, механизм группировки классов внутри библиотеки и, во-вторых, механизм управления доступом к членам класса.

По оценкам проекты на языке С начинают «рассыпаться» примерно тогда, когда код достигает объема от 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 можно вывес­ти без выдачи исключения.