Thinking in Java, 2nd edition, Revision 11

©2000 by Bruce Eckel

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]

5: Скрытие реализации

Основным обсуждением в объектно-ориентированном программировании является “отделение вещей, которые меняются, от тех, которые не меняются.”

Это очень важно для библиотек. Пользователь (клиентский программист) этой библиотеки должен полагаться на ту часть библиотеки, которую он использует, и знать, что ему не нужно будет снова переписывать код, как только выйдет новая версия этой библиотеки. С другой стороны, создатель библиотеки должен иметь свободу модификаций и расширений, и быть уверенным, что эти изменения не повлияют на работу кода клиентского программиста.

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

Для решения этой проблемы, Java предоставляет спецификаторы доступа для того, чтобы создатель библиотеки мог сказать что доступно клиентскому программисту, а что нет. Уровни контроля доступа от “полного” до “минимального” определяются с помощью ключевых слов: публичный - public, защищенный - protected, дружественный - “friendly” (не имеет ключевого слова) и приватный - private. Из предыдущего параграфа Вы можете посчитать, что как разработчик библиотеки, Вы будете хранить все, что возможно как “private”, и раскрывать только те методы, которые Вы хотите предоставить клиентскому программисту. Это абсолютно верно, хотя это бывает трудно понимать людям, программирующим на других языках (особенно на C), которые имеют доступ ко всему, без ограничений. К концу этой главы Вы поймете, насколько большое значение имеет контроль доступа в Java.

Однако, концепция библиотеки компонент и контроля доступа к ним это еще не все. Существует вопрос - как хранить вместе связанные компоненты в модуле библиотеки. В Java это реализуется с помощью ключевого слова package (пакет), и спецификаторы доступа действуют в зависимости от того, находится ли класс в том же пакете или нет. Итак, в начале этой главы Вы узнаете, как размещать компоненты библиотеки в пакетах. А затем, Вы сможете понять значение спецификаторов доступа.

package: модуль библиотеки

Пакет это что Вы используете, когда пишете ключевое слово import для подключения целой библиотеки, такой как

import java.util.*;

Это включает в программу библиотеку утилит, которая является частью стандартной поставки Java. Например, класс ArrayList находится в java.util, и Вы можете также указать полное имя java.util.ArrayList (которое Вы можете использовать без выражения import), либо просто написать ArrayList (при использовании import).

Если Вы хотите включить единичный класс, Вы можете указать этот класс в выражении import

import java.util.ArrayList;

После этого, Вы можете использовать ArrayList без ограничений. Однако, никакие другие классы из пакета java.util не будут доступны.

Использование импорта обусловлено необходимостью управления “пространством имен.” Имена всех членов класса изолированы друг от друга. Метод f( ) внутри класса A не будет конфликтовать с методом f( ) которой имеет такую же сигнатуру (список аргументов) в классе B. А что же насчет имен классов? Представьте, что Вы создаете класс stack и устанавливаете на машине, на которой уже есть класс stack, написанный кем-то другим? С Java в интернете такое вполне может произойти, и Вы об этом можете не узнать, т.к. классы часто загружаются автоматически в процессе запуска Java-приложения.

Из-за появления возможных конфликтов важно иметь полный контроль над пространством имен в Java, а также, иметь возможность создавать абсолютно уникальные имена.

До сих пор, большинство примеров в этой книге существовали в единичном файле и проектировались для локального использования, без упоминания о пакетах. (В этом случае класс располагался в “пакете по умолчанию.”) Ради упрощения такой подход будет использоваться, где это возможно, в оставшейся части книги. Однако, если Вы планируете создавать библиотеки и программы которые будут дружественными для других программ на Java на той же машине, Вам нужно будет подумать о предотвращении конфликтов с именами классов.

Когда Вы создаете файл исходного текста в Java, он обычно называется модулем компиляции (иногда модулем трансляции). Каждый модуль компиляции должен иметь расширение .java, и внутри него может быть расположен публичный класс, который должен иметь имя такое же, как имя файла (учитывая регистры, но без расширения .java). В каждом модуле компиляции может быть только один публичный класс, в противном случае, компилятор будет недоволен. Остальные классы в этом модуле компиляции, если они есть, скрыты от мира за пределами этого пакета, т.к. они не публичные, и представляют классы “поддержки” для главного публичного класса.

Когда Вы компилируете файл .java Вы получаете выходной файл с точно таким же именем и расширением .class для каждого класса в файле .java. Таким образом, из нескольких .java файлов Вы получаете несколько .class файлов. Если Вы работали с компилирующими языками, то Вы, возможно, получали от компилятора выходные файлы (обычно это “obj” файлы), которые, затем, объединялись вместе с другими файлами такого же типа с помощью линкера (для создания исполняемого файла) либо генератора библиотеки (для создания библиотеки). Но Java работает не так. Работающая программа это набор .class файлов, которые могут быть собраны в пакет и запакованы в JAR файл (с помощью Java архиватора jar). А интерпретатор Java способен находить, загружать и интерпретировать эти файлы[32].

Библиотека это также набор .class файлов. Каждый файл содержит один публичный класс (Вас не заставляют иметь публичный класс, но это типичная ситуация), так что для каждого файла есть один компонент. Если Вы хотите чтобы все эти компоненты хранились вместе (из различных .java и .class файлов), Вы используете ключевое слово package.

Когда Вы пишите:

package mypackage;

