Несколько полезных классов Java позволят упростить процесс отладки.
Чтобы отладить приложение, можно поступить по-разному. Во-первых, можно воспользоваться штатным отладчиком из какого-либо программного пакета. Во-вторых, можно самостоятельно вычислить ошибку - логически, по внешним признакам. И в-третьих, можно "подглядеть", что же там такое творится внутри вашей программы. Для этого, последнего, способа и существуют всевозможные утилиты и библиотеки. Так, в библиотеке MFC из компилятора Visual C++ есть специальные макросы, которые во время запуска примера пересылают требуемую информацию в окно специальной утилиты-монитора (не правда ли, похоже на подглядывание в замочную скважину?). Подобный мониторинг данных стар, как само программирование, и очень прост: нужно выводить на экран дисплея значения переменных, расположенных в участках программы, вызывающих сомнения. Просматривая полученные данные, можно последовательно приблизиться к ошибочному участку.
Что касается Java-приложений, то и здесь мониторинг оказывается возможным, если воспользоваться выводом данных в стандартные потоки вывода и ошибок - System.out и System.err. Часто можно обнаружить в исходных текстах такую строку:
System.out.println("Входим в конструктор класса");
В результате выполнения данной команды в консольном окне приложения появится текстовая строка "Входим в конструктор класса" - знак того, что выполнение программы происходит в заданном месте.
Нельзя ли упростить мониторинг Java-приложений? Конечно же можно. Немного фантазии и понимание, как работают стандартные потоки out и err, нам помогут.
В исходных текстах в файле System.java есть описания стандартных потоков:
... public final static InputStream in = nullInputStream(); ... public final static PrintStream out = nullPrintStream(); ... public final static PrintStream err = nullPrintStream(); ...
Из них следует, что стандартный поток ввода in является ссылкой типа InputStream, а потоки вывода и ошибок out и err - ссылками класса PrintStream. Виртуальная машина Java инициализирует эти ссылки с помощью "родного" кода, который, увы, нам неподконтролен.
Итак, наш план прост: создав альтернативные стандартные потоки, упакуем их поудобнее и придумаем комфортабельный интерфейс доступа к ним, что, собственно говоря, и является главной целью всей этой работы. Начать, разумеется, нужно с создания класса, инкапсулирующего стандартные потоки ввода, вывода и ошибок. Назовем его StdStreams и поместим его исходный текст в файл StdStreams.java (см. Листинг 1).
Листинг 1. Создание класса Stdstreams
package Mitrich.utils; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; /** * Данный класс инкапсулирует стандартные потоки. * Он закрыт для доступа извне и изменений. */ final class StdStreams { private static InputStream in; // Зарезервировано private static PrintStream out; private static PrintStream err; static { StdStreams.setIn ( System.in ); StdStreams.setOut( System.out ); StdStreams.setErr( System.err ); } /** * Возвращает ссылку на поток err. * @return java.io.PrintStream */ PrintStream getErr() { return err; } /** * Возвращает ссылку на поток in. * @return InputStream */ InputStream getIn() { return in; } /** * Возвращает ссылку на поток out. * @return java.io.PrintStream */ PrintStream getOut() { return out; } /** * Устанавливает ссылку на поток in * @param stream java.io.InputStream */ static void setIn(InputStream stream) { in = stream; } /** * Устанавливает ссылку на поток err * @param stream java.io.OutputStream */ static void setErr(OutputStream stream) { err = new PrintStream(stream); } /** * Устанавливает ссылку на поток out * @param stream java.io.OutputStream */ static void setOut(OutputStream stream) { out = new PrintStream(stream); } }
Поскольку созданный нами класс не нуждается в наследовании, он реализован как final. Внутри него располагаются три поля, хранящие ссылки на используемые нами потоки (отмечены модификаторами доступа private и static). Почему они сделаны закрытыми для доступа (privat), вполне понятно: незачем изменять их напрямую. А вот добавление static требует пояснения. Дело в том, что в Java статические поля инициализируются сразу же после создания экземпляра класса. Таким образом, ссылки на потоки гарантированно инициализируются перед использованием и, что более важно, эти поля становятся уникальными для всех экземпляров класса StdStream. Из этого следует, что изменение ссылок на потоки - глобально, и все классы, обращающиеся за сервисом к StdStream, обращаются к одному и тому же полю. Обратите внимание, что ссылка на стандартный поток in считается зарезервированной (нам она не нужна, но чем черт не шутит, а вдруг впоследствии пригодится...). Поскольку все ссылки выполнены как static, то их инициализацию мы производим в статическом блоке.
Чтобы иметь возможность читать и изменять ссылки на потоки (а это нам нужно), у нас имеется тройка методов для чтения ссылок и тройка методов для установки новых ссылок. Все читающие методы начинаются с приставки get, а устанавливающие - с приставки set. Внимательный читатель заметит, что в качестве типа параметров методов setOut() и setErr() выступает класс OutputStream, хотя, как раньше отмечалось, потоки System.out и System.err имеют тип PrintStream. Это объясняется очень просто: абстрактный класс OutputStream является предком всех потоковых классов, и поэтому в качестве параметра можно подставить ссылку на любой класс-наследник от OutputStream, что удобно. Захотели вывести данные в файл, а не на консоль - пожалуйста!
Пользовательский интерфейс для мониторинговых классов должен быть удобным. Пришлось немного повозиться и придумать такие классы, которые было бы легко запомнить и еще легче использовать, например класс SetIn (см. Листинг 2), записанный в файле SetIn.java ниже.
Листинг 2. Создание класса SetIn
package Mitrich.utils; import java.io.InputStream; import java.io.OutputStream; /** * Данный класс служит для перенаправления * потока in * Зарезервирован для личных целей пользователя */ public final class SetIn { /** * Предотвращает создание экземпляра класса */ private SetIn() { } /** * Перенаправить поток in на другой поток */ public static void to(InputStream stream) { StdStreams.setIn(stream); } /** * Устанавливает поток in на System.in */ public static void toDefault() { StdStreams.setIn( System.in ); } }
Он включает в себя описание самого класса и нескольких его методов. На пустой закрытый конструктор не стоит обращать внимания - он служит лишь для того, чтобы какой-нибудь "умелец" не смог создать экземпляр класса SetIn оператором new (попытавшись, он получит от компилятора отказ). Чтобы экземпляр класса создавался автоматически, его методы должны быть статическими. Тогда при обращении к одному из них виртуальная машина Java сама загрузит объект класса SetIn и выполнит вызванный метод. Вторая цель, которая преследовалась при описании методов как static, - заставить пользователя всегда употреблять имена методов вместе с именем класса, а, если вы помните, ссылка на статические поля и методы класса возможна лишь при использовании полного имени, включающего имя класса, которому поля и методы принадлежат. Так что, если мы хотим переопределить поток in в другое место, то директива будет выглядеть примерно так:
FileInputStream s = new FileInputStream("SomeFile.dat"); ... SetIn.to(s);
Как видите, переназначение потока ввода читается как естественная фраза на английском языке, чего мы, собственно, и хотели добиться. Сам метод реализован элементарно: он обращается к классу StdStreams, о котором мы уже говорили, и устанавливает поток ввода вызовом метода StdStreams.setIn(). Теперь вы наверняка поняли, зачем мы объявили класс StdStreams с областью видимости package - он виден лишь для классов, которые расположены внутри того же самого пакета.
Но вернемся к классу SetIn. Второй метод сбрасывает последнее выполненное переназначение и устанавливает поток ввода в начальное положение на System.in. Вызов этого метода тоже похож на естественный английский язык:
SetIn.toDefault();
Аналогичным образом реализованы и классы перенаправления стандартного потока вывода (см. Листинг 3) и стандартного потока ошибок (см. Листинг 4)
Листинг 3. Создание класса перенаправления стандартного потока вывода
package Mitrich.utils; import java.io.InputStream; import java.io.OutputStream; /** * Данный класс служит для перенаправления * потока out */ public final class SetOut { /** * Предотвращает создание экземпляра класса */ private SetOut() { } /** * Перенаправить поток out на другой поток */ public static void to(OutputStream stream) { StdStreams.setOut(stream); } /** * Устанавливает поток out на System.out */ public static void toDefault() { StdStreams.setOut( System.out ); } }
Листинг 4. Создание класса стандартного потока ошибок
package Mitrich.utils; import java.io.InputStream; import java.io.OutputStream; /** * Данный класс служит для перенаправления * потока err */ public final class SetErr { /** * Предотвращает создание экземпляра класса */ private SetErr() { } /** * Перенаправить поток err на другой поток */ public static void to(OutputStream stream) { StdStreams.setErr(stream); } /** * Устанавливает поток err на System.err */ public static void toDefault() { StdStreams.setErr( System.err ); } }
Еще один важный для нас класс - Debug. Он выполняет роль рубильника, которым мы либо включаем, либо выключаем мониторинг. Как это делается, вы поймете из листинга 5. Конструкция
Debug.on();
включает возможность мониторинга, а
Debug.off
ее выключает. Проверить текущее состояние можно, вставив выражение, подобное
if(Debug.isOn()) ...
Так же как и предыдущие классы, Debug не допускает создания своих экземпляров оператором new, и все его методы описаны как static, чтобы их нельзя было употреблять в отрыве от имени класса (см. Листинг 5).
Листинг 5. Создание класса Debug
package Mitrich.utils; /** * Данный класс играет роль флага разрешения * или запрещения мониторинга */ public final class Debug { /** * По умолчанию трассировка выключена */ private static boolean onFlag = false; /** * Не дает создать экземпляр класса */ private Debug() { } /** * Возвращает состояние флага мониторинга * @return boolean */ public static boolean isOn() { return Debug.onFlag; } /** * Сбрасывает флаг состояния мониторинга */ public static void off() { Debug.onFlag = false; } /** * Устанавливает флаг состояния мониторинга */ public static void on() { Debug.onFlag = true; } }И вот мы вплотную подошли к основному классу нашей маленькой библиотеки, который, собственно, и выполняет мониторинг. Два его метода - err() и out() - пересылают данные соответственно в поток ошибок и поток вывода. Но, как показывает исходный текст (см. Листинг 6), эти операторы перегружены, благодаря чему они могут выводить в поток данные любого типа.
Листинг 6. Создание основного класса, выполняющего мониторинг
package Mitrich.utils; import java.io.PrintStream; /** * Данный класс сделан для удобства мониторинга данных. */ public final class TraceTo { static StdStreams streams = new StdStreams(); /** * Предотвращает создание экземпляров класса */ private TraceTo() { } /** * Выводит данные параметра value в поток ошибок * @param value char[] */ public static void err(char[] value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value byte */ public static void err(byte value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value char */ public static void err(char value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value double */ public static void err(double value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value float */ public static void err(float value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value int */ public static void err(int value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value long */ public static void err(long value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param msg java.lang.Object */ public static void err(Object value) { if(Debug.isOn()) streams.getErr().println(value.toString()); } /** * Выводит данные параметра value в поток ошибок * @param msg java.lang.String */ public static void err(String value) { if(Debug.isOn()) streams.getErr().print(value); } /** * Выводит данные параметра value в поток ошибок * @param value short */ public static void err(short value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Выводит данные параметра value в поток ошибок * @param value boolean */ public static void err(boolean value) { if(Debug.isOn()) streams.getErr().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value char[] */ public static void out(char[] value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value byte */ public static void out(byte value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value char */ public static void out(char value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value double */ public static void out(double value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value float */ public static void out(float value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value int */ public static void out(int value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value long */ public static void out(long value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param msg java.lang.Object */ public static void out(Object value) { if(Debug.isOn()) streams.getOut().println(value.toString()); } /** * Пересылает данные параметра value в поток вывода * @param msg java.lang.String */ public static void out (String value) { if(Debug.isOn()) streams.getOut().print(value); } /** * Пересылает данные параметра value в поток вывода * @param value short */ public static void out(short value) { if(Debug.isOn()) streams.getOut().println(value); } /** * Пересылает данные параметра value в поток вывода * @param value boolean */ public static void out(boolean value) { if(Debug.isOn()) streams.getOut().println(value); } }
Методы устроены элементарно. Все, что они делают, так это проверяют, включена ли отладка (мы уже акцентировали внимание на этом), и если так, то выводят данные методом println() класса PrintStream. Получается, что методы err() и out() - просто оболочки вокруг уже имеющихся перегруженных методов. Исключение составляют методы, выводящие данные типов String и Object. Для данных типа String применяется вывод методом print(), который не переводит курсор на следующую строку. Это удобно, если необходимо склеить несколько строк в одну или поместить вывод данных переменной непосредственно за текстовым комментарием. Если же хотите сделать перевод каретки, просто добавьте в конец строки символ " ". Для данных типа Object ситуация иная. Чтобы сделать информацию о выводимых данных полезной, ссылку на класс-аргумент следует привести к типу String. Для большинства классов это означает последовательную печать значений всех внутренних полей в виде строки.
Использование класса Trace ничем не отличается от использования уже описанных классов:
... TraceTo.err("Произошла ошибка с кодом "); TraceTo.err(errorCode); ...
Как видите, все просто и естественно. А чтобы проверить работу нашей библиотеки, попробуйте написать свое собственное приложение, которое бы выводило данные в стандартные или файловые потоки, включало и выключало мониторинг и т. д. Это прекрасная практика, которая поможет вам освоить технику использования мониторинговых классов