Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Язык программирования JAVA.pdf
Скачиваний:
374
Добавлен:
02.05.2014
Размер:
2.57 Mб
Скачать

converted to PDF by BoJIoc

Оставшаяся часть главы посвящена программированию с использованием классов- оболочек для примитивных типов.

13.1. Класс Class

Для каждого класса и интерфейса в системе имеется представляющий его объект Class.

Этот объект может использоваться для получения основных сведений о классе или интерфейсе и для создания новых объектов класса.

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

Существует два способа получить объект Class: запросить его у имеющегося объекта методом getClass или искать его по уточненному (включающему все имена пакетов) имени статическим методом Class.forName.

Простейшие методы Class предназначены для перемещения по иерархии типов. Приведенный ниже класс выводит такую иерархию для конкретного объекта Class:

public class TypeDesc {

public static void main(String[] args) { TypeDesc desc = new TypeDesc();

for (int i = 0; i << args.length; i++) { try {

desc.printType(Class.forName(args[i]), 0); } catch (ClassNotFoundException e) {

System.err.print(e); // сообщить об ошибке

}

}

//по умолчанию работать со стандартным выводом public java.io.PrintStream out = System.out;

//используется в printType() для пометки имен типов private static String[]

basic

=

{

"class",

"interface"

},

extended

=

{

"extends",

"implements"

};

public void printType(Class type, int depth) {

if (type == null) // супертип Object равен null return;

// вывести тип

for (int i = o; i << depth; i++) out.print(" ");

String[] labels = (depth == 0 ? basic : extended); out.print(labels[type.isInterface() ? 1 : 0] + " "); out.println(type.getName());

//вывести интерфейсы, реализуемые классом

Class[] interfaces = type.getInterfaces(); for (int i = o; i << interfaces.length; i++) printType(interfaces[i], depth + 1);

//рекурсивный вызов для суперкласса

converted to PDF by BoJIoc

printType(type.getSuperclass(), depth + 1);

}

}

Данный пример просматривает имена, введенные в командной строке, и вызывает printType для каждого из них. Делать это необходимо в try-блоке на тот случай, если класс с заданным именем отсутствует. Ниже показан результат работы программы для класса java.util.Hashtable (используется полное уточненное имя, поскольку этого требует метод forName):

class java.util.Hashtable implements java.lang.Cloneable

extends java.lang.Object extends java.util.Dictionary extends java.lang.Object

Далее в тексте программы следует объявление выходного потока. Для краткости мы объявили его открытым, но в реальном приложении он должен быть закрытым, а обращения к нему должны осуществляться только через методы доступа. Затем следует описание двух строковых массивов.

Метод printType выводит описание своего типа, а затем рекурсивно вызывает себя для распечатки свойств супертипов. Параметр depth показывает, на сколько уровней мы поднялись в иерархии типов; в зависимости от его значения каждая строка с описанием снабжается соответствующим отступом. С каждым новым уровнем рекурсии это значение увеличивается.

При выводе типа метод isInterface определяет, является ли тип интерфейсом. Результат вызова используется для выбора префикса пояснительной надписи. В самом низу иерархии типов, где значение depth равно 0, выводятся надписи “class” и “interface”; типы, находящиеся выше в иерархии, расширяют или реализуют свои исходные типы, поэтому используются термины “extends” и “implements”. Именно для этого и создаются массивы Basic и Extended. После выбора нужного префикса имя типа выводится методом getName. Конечно, класс Class содержит метод toString, однако в этом методе уже использован префикс “class” или “interface”. Мы хотим сами контролировать префикс, и потому пришлось создать собственную реализацию метода.

После вывода описания типа метод printType осуществляет рекурсивный вызов себя самого. Сначала определяются все интерфейсы, реализуемые исходным типом. /Если тип, для которого выводится информация, представляет собой интерфейс, то он расширяет, а не реализует свои интерфейсы-супертипы. Тем не менее, учет таких подробностей привел бы к неоправданному усложнению кода./ Затем выводится расширяемый им суперкласс (если он существует). Постепенно метод доходит до объекта Class класса Object, который не реализует никаких интерфейсов и для которого метод getSuperClass возвращает null; на этом рекурсия завершается.

Упражнение 13.1

Модифицируйте TypeDesc, чтобы избежать вывода информации о классе Object. Эти сведения избыточны, поскольку каждый объект в конечном счете расширяет класс Object. Используйте ссылку на объект Class для типа Object.

Объект Class может воспользоваться методом newInstance для создания нового экземпляра (объекта) представляемого им типа. При этом вызывается безаргументный конструктор класса или возбуждается исключение NoSuch MethodError, если класс не имеет безаргументного конструктора. Если класс или безаргументный конструктор недоступны (не являются открытыми или находятся в другом пакете), возбуждается исключение IllegalAccessException. Если класс является абстрактным, или представляет собой интерфейс, или создание завершилось неудачно по какой-то другой причине, возбуждается исключение InstantiationException. Создавать новые объекты подобным образом оказывается удобно, когда вы хотите написать универсальный код и позволить

converted to PDF by BoJIoc

пользователю задать нужный класс. Например, в программе тестирования алгоритмов сортировки, приведенной в разделе Проектирование расширяемого класса”,

пользователь мог ввести имя тестируемого класса и использовать его в качестве параметра для вызова forName. Если введенное имя класса окажется допустимым, можно вызвать метод newInstance для создания объекта этого типа. Метод main для универсального класса SortDouble выглядит следующим образом:

static double[] testData = { 0.3, 1.3e-2. 7.9. 3.17, );

public static void main(String[] args) { try {

for (int arg = 0; arg << args.length; arg++) { String name = args[arg];

Class classFor = Class.forName(name); SortDouble sorter

=(SortDouble)classFor.newInstance(); SortMetrics metrics

=sorter.sort(testData); System.out.println(name + ": " + metrics); for (int i = 0; i << testData.length; i++)

System.out.println("\t" + testData[i]);

}

 

} catch (Exception e) {

// сообщить об ошибке

System.err.print(e);

}

 

}

Этот метод почти совпадает с BubbleSortDouble.main, однако из него исключены все имена типов. Он применим к любому типу, объекты которого могут использоваться в качестве объектов SortDouble и который содержит безаргументный конструктор. Теперь нам не придется переписывать метод main для каждого алгоритма сортировки универсальный main годится для всех случаев. Все, что нужно сделать, — выполнить

команду

java SortDouble TestClass ...

для любого класса-сортировщика (наподобие BubbleSortDouble); класс будет загружен и выполнен.

13.2. Загрузка классов

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

Например, вы разрабатываете игру, в которой играющие могут создавать собственные классы, использующие выбранную ими стратегию. Для этого вы создаете абстрактный класс Player, расширяемый игроками для реализации своих идей. Когда игроки будут готовы испытать свою стратегию, они пересылают скомпилированный байт-код класса в вашу систему. Байт-код необходимо загрузить в игру, применить и вернуть игроку его результат (score). Схема выглядит следующим образом:

converted to PDF by BoJIoc

На сервере игровая программа загружает каждый ожидающий своей очереди класс Player,

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

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

Наибольший интерес представляет процесс загрузки игровой программой скомпилированных классов. Здесь всем заправляет загрузчик классов. Чтобы создать загрузчик классов, следует расширить абстрактный класс Class Loader и реализовать в подклассе метод loadClass:

protected abstract Class loadClass(String name, boolean resolve) throws ClassNotFoundException

Загружает класс с заданным именем name. Если значение resolve равно true, то метод должен вызвать resolveClass, чтобы обеспечить загрузку всех классов, используемых внутри заданного.

В нашем примере мы создадим класс PlayerLoader, предназначенный для чтения байт- кода классов со стратегией игры и подготовки их к работе. Основной цикл будет выглядеть следующим образом:

public class Game {

static public void main(String[] args) { String name; // имя класса

while ((name = getNextPlayer()) != null) { try {

PlayerLoader loader = new PlayerLoader(); Class

classOf = loader.loadClass(name, true); Player

player = (Player)classOf.newInstance(); Game game = new Game();

player.play(game);

game.reportScore(name); } catch (Exception e) {

converted to PDF by BoJIoc

reportException(name, e);

}

}

}

}

Для каждой новой игры нужен свой объект-загрузчик PlayerLoader; следовательно, новый класс Player не будет смешиваться с классами, загруженными ранее. Новый PlayerLoader загружает класс и возвращает представляющий его объект Class, который используется для создания нового объекта класса Player. Затем мы создаем новую игру game и играем в нее. После ее завершения возвращается результат.

Класс PlayerLoader расширяет класс ClassLoader и задает собственную реализацию метода loadClass:

class PlayerLoader extends ClassLoader {

private Hashtable Classes = new Hashtable();

public Class loadClass(String name, boolean resolve) throws ClassNotFoundException

{

try {

Class newClass = (Class)Classes.get(name);

if (newClass == null) {

// еще не определен

try {

// проверить, не является

 

// ли системным классом

newClass = findSystemClass(name); if (newClass != null)

return newClass;

} catch (ClassNotFoundException e) {

;

// продолжить поиск

}

 

// класс не найден - его необходимо загрузить byte[] buf = bytesForClass(name);

newClass = defineClass(buf, 0, buf.length); Classes.put(name, newClass);

}

if (resolve)

resolveClass (newClass); return newClass;

} catch (IOException e) {

throw new ClassNotFoundException(e.toString());

}

}

// ... bytesForClass() и все прочие методы ...

}

Любая реализация loadClass должна загружать класс лишь в том случае, если это не было сделано ранее, поэтому метод содержит хеш-таблицу загруженных классов. Если класс уже присутствует в ней, то возвращается соответствующий объект Class. В противном случае loadClass сначала проверяет, можно ли найти класс в локальной системе, для чего вызывает метод find SystemClass класса ClassLoader; этот метод осуществляет поиск не только среди системных классов (находящихся в пакетах java), но и в пути, заданном для классов. Если при этом будет найден нужный класс, то после выполнения загрузки возвращается соответствующий ему объект Class.

converted to PDF by BoJIoc

Если поиск в обоих случаях заканчивается безрезультатно, необходимо прочитать байт- код класса, для чего служит метод bytesForClass:

protected byte[] bytesForClass(String name) throws IOException, ClassNotFoundException

{

FileInputStream in = streamFor(name);

int length

= in.available();

// получить количество байтов

if (length

== 0)

 

throw

new ClassNotFoundException(name);

byte[] buf = new byte[length];

// прочитать байт-код

in.read(buf);

return buf;

 

}

В нем использован метод streamFor (не приводится) для получения потока FileInputStream, содержащего байт-код класса. Затем мы создаем буфер нужного размера, считываем весь байт-код и возвращаем буфер.

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

После успешной загрузки класса метод loadClass возвращает новый объект Class.

Невозможно выгрузить класс, когда он перестает быть нужным. Вместо этого вы просто прекращаете его использование, чтобы класс-загрузчик мог быть уничтожен сборщиком мусора.

Чтобы получить объект-загрузчик для заданного объекта Class, следует вызвать его метод getClassLoader. Процесс загрузки классов был рассмотрен выше. Если у данного класса отсутствует класс-загрузчик, метод возвращает null.

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

1.Загрузка: получение байт-кода с реализацией класса.

2.Компоновка: поиск супертипов класса и загрузка их в случае необходимости.

3.Инициализация: присвоение начальных значений статическим полям класса и выполнение их инициализаторов, а также всех статических блоков.

Упражнение 13.2

Реализуйте классы Game и Player для какой-нибудь простой игры например, “крестики- нолики”. Проведите оценку различных реализаций Player по данным нескольких запусков.

13.3. Классы-оболочки: общий обзор

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