в начале файла (если Вы используете выражение package, перед ним могут быть только комментарии), этим Вы указываете, что этот модуль компиляции является частью библиотеки с названием mypackage. Или, другими словами, Вы говорите, что публичный класс внутри этого модуля компиляции скрыт под именем mypackage, и если кто-то захочет использовать этот класс он должен либо указать имя пакета, либо использовать ключевое слово import вместе с mypackage (используя варианты, показанные ранее). Заметьте, что в Java есть соглашение для имен пакетов, это - использование символов только нижнего регистра, даже для внутренних слов.

Например, предположим, что имя файла - MyClass.java. Это значит, что может быть только один публичный класс в этом файле, и имя этого класса должно быть - MyClass (включая регистры):

package mypackage;
public class MyClass {
  // . . .

Теперь, если кто-то хочет использовать класс MyClass или любой другой публичный класс из пакета mypackage, ему нужно будет использовать ключевое слово import чтобы сделать доступными имена из пакета mypackage. Существует также альтернатива - использование имен с префиксами:

mypackage.MyClass m = new mypackage.MyClass();

А ключевое слово import может это упростить:

import mypackage.*;
// . . . 
MyClass m = new MyClass();

Это стоит запомнить, т.к. с помощью ключевых слов package и import, Вы можете, как разработчик библиотеки, разделять глобальное пространство имен, и, в результате, исключить конфликт имен, не зависимо от того, сколько людей подключаются к интернет и начинают писать классы на Java.

Создание уникальных имен пакетов

Обратите внимание, что пакет, на самом деле, не всегда располагается в единичном файле, и может быть собран из большого количества .class файлов - и, в результате, получится небольшой беспорядок. Чтобы не допустить этого, по логике вещей, нужно поместить все .class файлы в один пакет и в отдельный каталог, с помощью иерархии файловой структуры Вашей операционной системы. Это один из способов - как Java решает проблему нагромождения файлов; С другим способом Вы познакомитесь позже, когда будет описана утилита jar.

Сборка файлов пакета в отдельном каталоге решает две другие проблемы: создания уникальных имен пакета, и поиска тех классов, которые могут быть скрыты где угодно в структуре каталогов. Это достигается, как было описано в Главе 2, с помощью указания пути к .class файлу в имени пакета, после ключевого слова package. Компилятор навязывает именно такую форму, но по соглашению, первая часть имени пакета зарезервирована - это доменное имя создателя класса в интернет. Поскольку доменные имена в интернете гарантированно являются уникальными, то, следуя этому соглашению, Вы гарантированно получаете уникальные имена пакетов и никогда получите конфликта имен. (Во всяком случае, пока Вы не отдадите это доменное имя кому-нибудь другому, кто начнет писать классы на Java с теми же именами путей, что и у Вас.) Конечно, если у Вас нет доменного имени, Вам придется придумать невероятную комбинацию (например, как Ваши имя и фамилия), для создания уникального имени пакета. Но если Вы решите опубликовывать код на Java, Вам стоит немного напрячься и получить собственное доменное имя.

Также необходимо расположение пакета в отдельном каталоге на Вашей машине. Когда Java программа запустится и потребует загрузки .class файла (что делается динамически, в том месте программы где создается объект соответствующего класса, либо впервые Вы получаете доступ к статическому члену класса), она сможет определить расположение каталога, где находятся .class файлы.

Интерпретатор Java действует следующим образом. Сначала, он ищет переменную среды с именем CLASSPATH (она устанавливается в операционной системе программой установки Java, либо инструментами, основанными на Java, на Вашей машине). CLASSPATH содержит один или более каталогов, которые используются как корневые для поиска .class файлов. Начиная с этого корневого каталога, интерпретатор берет имя пакета и заменяет каждую точку на косую черту для создания имени пути от корня в CLASSPATH (так, например, package foo.bar.baz превратится в foo\bar\baz или foo/bar/baz а, может быть, что-то другое, в зависимости от Вашей операционной системы). Затем это добавляется к различным элементам переменной CLASSPATH. Вот как интерпретатор ищет .class файлы, с именем класса, который Вы пытаетесь создать. (Он также производит поиск в стандартных каталогах, относительно того, где располагается сам интерпретатор).

Чтобы понять это, давайте рассмотрим мое доменное имя - bruceeckel.com. Резервируя его - com.bruceeckel - создаем уникальное глобальное имя для моих классов. (Имена com, edu, org, и т.д., раньше писались с заглавными буквами в пакетах Java, однако это изменилось в Java 2, так что сейчас имя пакета должно быть написано полностью в нижнем регистре.) Теперь если я хочу создать библиотеку с именем simple, у меня получится следующее имя пакета:

package com.bruceeckel.simple;
Теперь это имя пакета может быть использовано, как прикрытие для пространства имен у следующих двух файлов:
//: com:bruceeckel:simple:Vector.java
// Создание пакета.
package com.bruceeckel.simple;

public class Vector {
  public Vector() {
    System.out.println(
      "com.bruceeckel.util.Vector");
  }
} ///:~

Когда Вы создадите пакет, Вы обнаружите, что выражение package должно быть первой строкой кода после комментариев в файле. Второй файл выглядит следующим образом:

//: com:bruceeckel:simple:List.java
// Создание пакета.
package com.bruceeckel.simple;

public class List {
  public List() {
    System.out.println(
      "com.bruceeckel.util.List");
  }
} ///:~

Оба этих файла располагаются в подкаталоге на моей машине:

C:\DOC\JavaT\com\bruceeckel\simple

Если Вы вернетесь назад, то увидите имя пакета com.bruceeckel.simple. А что же насчет первой части пути? Об этом заботится переменная CLASSPATH, которая, на моей машине, содержит следующее значение:

CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT

Вы видите, что CLASSPATH содержит несколько альтернативных путей поиска.

Однако, при использовании JAR файлов, есть небольшая разница. Вы должны указывать имя JAR файла в CLASSPATH, а не только путь к нему. Так, для JAR файла grape.jar, Ваша переменная CLASSPATH может содержать:

CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar

Как только переменная CLASSPATH корректно установлена, следующий файл может располагаться в любом каталоге:

//: c05:LibTest.java
// Использует библиотеку.
import com.bruceeckel.simple.*;

public class LibTest {
  public static void main(String[] args) {
    Vector v = new Vector();
    List l = new List();
  }
} ///:~

Когда компилятор встречает выражение import, он начинает поиск с каталогов, указанных в CLASSPATH, там ищет подкаталог com\bruceeckel\simple, а затем, откомпилированный файл с соответствующим именем (Vector.class для Vector и List.class для List). Обратите внимание, что оба класса и необходимые методы в Vector и List должны быть публичными.

Установка переменной CLASSPATH стало как бы испытанием для новичков в Java (так было и для меня, когда я начинал), хотя JDK в Java 2 от Sun стал более умным. Вы увидите, что, после установки, даже если Вы не установили переменную CLASSPATH, Вы сможете компилировать и запускать основные программы на Java. Однако, для компиляции и запуска исходных кодов из этой книги (доступных на CD ROM поставляющемся вместе с книгой, либо на www.BruceEckel.com), Вам нужно будет сделать некоторые модификации переменной CLASSPATH (которые описываются в пакете исходных кодов).

Коллизии

Что произойдет, если две библиотеки импортируются с помощью * и содержат одинаковые имена? Например, предположим, что в программе есть следующие строки:

import com.bruceeckel.simple.*;
import java.util.*;

Т.к. java.util.* также содержит класс Vector, это приведет к потенциальной коллизии. Однако, пока Вы не пишете код, который может вызвать коллизию, все будет в порядке, и это хорошо, т.к. в противном случае, Вам придется очень много печатать на клавиатуре, чтобы предотвратить возможную коллизию.

Коллизия произойдет, если Вы попробуете создать класс Vector:

Vector v = new Vector();

Какой из классов Vector должен здесь участвовать? Этого не знает ни компилятор, ни читатель. Так что, компилятор выразит недовольство и заставит Вас быть более точным. Если Вам нужен стандартный класс Java, например, Vector, Вы можете написать:

java.util.Vector v = new java.util.Vector();

Поскольку такая форма (совместно с CLASSPATH) полностью определяет положение этого класса Vector, нет потребности в выражении import java.util.*, пока Вы не захотите использовать что-нибудь еще из java.util.

Библиотека инструментов пользователя

С помощью этих знаний, Вы сейчас сможете создать свои собственные библиотеки инструментов, чтобы уменьшить, либо полностью исключить дублирование кода. Вот пример - создание псевдонима для System.out.println( ), чтобы уменьшить объем печати. Это может стать частью пакета с названием tools:

//: com:bruceeckel:tools:P.java
// P.rint & P.rintln сокращения.
package com.bruceeckel.tools;

public class P {
  public static void rint(String s) {
    System.out.print(s);
  }
  public static void rintln(String s) {
    System.out.println(s);
  }
} ///:~

Вы можете использовать эти сокращения для печати String либо с новой строки (P.rintln( )), либо на текущей строке (P.rint( )).

Как Вы можете догадаться, этот файл должен располагаться в одном из каталогов, указанных в CLASSPATH плюс com/bruceeckel/tools. После компиляции файл P.class может использоваться где угодно в Вашей системе после выражения import:

//: c05:ToolTest.java
// Использует библиотеку инструментов.
import com.bruceeckel.tools.*;

public class ToolTest {
  public static void main(String[] args) {
    P.rintln("Available from now on!");
    P.rintln("" + 100); // Приводит к типу String
    P.rintln("" + 100L);
    P.rintln("" + 3.14159);
  }
} ///:~

Обратите внимание, что все объекты могут быть преобразованы в представление String простой установкой их в выражение вместе с объектом String; в примере выше, это делается с помощью помещения пустой строки в начале выражения String. Однако, есть небольшое замечание. Если Вы вызываете System.out.println(100), это работает без приведения к типу String. Конечно, Вы можете, используя перегрузку, заставить класс P делать то же самое (это упражнение представлено в конце этой главы).

Итак, начиная с этого момента, как только у Вас появляется новая полезная утилита, Вы вполне можете добавить ее в каталог tools. (Либо в Ваш собственный каталог util или tools.)

Использование импорта для изменения поведения

В языке C существует условная компиляция, которая позволяет устанавливать различное поведение Вашего кода без изменения самого кода. В Java такой возможности нет. Причина, по которой эта функция отсутствует в Java, возможно в том, что в языке C эта функция в основном использовалась для создания кросс-платформенных приложений: компилировались различные куски кода, в зависимости от платформы, на которой они работали. Поскольку Java автоматически поддерживает кросс-платформенность, необходимости в такой функции нет.

Однако, есть и другая необходимость в условной компиляции. Самое распространенное использование - отладочный код. Отладка включается в процессе разработки, и отключается в конечном продукте. Аллен Холуб (Allen Holub) (www.holub.com) предложил идею - использования пакетов, для имитации условной компиляции. Он использовал это для создания Java-версии очень полезного механизма контроля (assertion) из языка C, с помощью которого Вы можете сказать “это должно быть истинно” либо “это должно быть ложно” и, если выражение не удовлетворяет этому контролю, Вы узнаете об этом. Такой инструмент является очень полезным во время отладки.

Вот класс, который Вы можете использовать для отладки:

//: com:bruceeckel:tools:debug:Assert.java
// Инструмент контроля для отладки.
package com.bruceeckel.tools.debug;

public class Assert {
  private static void perr(String msg) {
    System.err.println(msg);
  }
  public final static void is_true(boolean exp) {
    if(!exp) perr("Assertion failed");
  }
  public final static void is_false(boolean exp){
    if(exp) perr("Assertion failed");
  }
  public final static void 
  is_true(boolean exp, String msg) {
    if(!exp) perr("Assertion failed: " + msg);
  }
  public final static void 
  is_false(boolean exp, String msg) {
    if(exp) perr("Assertion failed: " + msg);
  }
} ///:~

Этот класс просто инкапсулирует булевские тесты, и печатает сообщение об ошибке, если эти тесты завершаются неудачно. В Главе 10, Вы познакомитесь с более изощренным инструментом для борьбы с ошибками, называемым обработка исключений, а пока метод perr( ) будет отлично работать.

Результат отправляется на консоль в поток стандартных ошибок - System.err.

Когда Вам необходимо использовать этот класс, Вы добавляете одну строку в свою программу:

import com.bruceeckel.tools.debug.*;

Для отключения этого контроля, Вы можете использовать код, где реализован второй класс Assert, находящийся в другом пакете:

//: com:bruceeckel:tools:Assert.java
// Отключение контроля 
package com.bruceeckel.tools;

public class Assert {
  public final static void is_true(boolean exp){}
  public final static void is_false(boolean exp){}
  public final static void 
  is_true(boolean exp, String msg) {}
  public final static void 
  is_false(boolean exp, String msg) {}
} ///:~

Так, если Вы измените предыдущее выражение import на:

import com.bruceeckel.tools.*;

программа больше не будет печатать контрольные данные. Вот пример:

//: c05:TestAssert.java
// Демонстрация инструмента контроля.
// Комментируете первую или вторую строчку и 
// получаете различные результаты:
import com.bruceeckel.tools.debug.*;
// import com.bruceeckel.tools.*;

public class TestAssert {
  public static void main(String[] args) {
    Assert.is_true((2 + 2) == 5);
    Assert.is_false((1 + 1) == 2);
    Assert.is_true((2 + 2) == 5, "2 + 2 == 5");
    Assert.is_false((1 + 1) == 2, "1 +1 != 2");
  }
} ///:~

Изменением импортируемого пакета, Вы производите переход от отладочной к конечной версии. Эта техника может быть использована для создания отладочного кода любого типа.

Пакетное предостережение

Необходимо запомнить, что когда Вы создаете пакет, Вы косвенно задаете структуру каталогов при задании имени пакета. Пакет должен находиться в каталоге, определенном в имени пакета, причем этот каталог должен быть доступен по переменной CLASSPATH. Экспериментирование с ключевым словом package может быть бесполезным вначале, поскольку пока Вы не будете придерживаться правила: имя пакета определяет путь к нему, Вы будете получать множество непонятных run-time сообщений, сообщающих о невозможности найти какой-нибудь класс, даже если он находится в том же самом каталоге. Если Вы получите подобное сообщение, попробуйте закомментировать выражение package, и, если все заработает, то Вы знаете, в чем проблема.

Спецификаторы доступа в Java

Спецификаторы доступа Java public, protected и private располагаются перед каждым определением каждого члена в Вашем классе, независимо от того, метод это или просто поле. Каждый спецификатор доступа определяет доступ только для одного конкретного определения. В этом - явное различие с языком C++, в котором спецификатор доступа определяет доступ для всех последующих определений, пока не встретится другой спецификатор доступа.

Так или иначе, у всего имеется какой-то тип доступа. Далее Вы узнаете все о различных типах доступа, начиная с типа доступа по умолчанию.

Дружественный доступ “Friendly”

А что, если Вы вообще не определяете спецификатор доступа, как это было сделано во всех примерах до настоящей главы? Доступ по умолчанию не имеет ключевого слова, но обычно называется дружественным - “friendly.” Это значит, что все другие классы в том же пакете имеют доступ к дружественным членам, но для классов за пределами этого пакета, члены являются приватными (private). Т.к. файл модуля компиляции может принадлежать только одному пакету, все классы внутри этого единичного модуля компиляции автоматически являются дружественными друг другу. Таким образом, говорят, что дружественные элементы имеют доступ на уровне пакета.

Дружественный доступ позволяет Вам объединять связанные классы в пакете, так, что они могут легко общаться друг с другом. Когда Вы располагаете классы вместе в одном пакете, (определив таким образом совместный доступ для дружественных членов), Вы “владеете” кодом в этом пакете. Во многих языках, Вам волей-неволей приходится организовывать определения в файлах, но Java Вас заставляет создавать их в разумной форме. К тому же, Вы, возможно, захотите исключить классы, которые не должны иметь доступ к классам в том же пакете.

Класс управляет тем, какой код имеет доступ к его членам. И нет никакого магического способа “прорваться внутрь.” Код из другого пакета не может появиться и сказать, “Привет, Я друг Боба!” и затем посмотреть все защищенные, дружественные и приватные члены Боба. Единственный путь получить доступ, это:

  1. Сделать этот член публичным. И кто угодно, откуда угодно сможет получить к нему доступ.
  2. Сделайте это член дружественным, удалив все спецификаторы доступа, и расположите классы в одном пакете.
  3. Как Вы увидите в Главе 6, когда наследование определено, унаследованный класс получает доступ к защищенным членам, а также к публичным членам (но не приватным). Этот класс может получить доступ к дружественным членам, только если эти два класса находятся в одном пакете. Но Вам не стоит беспокоиться об этом сейчас.
  4. Предоствавьте методы “accessor/mutator” (также известные как “get/set” методы), которые читают и изменяют значение какого-то поля класса. Это самый цивилизованный подход в терминах ООП, и это основной подход в JavaBeans, как Вы увидите в Главе 13.

public: интерфейсный доступ

Если Вы используете ключевое слово public, это значит, что объявление, следующее сразу за этим словом, доступно всем, и, конечно, клиентскому программисту, который использует эту библиотеку. Предположим, что Вы создаете пакет dessert, содержащий следующий модуль компиляции:

//: c05:dessert:Cookie.java
// Создаем библиотеку.
package c05.dessert;

public class Cookie {
  public Cookie() { 
   System.out.println("Cookie constructor"); 
  }
  void bite() { System.out.println("bite"); }
} ///:~

Запомните, Cookie.java должен располагаться в каталоге c05\dessert (с05 означает пятую главу этой книги), который должен быть доступен по одному из путей в CLASSPATH. Не надейтесь, что Java всегда просматривает текущий каталог, как один из начальных каталогов для поиска классов. Если Вы не добавите путь "." в переменную среды CLASSPATH, Java не будет этого делать.

Теперь, если Вы создадите программу, использующую Cookie:

//: c05:Dinner.java
// Использует библиотеку.
import c05.dessert.*;

public class Dinner {
  public Dinner() {
   System.out.println("Dinner constructor");
  }
  public static void main(String[] args) {
    Cookie x = new Cookie();
    //! x.bite(); // Недоступно
  }
} ///:~

Вы сможете создать объект Cookie, т.к. его конструктор и сам класс являются публичными. (Далее Вы больше узнаете о концепции публичных классов.) Однако, метод bite( ) недоступен внутри Dinner.java т.к. bite( ) остается дружественным только внутри пакета dessert.

Пакет по умолчанию

Вы, возможно, удивитесь, когда узнаете, что следующий код компилируется, хотя Вам может показаться, что он нарушает правила языка Java:

//: c05:Cake.java
// Получает дочтуп к классу 
// в другом модуле комиляции

class Cake {
  public static void main(String[] args) {
    Pie x = new Pie();
    x.f();
  }
} ///:~

Другой файл в том же каталоге содержит следующее:

//: c05:Pie.java
// Другой класс.

class Pie {
  void f() { System.out.println("Pie.f()"); }
} ///:~

Вначале Вы можете посчитать эти файлы абсолютно чужими, и все же Cake может создать объект Pie и вызвать его метод f( )! (Конечно, Вам нужно, чтобы CLASSPATH содержал ".", иначе файлы не будут компилироваться.) Вы можете подумать, что и класс Pie и его метод f( ) являются дружественными и недоступны объекту Cake. То, что они дружественны - это верно! А причина, по которой они доступны в Cake.java в том, что они находятся в одном и том же каталоге и не имеют конкретного имени пакета. Java считает эти файлы частью “пакета по умолчанию” для этого каталога, и поэтому, дружественными всем остальным файлам в этом каталоге.

private: Вы не можете коснуться этого!

Ключевое слово private означает, что никто не имеет доступа к этому члену, за исключением класса, в котором он находится. Другие классы в том же пакете не смогут получить доступ к приватным членам. С другой стороны, очень часто пакет создается группой людей, работающих вместе и private позволит Вам свободно изменять члены класса, не беспокоясь о том, что это может нарушить работу другого класса в том же пакете.

Дружественного доступа по умолчанию часто достаточно для скрытия реализации; запомните: “дружественный” член недоступен пользователю пакета. Это здорово, т.к. доступ по умолчанию - то, что Вы обычно используете (и это именно то, что Вы получаете, когда вообще не ставите идентификаторы доступа). Таким образом, Вы можете подумать, что доступ к членам должен быть публичным для клиентского программиста, и в результате, Вам не нужно использовать ключевое слово private, т.к. Вы можете прожить без него. Однако, использование private очень важно, особенно в многопоточных программах. (Как Вы увидите в Главе 14.)

Вот пример использования private:

//: c05:IceCream.java
// Демонстрирует ключевое слово "private".

class Sundae {
  private Sundae() {}
  static Sundae makeASundae() { 
    return new Sundae(); 
  }
}

public class IceCream {
  public static void main(String[] args) {
    //! Sundae x = new Sundae();
    Sundae x = Sundae.makeASundae();
  }
} ///:~

Этот пример показывает использование private: Вам может потребоваться контроль создания объекта и предотвращение прямого доступа к какому-нибудь конструктору (или всем конструкторам). В примере выше, Вы не можете создать объект Sundae с помощью его конструктора; для этого Вы должны вызвать метод makeASundae( )[33].

Любой метод, который Вы считаете вспомогательным для этого класса можно сделать приватным, что будет гарантировать, что Вы не использовали случайно этот метод в другом месте того же пакета, и в результате не можете изменить либо удалить его. Создание метода приватным, гарантирует именно такой результат.

Это верно и для приватных полей внутри класса. Если Вы не собираетесь раскрывать реализацию (что бывает реже, чем Вы думаете), Вам следует сделать все поля приватными. Однако, если ссылка на объект является приватной внутри класса, это не значит, что другой объект не может иметь публичную ссылку на тот же объект. (Смотрите приложение A об алиасах.)

protected: “тип дружественного доступа”

Спецификатор доступа protected требует дополнительных усилий для понимания. Но Вы должны знать, что Вам не требуется понимать этот раздел, чтобы продолжать дальнейшее чтение разделов о наследовании (Глава 6). Но для завершенности, здесь представлено краткое описание и примеры использования ключевого слова protected.

Ключевое слово protected разрешает концепцию названную наследование, которое берет существующий класс и добавляет в него новые члены не затрагивая исходного (базового класса). Вы также можете изменить поведение существующих методов класса. Для наследования от существующего класса Вы говорите, что новый класс расширяет (extends) существующий класс:

class Foo extends Bar {

Дальнейшее определение класса выглядит также.

Если Вы создаете новый пакет и наследуете класс из другого пакета, то единственные члены, к которым Вы имеете доступ, это публичные члены в исходном пакете. (Конечно, если наследование происходит в том же самом пакете, Вы имеете нормальный пакетный доступ для всех “дружественных” членов.) Но иногда, создатель базового класса хочет разрешить доступ к конкретному члену только для наследуемого класса, но не всему миру в целом. Именно это делает protected. Если Вы рассмотрите снова файл Cookie.java, нижеследующий класс не может получить доступ к “дружественному” члену:

//: c05:ChocolateChip.java
// Нет доступа к члену
// другого класса.
import c05.dessert.*;

public class ChocolateChip extends Cookie {
  public ChocolateChip() {
   System.out.println(
     "ChocolateChip constructor");
  }
  public static void main(String[] args) {
    ChocolateChip x = new ChocolateChip();
    //! x.bite(); // Нет доступа к bite
  }
} ///:~

Одна из интересных особенностей наследования заключается в том, что если метод bite( ) существует в классе Cookie, то он также существует в любом наследуемом от Cookie классе. Но, т.к. bite( ) является “дружественным” в другом пакете, он недоступен нам в этом. Конечно, Вы можете сделать его публичным public, но тогда каждый будет иметь к нему доступ, и может быть, Вы не хотите этого. Если мы изменим класс Cookie, как показано ниже:

public class Cookie {
  public Cookie() { 
    System.out.println("Cookie constructor");
  }
  protected void bite() {
    System.out.println("bite"); 
  }
}

то метод bite( ) будет иметь “дружественный” доступ внутри пакета dessert, а также будет доступен всем наследникам класса Cookie. Однако, он - не публичный.

Интерфейс и реализация

Контроль доступа часто называют скрытием реализации. Завертывание методов и данных в классах в комбинации со скрытием реализации называется часто инкапсуляцией[34]. Результат - это тип данных с определенными характеристиками и поведением.

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

Отсюда следует вторая причина, это разделение описания и реализации. Если эта структура используется в нескольких программах, но клиентские программисты не могут ничего общения с публичными членами, то Вы можете менять как угодно все, что не является публичным (e.g., “дружественным,” защищенным, либо приватным), без модификаций клиентского кода.

Мы живем в мире объектно-ориентированного программирования, где class обычно описывает “класс объектов,” как Вы можете описать класс рыб или класс птиц. Любой объект, принадлежащий этому классу разделит эти характеристики и поведение. Класс - это описание того, как все объекты этого типа будут выглядеть и действовать.

В оригинальном объектно-ориентированном языке, Simula-67, ключевое слово class использовалось для описания нового типа данных. То же самое ключевое слово используется в большинстве объектно-ориентированных языков. Это основной момент целого языка: создание нового типа данных, который является чем-то большим, чем просто контейнером содержащим данные и методы.

Класс - основная концепция ООП в Java. Это одно из ключевых слов, которое не будет выделено жирным шрифтом в этой книге —чтобы не было путаницы со словом повторяемым также часто, как и “класс.”

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

public class X {
  public void pub1( ) { /* . . . */ }
  public void pub2( ) { /* . . . */ }
  public void pub3( ) { /* . . . */ }
  private void priv1( ) { /* . . . */ }
  private void priv2( ) { /* . . . */ }
  private void priv3( ) { /* . . . */ }
  private int i;
  // . . .
}

Это всего лишь частично упростит чтение, т.к. описание и реализация все еще находятся вместе. То есть, Вы еще видите исходный код —реализацию—, поскольку она находится здесь же, в классе. К тому же, документация из комментариев, поддерживаемая утилитой javadoc (описанной в Главе 2) преуменьшает важность чтения кода клиентским программистом. Отображение интерфейса для пользователя класса это, на самом деле, занятие браузера классов, инструмента, чья работа состоит в том, чтобы просмотреть все доступные классы и показать Вам, что Вы можете делать с ними (т.е. показать все доступные члены), в удобной форме. К тому времени, как Вы прочитаете это, такие браузеры должны быть частью любой хорошей среды разработки Java.

Доступ класса

В Java, спецификаторы доступа могут также использоваться для определения того, какие классы внутри библиотеки будут доступны пользователям библиотеки. Если Вы хотите сделать класс доступным клиентскому программисту, Вы располагаете ключевое слово public где-нибудь перед открывающей фигурной скобкой тела класса. Это определяет, может ли клиентский программист создать объект этого класса.

Для контроля доступа к классу, спецификатор должен располагаться перед ключевым словом class. Итак, Вы можете написать:

public class Widget {

Если имя Вашей библиотеки mylib любой клиентский программист может получить доступ к Widget с помощью

import mylib.Widget;

либо

import mylib.*;

Однако, существует несколько дополнительных ограничений:

  1. Может существовать только один публичный класс в одном модуле компиляции (файле). Идея состоит в том, что один модуль компиляции имеет один публичный интерфейс, представленный этим публичным классом. Он может иметь так много поддерживающих “дружественных” классов, сколько Вам необходимо. Если у Вас больше одного публичного класса в модуле компиляции, компилятор выдаст сообщение об ошибке.
  2. Имя публичного класса должно полностью совпадать, с именем файла, содержащего соответствующий модуль компиляции, включая регистры символов. Так, например, для класса Widget, имя файла должно быть Widget.java, но никак не widget.java или WIDGET.java. Итак, Вы получите ошибку компиляции, если Вы с этим не согласны.
  3. Возможно, но не типично, что у Вас будет модуль компиляции вообще без публичного класса. В этом случае, Вы можете называть файл как хотите.

А что, если у Вас есть такой класс внутри библиотеки mylib, который Вы используете для выполнения задач представленных классом Widget или каким-то другим публичным классом в mylib? Вы не хотите создавать документацию для клиентского программиста, и думаете, что когда-нибудь позже Вы захотите все изменить, либо вообще удалить класс, заменяя его другим. Чтобы иметь такую возможность, Вам нужно убедиться, что ни один клиентский программист не зависит от Ваших деталей реализации, скрытых внутри mylib. Для достижения этого, Вы удаляете ключевое слово public из класса, в этом случае он становится дружественным. (Этот класс может быть использован только внутри этого пакета.)

Обратите внимание, что класс не может быть private (это сделало бы его никому не доступным кроме самого этого класса), или protected[35]. Итак, у Вас есть только выбор из двух вариантов: “дружественный” или публичный. Если Вы не хотите, чтобы кто-то другой имел доступ к классу, Вы можете сделать все конструкторы приватными, этим запрещая любому кроме Вас, создание объекта этого класса внутри статического члена класса.[36]. Вот пример:

//: c05:Lunch.java
// Демонстрирует спецификаторы доступа к классу.
// Делает класс приватным
// с помощью приватных конструкторов:

class Soup {
  private Soup() {}
  // (1) Позволяет создание с помощью статического метода:
  public static Soup makeSoup() {
    return new Soup();
  }
  // (2) Создание статического объекта
  // возвращается ссылка на запрос.
  // (шаблон "Singleton"):
  private static Soup ps1 = new Soup();
  public static Soup access() {
    return ps1;
  }
  public void f() {}
}

class Sandwich { // Использует Lunch
  void f() { new Lunch(); }
}

// В файле только один публичный класс:
public class Lunch {
  void test() {
    // Вы не можете сделать это! Приватный контруктор:
    //! Soup priv1 = new Soup();
    Soup priv2 = Soup.makeSoup();
    Sandwich f1 = new Sandwich();
    Soup.access().f();
  }
} ///:~

До сих пор, большинство методов возвращали либо void либо примитивный тип, и описание:

  public static Soup access() {
    return ps1;
  }

может вначале привести в замешательство. Слово перед именем метода (access) говорит о том, что возвращает метод. Пока чаще всего был тип void, и это означало, что метод не возвращает ничего. Но Вы можете возвратить также ссылку на объект, что и происходит здесь. Этот метод возвращает ссылку на объект класса Soup.

Класс Soup показывает как предотвратить прямое создание класса, сделав все конструкторы приватными. Запомните, что если Вы не сознаете явно ни одного конструктора, конструктор по умолчанию (конструктор без аргументов) сам будет создан для Вас. Если Вы напишите конструктор по умолчанию, он не будет создаваться автоматически. Если Вы сделаете его приватным, то никто не сможет создать объект этого класса. Но сейчас, как кто-нибудь сможет использовать этот класс? Пример выше показывает два варианта. Первый, с помощью статического метода создается объект типа Soup и возвращается ссылка на него. Это может быть полезно, если Вы хотите выполнить несколько дополнительных операций с классом Soup, перед тем как его возвратить, либо если Вы хотите хранить количество создаваемых объектов типа Soup (возможно для контроля их популяции).

Второй вариант использует так называемый шаблон разработки, который описан в книге Thinking in Patterns with Java, доступной на с www.BruceEckel.com. Этот специфический шаблон называется “singleton” потому что он позволяет создавать только один объект. Объект класса Soup создается как статический приватный член класса Soup, и существует один и только один объект, и Вы не можете получить его никаким другим способом, кроме как с помощью публичного метода access( ).

Как ранее было упомянуто, если Вы вообще не ставите идентификатор доступа для класса, он становится “дружественным.” Это означает, что объект этого класса может быть создан в любом другом классе того же пакета, но не за его пределами. (Запомните, все файлы в одном каталоге не имеющие явного выражения package, принадлежат пакету по умолчанию для этого каталога.) Однако, однако, если статический член этого класса - публичный, то клиентский программист сможет получить доступ к этому статическому члену, даже если он не сможет создать объект этого класса.

Резюме

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

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

Эта глава показывает, как формировать библиотеку из классов; как располагать классы в библиотеке, и как управлять доступом к членам класса.

Подсчитано, что проекты на языке C начинают разлаживаться между 50K и 100K строчек кода, т.к. C имеет единое “пространство имен”, и имена начинают конфликтовать друг с другом, требуя дополнительных модификаций кода. В Java, ключевое слово package, схема именования пакетов, и ключевое слово import дает вам полный контроль над именами, и коллизия имен легко предотвращается.

Есть две причины для использования контроля доступа к членам данных. Первая - не дать пользователям доступа к инструментам, которые они не должны использовать; инструменты, которые необходимы для внутренних манипуляций типа данных, но не часть интерфейса, который нужен пользователям для решения их проблем. Создание методов и полей приватными - удобство для пользователей, поскольку они могут легко увидеть то, что им нужно, и что они могут игнорировать. Это упрощает для них понимание работы класса.

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

Спецификаторы доступа Java дают значительный контроль создателю класса. Пользователи класса смогут просто увидеть, что они в точности могут использовать и что могут игнорировать. Хотя, более важно знать, что ни один пользователь не зависит ни от какой части внутренней реализации класса. Если Вы уверены в этом как проектировщик класса, Вы сможете изменить внутреннюю реализацию, зная, что ни один клиентский код не будет затронут этими изменениями, из-за того что он не может получить доступ к той части класса.

Когда у Вас есть возможность изменять внутреннюю реализацию, Вы можете не только улучшить свой дизайн позже, но также иметь свободу создания ошибок. Не важно, с каким вниманием Вы планируете и пишите, Вы все равно сделаете ошибки. Знания, что это относительно безопасно - сделать такие ошибки- Вы получитет больше опыта, обучитесь быстрее, и закончите Ваш проект раньше.

Упражнения

Решения к избранным упражнениям находятся в электронном документе The Thinking in Java Annotated Solution Guide, доступном за небольшую плату на www.BruceEckel.com.

  1. Напишите программу создающую объект ArrayList без явного импорта java.util.*.
  2. В разделе “package: модуль библиотеки,” перепишите фрагменты кода, относящиеся к mypackage в компилируемый и запускаемый набор файлов Java.
  3. В разделе “Коллизии,” возьмите фрагменты кода и перепишите их в программу, и проверьте, что коллизии действительно происходят.
  4. Обобщите класс P определенный в этой главе добавлением перегруженных версий rint( ) и rintln( ) необходимыми для управления всеми основными типами Java.
  5. Измените выражение import в TestAssert.java для включения или выключения механизма контроля.
  6. Создайте класс с публичными, приватными, защищенными, и “дружественными” методами и данными. Создайте объект этого класса и посмотрите какие ошибки компилятора Вы получите, пытаясь получить доступ ко всем членам этого класса. Убедитесь, что классы в одном каталоге являются частью пакета по умолчанию.
  7. Создайте класс с защищенными(protected) данными. Создайте второй класс в том же файле с методом, который манипулирует с защищенными данными в первом классе.
  8. Измените класс Cookie как указано в разделе “protected: ‘тип дружественного доступа.’” Проверьте что метод bite( ) не публичный.
  9. В разделе “Доступ класса” Вы найдете фрагменты кода описывающие mylib и Widget. Создайте эту библиотеку, и затем создайте Widget в классе не являющемся частью пакета mylib.
  10. Создайте новый каталог и отредактируйте переменную CLASSPATH чтобы включить туда новый каталог. Скопируйте файл P.class (после компиляции com.bruceeckel.tools.P.java) в Ваш новый каталог и затем измените имена файла, класс P внутри и имена методов. (Вы можете также захотеть добавить дополнительный вывод, чтобы видеть как это работает.) Создайте еще одну программу в другом каталоге которая использует Ваш новый класс.
  11. Следуя форме примера Lunch.java, создайте класс с именем ConnectionManager, который управляет фиксированным массивом объектов Connection. Клиентский программист не должен иметь возможности явного создания объектов Connection, а может только получить их из статического метода в ConnectionManager. Когда в ConnectionManager параметр выходит за пределы объектов, он возвращает ссылку на null. Проверьте классы в main( ).
  12. Создайте следующий файл в каталоге c05/local (доступном по CLASSPATH):
///: c05:local:PackagedClass.java
package c05.local;
class PackagedClass {
  public PackagedClass() {
    System.out.println(
      "Creating a packaged class");
  }
} ///:~

Затем создайте следующий файл в другом каталоге - не c05:

///: c05:foreign:Foreign.java
package c05.foreign;
import c05.local.*;
public class Foreign {
   public static void main (String[] args) {
      PackagedClass pc = new PackagedClass();
   }
} ///:~

Объясните, почему компилятор генерирует ошибку. Изменит ли что-нибудь помещение класса Foreign в пакет c05.local?


[32] В Java что заставляет использовать интерпретатор. Существуют компиляторы Java создающие единичный исполняемый файл.

[33] Есть другой эффект в этом случае: т.к. конструктор по умолчанию - единственный из определенных, и он - приватный, это предотвратит наследование от этого класса. (Тема, описанная в Главе 6.)

[34] Однако, люди часто ссылаются на скрытие реализации как на инкапсуляцию.

[35] В действительности внутренний класс может быть приватным или защищенным, но это уже специальные случаи. Они будут описаны в Главе 7.

[36] Вы также можете сделать это наследованием (Глава 6) от этого класса.

[ Предыдущая глава ] [ Оглавление ] [ Содержание ] [ Индекс ] [ Следующая глава ]