    Глава 8 

    Файлы ввода-вывода п потоки данных 

    Понятие потока данных (stream) используется для описания 
последовательности байтов. В программировании существуют ситуации, когда байты 
хранятся в определенном порядке, в котором они должны быть и прочитаны. 
Например, текст этой книги можно рассматривать как поток символов. Автор 
непрерывно писал символы слева направо, от первой до последней страницы. 
Читатель читает эти символы точно в том же порядке. 
    Другим примером потока данных служит запись на магнитной ленте. Лента — 
это длинная полоса материального носителя информации, на котором можно хранить 
последовательность байтов. По существу, это одномерная строка или поток 
значений в байтах. Если требуется записать байт данных в этот поток, ленту 
перематывают в начало и записывают туда значение байта. Затем лента немного 
передвигается и становится готовой к записи следующего байта. Он записывается 
в следующую позицию, затем лента опять передвигается. Процесс повторяется, 
пока все данные не будут записаны на ленту (в поток данных). 
    Допустим, поток байтов записан и готов к чтению. Для того чтобы прочитать 
его, необходимо перемотать ленту к первой позиции, с которой начиналась 
запись. Затем читается байт, лента передвигается и читается следующий байт. 
Все данные будут прочитаны точно в том же порядке, в каком были записаны. 
    Итак, поток данных — это записанная последовательность байтов, которую 
можно читать в определенном порядке. "Произвольный доступ", т.е. запись и 
чтение в произвольном порядке, исключен. Способ хранения потока данных таков, 
что запись и чтение могут производиться только в одном и том же порядке, как в 
примере с магнитной лентой. 
    Концепция потока данных очень общая. Упоминающуюся в предыдущем примере 
магнитную ленту можно заменить на любой физический носитель, и он также будет 
подходить под эту концепцию. Примером потока данных может служить файл на 
дискете. Поток данных записывается в виде последовательности байтов в секторах 
дискеты, и его можно прочитать в том же порядке, в каком он был записан. 

    239 

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

    Классы InputStream и Outputstream 

    Во всех приведенных примерах четко разграничивались процессы записи в 
поток данных и чтения из него. Нельзя осуществлять оба этих процесса 
одновременно. Класс JavaOutputStream описывает поток записываемых данных, 
класс JavalnputStream — поток читаемых данных. 
    В разное время одни и те же носители информации могут быть представлены 
потоками вывода и потоками ввода. Например, при записи файла в Java 
используется объект класса FileOutputStream. Данный объект связан с 
определенным файлом на локальном диске. Позднее, когда потребуется прочитать 
эти данные, можно связать с тем же файлом объект типа Fileln-putStream. 
    Классы OutputStream и InputStream — абстрактные. В них описаны и частично 
реализованы возможности обобщенного потока. Например, в классе InputStream 
объявлен абстрактный метод readQ. Этот метод возвращает очередной байт 
входного потока. В классе OutputStream определен абстрактный метод writeQ, 
которому передается очередной байт, подлежащий записи в поток данных. В 
классах OutputStream и InputStream объявлены методы, которые реализуют все 
классы потоков данных. 
    Но классы OutputStream и InputStream не связаны ни с каким определенным 
типом носителя информации. Во всех предыдущих примерах поток данных 
представлял собой последовательность байтов, хранящуюся в определенном месте, 
будь то дискета, сокет или массив байтов в памяти. Неважно, что является 
носителем информации, функциональные возможности всех потоков данных 
описываются абстрактными классами OutputStream и Input-Stream. 
    Поскольку классы OutputStream и InputStream являются абстрактными, их 
экземпляры никогда непосредственно не создаются. В пакете java.io объявлено 
несколько различных потоковых классов, порожденных от OutputStream и 
InputStream и реализующих их методы. Данные неабстрактные классы можно 
разделить на две категории. К первой относятся" те классы, которые можно 
назвать физическими потоковыми классами, то есть реализующими потоки данных 
для конкретных носителей информации. Во второй категории реализованы 'фШьтры 
потоков, которые можно рассматривать как потоки, инкапсулирующие другие потоки. 

    Физические классы типа InputStream 

    В физических потоковых классах для определенных носителей информации 
реализованы методы, определенные в классах OutputStream и Input-Stream. 
Например, в классе FilelnputStream реализован абстрактный метод 

    24О	Visual J++: основы программирования 

    readQ класса InputStream с вызовом соответствующей функции операционной 
системы, которая обращается к файлам на локальном диске. В реализации метода 
writeQ в классе FileOutputStream также вызывается функция операционной 
системы, которая записывает байты в файл на диске. В следующей таблице 
представлены различные физические классы типа Input-Stream и OutputStream и 
приведено описание каждого из них. 

    Класс     Описание 

    FilelnputStream	Класс типа InputStream, работающий с файлами на диске. 
    ByteArraylnptitStream      Класс типа InputStream, обеспечивающий доступ к 
отдельным элементам массива байтов. 
    SocketlnputStream	Класс типа InputStream, читающий байты из сокета TCP/IP. 
    PipedlnputStream	Класс типа InputStream, работающий с байтами, переданными 
классом PipedOutputStream. Байты, записанные в класс PipedOutputStream, 
автоматически буферизуются в классе PipedlnputStream. Работа фоновой программы 
чтения приостанавливается, пока данные не будут полностью буферизованы. 
    StringBufTerlnputStream     Похож на класс ByteArraylnputStream, только 
получает последовательность байтов из строки. ____ 

    Метопы класса InputStream 

    В классе InputStream объявлены несколько методов, облегчающих чтение 
потоков данных. Все неабстрактные классы, порожденные от InputStream, 
реализуют или переопределяют реализацию всех этих методов класса Input-Stream. 
    У метода readQ класса InputStream имеются три перегруженные версии, в 
каждой из которых читается один или более байтов из потока данных. 
Перегруженная версия метода readQ без параметров читает один байт из потока 
данных и возвращает его преобразованным в целое число, лежащее в диапазоне от 
0 до 255. Если в потоке данных больше не осталось байтов (прочитаны все 
байты), то метод возвращает -1. В приведенной ниже части кода показано, как 
использовать метод read() класса InputStream. Этот метод читает данные из 
передаваемого входного потока до тех пор, пока не будет прочитан определенный 
символ или пока не будет достигнут конец потока данных. 
    public boolean readToChar(InputStream is, int ch) { 
    for (int с = is.read(); с != -1 &S~c~!= ch; с = is.read()); return(c != -1 
? true : false); 
    } 
    Существуют еще две перегруженные версии метода read(), с помощью которых 
можно прочитать несколько байтов за раз в виде массива байтов, вместо того 
чтобы читать по одному символу, как в версии метода без 

    Глава 8. Файлы ввоаа-вывооа п потоки аанных 241 

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

    public void  readSomeBytes(InputStream  is,   byte[]   ab)    { //  
Получение  числа  байтов для  чтения из  потока, int  n  = is. read О; 
    //  Чтение  байтов  в  буфер, is.read(ab,   0,   n); } 

    Метод skipQ класса InputStream пропускает указанное число байтов потока 
ввода. Эти байты будут потеряны для данного сеанса чтения, и пока вы не 
закроете и повторно не откроете поток, их нельзя будет прочитать. У метода 
skipQ есть один параметр: число байтов, которые следует пропустить. 
    В классе InputStream объявлены неабстрактные методы, предназначенные для 
создания функций разметки в порожденных классах. Метод mark() (как, по крайней 
мере, следует из его названия) заставляет класс InputStream начать записывать 
изменения в буфер, чтобы впоследствии можно было вернуться к отмеченной 
позиции. Однако не во всех классах, порожденных от InputStream, можно получить 
такую возможность, так как на самом деле в реализации этого метода ничего не 
делается! (Только в классе Bufferedln-putStream реализована возможность 
перехода на отмеченную позицию, о чем будет рассказано в следующем параграфе.) 
    Для того чтобы указать, может ли класс типа InputStream поддерживать 
функцию перехода на отмеченную позицию, к классу InputStream добавлен метод 
markSupportedQ. Этот метод возвращает значение true, если объект типа 
InputStream поддерживает методы mark() и resetQ. В противном случае он 
возвращает значение false. 
    Метод resetQ дополняет метод markQ. Отметив позицию в потоке с помощью 
метода mark(), можно позднее вернуться к ней с помощью метода resetQ. 
Например, если требуется просмотреть три или четыре символа в потоке ввода и 
затем вернуться в исходную позицию, можно написать код следующего вида: 

    242	Visual J++: основы программирования 


    public byte[] lookAhead(InputStream is) { if (! is .markSupportedO ) 
return null; 
    is.mark();	/ byte ab[] = new byte[4];     ? is .read(ab) ;       4~"^ 
    is.reset ();	( return ab; 
    } 

    Метод availableQ класса InputStream возвращает число байтов, доступных для 
непосредственного чтения. Он особенно полезен для потоков данных, получаемых 
по сети, которые могут блокировать выполнение потока, вызвавшего метод readQ, 
пока данные не станут доступны. Метод available() возвращает число байтов, 
которые можно прочитать непосредственно, без блокирования вызвавшего его 
потока выполнения. Он возвращает -1, если файл закрыт. 
    Для того чтобы закрыть поток ввода, следует использовать метод closeQ, 
который закрывает поток данных и освобождает ресурсы, связанные с открытием файла. 

    Класс FilelnputStream 

    Класс FilelnputStream порожден от InputStream и служит для получения 
данных из файла локального диска. При создании объекта класса FilelnputStream 
конструктору класса передается либо имя файла, либо объект File, либо объект 
FileDescription. В результате объект класса FilelnputStream связывается с тем 
файлом, который указан конструктору. Ниже приведен пример кода инициализации, 
который открывает файл, заданный для чтения первым аргументом программы (как 
объект FilelnputStream): 

    import java.io.*; 
    public class MyProgram { FilelnputStream in; 
    public void main(String[) astrArgs) { if(astrArgs.length < 1) { 
    System.err.println("UsageError: run MyProgram class \ through  interpreter 
with \ arguments:\n <input-filename>"); System exit(-l); } 
    try { 
    in  = new FilelnputStream(astrArgs[I]); }   catch(FileNotFountException  
e)    { 
    System.err.println("Error:   file  not  found:   "  + astrArgs[1]}; 


    Глава 8. Файлы ввода-вывода и потоки панных	243 


    System.exit(-1); 
    } 
    // Действия над потоком ввода данных.. 
    try { 
    in.close (); } catch (lOException e) ( 
    // Обработка исключения... 


    Класс ByteAiTaylnputStream 

    Используя класс ByteArraylnputStream, можно преобразовать массив байтов в 
поток ввода данных. Этот класс предназначен для чтения данных из массива так, 
как если бы массив был потоком данных. Метод resetQ класса 
ByteArraylnputStream реализован таким образом, что он сбрасывает текущую 
позицию в потоке данных на начало массива. 
    При создании объекта класса ByteArraylnputStream конструктору объекта 
передается массив байтов, который требуется представить в виде потока. 
Существуют два конструктора, с помощью которых можно преобразовать в поток 
целый массив или какую-то его часть: 

    byte ab[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05);    // Данные. 
    InputStream isl = new ByteArraylnputStream(ab);      // Весь массив. 
InputStream is2 = new ByteArraylnputStream(ab, 2,3); 
    // Три элемента, начиная со второго. 


    Класс SocketlnputStream 

    Создав объект класса SocketlnputStream, можно представить данные 
установленного сетевого соединения в виде потока байтов. Класс 
SocketlnputStream находится в naKCTejava.net. В главе 9 описаны сетевые 
соединения и вообще работа в сети. 
    Поскольку оба класса, FilelnputStream и SocketlnputStream, порождены от 
класса InputStream, определяющего их общие возможности, то апплеты могут 
открывать файлы, загружаемые по URL. Поэтому апплет, открытый с HTML-страницы, 
в локальной системе может использовать тот же код, что и апплет, открытый 
через Internet: в обоих случаях открытые файлы представлены апплету в виде 
потоков ввода, что освобождает пользователя от волнений относительно среды их 
хранения. Если данные хранятся в локальной файловой системе, то для доступа к 
ним используется объект класса FilelnputStream. Если данные переданы через 
Internet посредством протоколов HTTP или FTP, то используется объект класса 
SocketlnputStream. В обоих случаях апплет рассматривает оба этих класса как 
класс InputStream и не различает, где находятся данные. 

    244	Visual J++: основы программирования 


    Класс PipedlnputStream 

    Объект класса PipedlnputStream буферизует байты, записанные в выходной 
поток канала обмена данными. Для такого потока данных создается фоновый поток 
управления. При записи в выходной поток канала обмена данными байт передается 
всем входным потокам, которые связаны с данным выходным потоком. Затем этот 
байт буферизуется потоком типа PipedlnputStream и выдается, когда объект 
класса PipedlnputStream вызывает метод readQ. Фоновые программы, которые 
читают из PipedlnputStream, когда в буфере нет данных, блокируются до тех пор, 
пока данные не будут записаны в соответствующий PipedOutputStream. В 
приведенном ниже примере апплет использует пару PipedlnputStrearn/ 
PjpgdOutputStream для передачи байтов между разными программными потоками. 

    import java.applet.*; import java.awt.*; import java.io.*; 
    class MyApplet extends Applet implements Runnable { PipedlnputStream 
m__iskeys; PipedOutputStream m_oskeys; double m_dblAverageKey; int 
m_nKeyCount; Thread m_thread; 
    public void init() { 
    m_osl«;ys = new PipedOutputStream(); try {v • 
    m_iskeys = new PipedlnputStreamlm oskeys); } catch (lOException ioe) {     
    ———T---—•— 
    // Обработка исключения... ) } 
    public void start () { 
    m_dblAverageKey = 0.0; 
    m_nKeyCount = 0; 
    m_thread = new Thread(this); 
    m_thread.start(); } 
    public void stop() { m_thread = null; } 
    public boolean keyDown(Event evt, int key) { try { 
    m_oskeys.write(key); } catch (lOException e) { 
    // Обработка исключения... 1 


    Глава 8. Файлы ввоаа-вывоаа и потоки аанных	245 


    System.out.println("Key was hit: " + key); return true; } 
    public void run() ( 
    int key; 
    while(null != m_thread) { try { 
    key = m_iskeys.read() ; } catch (lOException e) { 
    // Обработка исключения... } 
    m_dblAverageKey *= m_nKeyCount; m_dblAverageKey += key; m_dblAverageKey /= 
++m_nKeyCount; repaint (); 
    } ) 
    public void paint (Graphics g) { 
    FontMetrics fm = g.getFontMetrics(); 
    g.drawstring("Average key value: " + m_dblAverageKey, fm.getMaxAdvance() , fm.getHeight()); 
    ) } 


    Класс StringBuffeMnputStream 

    Класс StringBufferlnputStream аналогичен классу ByteArraylnputStream, но 
получает байты данных не из массива, а от объекта типа String. Поскольку со 
строками работать легче, чем с массивами байтов (потому что можно осуществлять 
их слияние и усечение), то и использовать класс StringBufferlnputStream легче, 
чем класс ByteArraylnputStream. Объекты обоих классов применяются, когда надо 
создать поток из байтов или символов, находящихся в памяти. 

    Классы ФИЛЬТРУЮЩИХ потоков 

    Второй тип классов, порожденных от класса InputStream, отличается от 
физических потоковых классов. Вместо физических носителей информации, 
ассоциированных с объектом класса InputStream, эти классы инкапсулируют другие 
потоки данных и используют их в качестве средств хранения. То есть объект 
такого класса является оболочкой потока данных. В классе Filterln-putStream 
имеется переменная /я, объявленная как protected, которая является объектом 
типа InputStream. Методы чтения класса FilteredlnputStream предназначены для 
чтения байтов из переменной in. 
    Сам класс FilterlnputStream фактически ничего не меняет в данных, 
поступающих из переменной in типа InputStream. Поэтому он не очень 


    246 Visual J++: основы программирования 

полезен. Имеется несколько классов, порожденных от FilteredlnputStream, 
которые меняют или как-то используют данные, полученные при помощи метода 
readQ класса FilterlnputStream. Метод readQ читает их из переменной in типа 
InputStream. В следующей таблице перечислены все включенные в пакет java.io 
классы, порожденные от класса FilterlnputStream и дано краткое описание 
каждого из них. 

    Класс	Описание

    BufferedlnputStream	Буферизует   входные  данные.   Данными   заполняется 
    внутренний буфер произвольной длины. Уменьшает количество обращений к 
диску путем хранения данных во внутреннем буфере. 
    DatalnputStream	Содержит методы readBoolean() и readlnt(), которые 
преобразуют поступающие байты данных в значения одного из встроенных типов. 
    LineNumberlnputStream     Пример простого входного фильтра-счетчика. 
Отслеживает число прочитанных или пропущенных символов новой строки. 
    PushbacklnputStream         Простой фильтр, который позволяет возвращать 
во входной поток один символ (т.е. вы можете прочесть символ из потока, 
вернуть его назад в поток и снова прочесть). Максимальное количество символов, 
которое можно поместить обратно в PushbacklnputStream,— один. 
    SequencelnputStream         Объединяет последовательность потоков ввода в один 
    общий поток. 

    Класс FilterlnputStream содержит лишь методы родительского класса 
InputStream. В классах, перечисленных в этой таблице, могут быть реализованы 
дополнительные public-методы. Так, в классе LineNumberlnputStream помимо 
методов, унаследованных от класса InputStream, имеются методы getLineNumberQ и setLineNumberQ. 

    Класс BufferedlnputStream 

    Класс BufferedlnputStream является примером буфера потока данных. 
Буферизованный входной поток никаким образом не меняет байты входного потока, 
инкапсулированного в переменной in. Вместо этого класс BufferedlnputStream 
расширяет функциональные возможности класса InputStream. 
    Входные данные хранятся в буфере — внутреннем массиве байтов. Основное его 
предназначение,— уменьшение количества обращений к устройству хранения при 
чтении отдельных байтов. Вместо того чтобы при вызове метода readQ запрашивать 
у инкапсулированного потока in один новый байт, класс BufferedlnputStream 
заполняет внутренний буфер сразу многими байтами из потока ввода. Это 
уменьшает общее количество обращений к основному носителю информации. Большое 
количество запросов одного байта заменяется, таким образом, на один запрос 
многих байтов. 
    Поскольку класс BufferedlnputStream, по существу, выполняет 
предварительную выборку такого количества байтов, которое помещается в его буфер, 

    Глава 8. Файлы ввгаа-вывооа и потоки ааииых	247 

в нем реализованы методы mark() и reset() класса InputStream. При вызове 
метода mark() происходит нечто похожее на помещение закладки в текущей позиции 
потока байтов. При вызове метода resetQ объект BufferedlnputStream 
"прокручивает назад" указатель текущей позиции чтения во внутреннем буфере до 
последней отмеченной позиции. Единственный целочисленный параметр метода 
mark() указывает буферу минимальное число байтов, которые требуется 
предварительно просмотреть. Если методу markQ передано число N, то до того, 
как будет стерта отметка, можно будет прочесть из буферизованного потока ввода 
N байтов. 

    Класс LineNumberlnputStream 

    Это еще один пример класса FilterlnputStream, который не модифицирует и не 
транслирует данные, поступившие из инкапсулированного потока ввода данных. Как 
и в классе BufferedlnputStream, метод readQ класса LineNum-berlnputStream 
возвращает почти те же самые данные, что и при прямом вызове метода readQ 
инкапсулированного класса InputStream. 
    На самом же деле имеется некоторое различие между байтами 
инкапсулированного потока ввода и байтами, выводимыми классом 
LineNumberln-putStream. Объект LineNumberlnputStream транслирует все возможные 
комбинации окончания строки в один символ перевода строки. Имеется три 
варианта окончания строки: одиночный символ возврата каретки '\г', одиночный 
символ перевода строки '\п' и, наконец, символ перевода строки, следующий 
непосредственно за символом возврата каретки. Любая из этих комбинаций 
автоматически транслируется объектом LineNumberlnputStream в одиночный символ 
перевода строки, когда байты проходят через него из инкапсулированного потока 
ввода в код вашей программы. 
    Класс LineNumberlnputStream подсчитывает число новых строк, которые он 
посылает. Метод getLineNumber() возвращает число прошедших через поток 
символов перевода строки. Метод setLineNumber() позволяет изменять текущее 
значение счетчика строк, устанавливая нужное вам значение. 
    Ниже приведена простая программа, которая для подсчета количества строк 
использует объект LineNumberlnputStream. Обычно данный объект вызывается с 
такой целью только в простейших ситуациях. 

    import java.io*; 
    public class LineCounter { 
    static LineNumberlnputStream m_istreamLineNum; 
    public static void main(String astrArgs[]) { if(1 != astrArgs.length) { 
    System.err.println("Usage error:\n LineCounter <input-file>") ; 
System.exit (-1); } try { 
    FilelnputStream is =new FilelnputStream(astrArgs[0]); m_istreamLineNum = 
new LineNumberlnputStream(is); 


    248	Visual J++: основы программирования 


    } catch (Exception e) { 
    System.err.println("Error:\n Opening input file" +astrArgs[0]); 
    System.exit(-1); } 
    try { 
    while (-1 != m_istreamLineNum.read()); } catch (Exception e) { 
    System.err.println("Error:\n Reading from the input\flie."); 
    } 
    System.out.println("Line numbering complete. Number of Lines is: " + 
    m_streamLineNum.getLineNumber()}; } 
    } 

    Класс LineNumberlnputStream можно считать простейшим счетчиком, связанным 
с инкапсулированным потоком ввода. Легко представить, как из класса 
I^lter^dlnputStream создаются счетчики других типов: класс 
ByteCoun-te7lnpuTsfream для подсчета фактического числа байтов, переданных 
методом read(); класс CharacterCounterlnputStream для подсчета количества 
различных символов, переданных методом readQ, и т. д. В следующем параграфе 
будет показано, как можно создать класс типа FiteredlnputStream, определяющий 
скорость чтения байтов из инкапсулированного потока ввода. Этот класс можно 
использовать, в частности, для проверки того, насколько быстро загружается 
определенный файл 

    Класс PushbacklnputStream 

    Класс PushbacklnputStream, подобно классу BufferedlnputStream, не 
транслирует и не модифицирует данные, хранящиеся в инкапсулированном потоке 
ввода. Его дополнительные возможности заключаются в том, что он позволяет 
"вернуть назад" поток на один символ. Это означает, что можно осуществить 
предварительный, щюсмотр одного символа в потоке ввода. Метод unreadQ (чье имя 
означает, что его действие противоположно действию метода readQ класса 
PushbacklnputStream) принимает символ и хранит его внутри объекта данного 
класса. Если сразу же после вызова метода unreadQ вызвать метод readQ, то 
последний вернет тот символ, который был передан методу unreadQ. 

    Класс DatalnputStream 

    В классе DatalnputStream реализовано несколько различных методов чтения, 
каждый из которых способен читать из инкапсулированного потока ввода in 
значения одного из встроенных типов. Этот класс очень полезен, например, при 
чтении из потока ввода значений типа long или String. Без использования 
оболочки DatalnputStream вам пришлось бы самим выстраивать байты в корректную 
цепочку. В следующей таблице перечислены различные типы данных, которые можно 
прочитать из потока ввода, и методы, с помощью которых это делается. 

    Глава 8. Файлы ввоаа-вывода п потоки ааниых	249 


    Встроенный тип     Метопы класса DatalnputStream и их описание 

    boolean	readBoolean(). Читает один байт из потока ввода. Нулевой байт 
означает false, остальные означают true. 
    byte	readByte(). От простого метода read() отличается только тем, что 
возвращает тип byte, а не преобразует его в тип int. readUnsignedByte() 
возвращает такое же значение, как и метод read(). Отличается тем, что в конце 
потока ввода не возвращает значение -1. Как будет показано далее, вместо этого 
при достижении конца потока ввода осуществляется выдача исключения EOFException. 
    char	readChar(). Возвращает один символ в формате Unicode (16 битов — 2 
байта). Первый прочитанный байт становится старшим байтом, а второй — младшим. 
Этот метод блокирует вызывающий его программный поток (приостанавливает его 
дальнейшее выполнение) на время чтения обоих байтов. 
    double	readDouble(). Читает из входного потока ввода 8 байтов значения 
типа double. Метод блокирует программный поток, пока не будут прочитаны все 8 байтов. 
    float	readFloat(). Читает 32-битовое число типа float. Метод блокирует 
программный поток, пока не будут прочитаны все 4 байта. 
    short	readShort(). Читает из потока ввода 16-битовое целое со знаком. 
Метод блокирует программный поток, пока не будут прочитаны оба байта. 
    readUnsignedShort(). Читает те же 16 битов, но загружает их в 32-битовую переменную. 
    int	readlntQ. Читает из инкапсулированного потока ввода 32-битовое целое 
со знаком. Метод блокирует программный поток, пока не будут прочитаны 4 байта. 
    long	readLongQ. Читает из инкапсулированного потока ввода 64-битовое целое 
со знаком. Метод блокирует программный поток, пока не будут прочитаны все 8 байтов. 
    String	readUTF(). Читает из инкапсулированного потока ввода строку. При 
записи в поток строки преобразуются в формат UTF. (Обычно их записывают с 
использованием объектов класса 
    _____________DataOutputStream, обратного классу DatalnputStream.)____ 

    Следует отметить, что любой из описанных методов после достижения конца 
потока ввода выдает исключение EOFException, сообщающее о конце файла. Метод 
readQ, который класс DatalnputStream наследует от родительского класса 
FilterlnputStream, действует так же, как и в остальных классах типа 
InputStream. Он не выдает исключение EOFException, если был вызван, когда 
инкапсулирующий поток ввода окончился. Вместо этого метод возвращает значение -1. 
    В главе 12 будет рассказано о том, как создать обобщенные классы 
ObjectlnputStream и ObjectOutputStream, порожденные соответственно от 
DatalnputStream и DataOutputStream. Эти классы могут читать и записывать в 
потоки любые существующие объекты. 

    25О Visual J++: основы программирования 


    Класс SequencelnputStream 

    Класс SequencelnputStream является для потоков ввода чем-то вроде 
"веерного" входного фильтра. Он может быть связан не с одним потоком ввода, а 
со многими. Класс SequencelnputStream преобразует все эти потоки ввода в 
единый поток с помощью своего метода readQ. Например, при объединении двух 
потоков ввода в один последовательный поток метод readQ класса 
SequencelnputStream, вызванный несколько раз, сначала вернет все символы 
первого потока ввода, а затем — все символы второго потока ввода. Определить, 
из какого InputStream получен конкретный байт, невозможно. 
    Ниже представлена программа, очень похожая на приводимую ранее программу 
LineCounter, в которой производился подсчет числа строк в нескольких файлах. 

    import java.io.*; import Java.util.*; 
    public class MultiFileLineCounter { 
    public static void main(String astrArgs[]) { SequencelnputStream sis; try { 
    // Аргументами служат файлы, которые надо // открыть и в которых надо 
пересчитать строки. Vector vectFiles = new Vector (); for(int ii=0; 
ii<astrArgs.length; ii++) vectFiles.addElement(new 
    FilelnputStream(astrArgs[ii])); 
    sis = new SequencelnputStream(vectFiles.elements()); } catch (Exception e) { 
    System.err.println("Problem opening input file(s)."); System.exit(-1); } 
    // Создание счетчика строк для SequencelnputStream. LineNumberlnputStream 
lis = new LineNumberlnputStream(sis); 
    try { 
    while(-1 != lis.read()); } catch (Exception e) { 
    System.err.println("Problem reading from input file(s)."); 
    System.exit(-1); } 
    System.out.println("Total count of lines is " + 
    lis.getLineNumber()); } } 

    В классе SequencelnputStream есть два конструктора. Первому передается с 
перечень потоков ввода. В нашем примере это осуществляется с помощью ' метода 
Vector.elements(), который создает список всех элементов объекта Vector. После 
этого все потоки из списка объединяются в единый поток. 

    Глава 8. Файлы ввода-вывода и потоки ааиных 251 

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

    Lie почки потоков FilteredlnputStream 

    Из приведенного выше примера класса MultiFileLineCounter следует очень 
интересное заключение: потоки можно объединять в своеобразные цепочки. В этом 
примере поток LineNumberlnputStream используется для того, чтобы 
инкапсулировать фильтрующий поток ввода другого типа: SequencelnputStream. 
Тот, в свою очередь, инкапсулирует поток, который фактически связан с 
носителем информации, в данном случае с файлом (FilelnputStream). 
    Поскольку в конце концов все фильтрующие потоки порождены от класса 
InputStream, то каждый такой поток можно использовать для инкапсуляции другого 
фильтрующего потока ввода, который может инкапсулировать следующий поток, и т. 
д. Концепция инкапсуляции объектов класса FilterlnputStream друг другом 
проиллюстрирована на рис. 8.1. Можно представить, что с каждым уровнем 
инкапсуляции прибавляются новые функциональные возможности по управлению 
содержимым необработанных потоков ввода. Это показано в программе 
MultiFileLineCounter. Для инкапсуляции нескольких различных объектов типа 
FilelnputStream используется объект SequencelnputStream, который преобразует 
их в единый поток ввода. Объект LineNumberlnputStream, в свою очередь, 
инкапсулирует объект SequencelnputStream, позволяя отслеживать ключевые 
характеристики объединенных файлов. (Конечно, количество строк в файле можно 
только с большой натяжкой считать "ключевой характеристикой", но эту концепцию 
нетрудно распространить на более важные п араметры.) 

    InputStrcam		Filter Input Stream		Filter Input Stream	U--	Filter Input Stream 
    ———— >		—— *• 
    IOTOK, связанный ; устройством хранения		Инкапсулирует поток 
InputStream		Инкапсулирует            Инкапсулирует первый поток              
предыдущщидцто FilterlnputStream         FilterlnputStream 

    Рис. 8.1. Цепочка потоков FilterlnputStream 


    252 Visual J++: основы программирования 


    Пользовательские потоки FilterlnputSfream 

    Конечно, каждый пользователь может создать собственные фильтрующие потоки 
ввода, чтобы трансформировать содержимое потоков способом, не реализованным 
классами FilterlnputStream, которые содержатся в пакете java.io. Имеются три 
основные операции, которые фильтрующие потоки производят над 
инкапсулированными потоками ввода: 
    1.   Измерение характеристик данных потока ввода. Например, поток 
LineNumberlnputStream определяет количество строк, переданных 
инкапсулированным потоком ввода. 
    2.   Преобразование байтов потока ввода или изменение отдельных байтов 
данных с помощью некоторых методов. Пример будет приведен ниже. 
    3.   Трансляция данных потока ввода. В противоположность трансформации 
отдельных байтов, трансляция в FilterlnputStream изменяет тип данных. 
Например, методы readXXX() класса DatalnputStream, такие как read-BooleanQ и 
readDoubleQ, действительно преобразуют тип данных из набора байтов в более 
сложные структуры. 
    Разуяи§^еукзу^1ПЁОнсфоЁма^ией, (^тй^ттШ^М^Р^слящей (translation) не 
"только семантическая. Когда происходит только трансформация одних байтов 
данных в другие, результатом является поток байтов. При трансляции байтов в 
некоторый другой тип, например многобайтовое целое, результат нельзя передать 
объекту FilterlnputStream, поскольку он больше не является потоком байтов. 
    Следующий пример поможет понять эту разницу. Пусть имеется объект класса 
RatedlnputStream, измеряющий скорость чтения байтов из инкапсулированного 
потока ввода (код такого класса представлен в следующем примере). При попытке 
инкапсулировать объект DatalnputStream объектом RatedlnputStream возможности 
фильтрации первого становятся недоступными! Поскольку RatedlnputStream может 
проверять только данные, проходящие через его метод readQ, любые данные, 
прочитанные с помощью метода readXXXQ класса DatalnputStream, подсчитаны быть 
не могут. 
    Транслирующий фильтр желательно устанавливать в начале или конце потока. С 
другой стороны, трансформирующий и измеряющий фильтры можно использовать в 
любом порядке. В рассматриваемом примере с классами RatedlnputStream и 
DatalnputStream необходимо, чтобы DatalnputStream инкапсулировал 
RatedlnputStream, но ни в коем случае не наоборот. 
    Ниже приведен код для создания уже упоминавшегося класса RatedlnputStream. 
Этот класс регистрирует общее число байтов и интервал времени, прошедший с 
момента первого вызова метода readQ. В интерфейс класса включен метод 
getRateQ, с помощью которого можно узнать текущую скорость чтения данных из 
потока, измеряемую в байтах в секунду. Метод resetRate() очищает счетчик до 
следующего запроса одного байта методом readQ. 

    Глава 8. Файлы ввопа-вывопа и потоки оаииых	253 


    import java.io.*; 
    public class KatedlnputStream extends FilterlnputStream { I/ Конструктор 
принимает ссылку на инкапсулированный // InputStream. public 
RatedlnputStream(InputStream in) { 
    super(in); 
    m_dStartTime = -1.0; 
    m_dByteCount = 0.0; } 
    // Метод readO увеличивает m_dByteCount на единицу при 
    // каждом вызове. Если начальное время еще не записано, 
    // то записывается сейчас, 
    public int read() { 
    if(-l == (int )m_dStartTime) 
    m_dStartTime = System.currentTimeMillis(); try { 
    m_dByteCount++; return in. readO ; } catch (lOException e) { 
    return -1; } } 
    // Метод getRateO возвращает значение (типа double) // скорости чтения в 
байтах в секунду, public double getRateO { if(m_dStartTime <= 0.0) return 0.0; 
    return m_dByteCount / (m_dStartTime * 1000) ; } 
    public void resetRateO { 
    m_dStartTime = -1.0; 
    m_dByteCount = 0.0; } 
    // Private-переменные состояния, private double m_dStartTime; private 
double m_dByteCount; } 

    Единственный метод, который нуждается в переопределении при реализации 
пользовательского фильтра, — это метод read(), предназначенный для чтения 
одного байта. В классе FilterlnputStream уже реализовано несколько 
перегруженных версий метода read(), предназначенных для считывания данных в 
массивы байтов (read(byte[ ]) и read(byte[ ], offset, length)). В этих методах 
осуществляется последовательный вызов метода read(), читающего один байт. 
Конечно, при необходимости можно реализовать и свои версии 

    254	Visual J++: основы программирования 

    метода readQ для чтения массивов байтов. Такой способ наиболее эффективен 
при оптимизации чтения большого количества байтов. 

    Класс OutputStream 

    Поток вывода — это поток байтов, в который можно записывать, в 
противоположность потоку ввода, из которого можно читать. Когда открывают файл 
для чтения, используют поток ввода. Когда открывают файл для записи, 
используют поток вывода. Классы InputStream и OutputStream являются 
абстрактными, методы read() и writeQB них не реализованы. 
    Итак, самым важным методом класса OutputStream является метод write(). 
Метод writeQ класса OutputStream симметричен методу readQ класса Input-Stream. 
Метод writeQ в качестве параметра принимает один байт (преобразованный в 
целое). При вызове этого метода байт добавляется к потоку, и "текущий 
указатель" потока сдвигается на одну позицию, так что в поток можно записывать 
следующий байт. 
    Имеются две перегруженные версии метода writeQ, как и две перегруженные 
версии метода readQ класса InputStream. Обе они позволяют записывать 
одновременно более одного байта в поток, в котором все байты последовательно 
хранятся в массиве байтов. Ниже приведен синтаксис двух перегруженных версий 
метода writeQ, симметричных двум перегруженным методам read() класса 
InputStream:	- • -— . 

    public  void write(byte  ab [ ])   throws   lOException; 
    public void write(byte  ab[],   int offset,   int  length)throws   lOException; 

    Оставшиеся методы класса OutputStream — flushQ и closeQ. Метод flushQ 
используется для принудительной записи данных на носитель. Например, метод 
flushQ класса BufferedOutputStream автоматически записывает содержимое 
внутреннего буфера объекта в инкапсулированный поток вывода, даже если буфер 
еще не полон. Метод flushQ не имеет параметров. В различных классах типа 
OutputStream реализованы различные перегруженные версии методов writeQ и flush(). 
    Метод closeQ закрывает инкапсулированный поток вывода. После того как 
поток вывода закрыт, любой вызов метода write() или flushQ обязательно 
приведет к выдаче исключения lOException. 

    Классы типа OutDiitStream 

    В приведенной ниже таблице перечислены различные типы существующих классов 
потоков вывода и даны их краткие описания. Представленные в этой таблице 
классы эквивалентны классам потоков ввода соответствующих типов. Поэтому 
буферизованный поток вывода действует так же, как аналогичный поток ввода, 
только в "обратном направлении". Другими словами, объект BufferedOutputStream 
содержит внутренний буфер, который заполнятся при вызове перегруженного метода 
writeQ. Когда внутренний буфер заполняется, все данные за один раз 
записываются в инкапсулированный 

    Глава 8. Файлы ввоаа-вывоаа и потоки панных	255 

поток вывода. Это очень похоже на то, что делает буферизованный поток 
ввода, только осуществляется в обратном направлении. Не у всех потоков ввода 
имеются соответствующие им потоки вывода. Например, у класса 
TLineNumberlnputStream в пакете java.io нет соответствующего класса 
LineNumberOutputStream. Но это не мешает созданию пользовательского класса 
LineNumberOutputStream, порожденного от FilteredOutputStream. В большинстве 
случаев именно так и делается, если требуется создать поток вывода, 
функционально эквивалентный потоку ввода. 
    Класс     Описание 

    OutputStream	Абстрактный родительский класс для всех потоков вывода. В нем 
определены методы write() и flush(), хотя отсутствует их реализация. 
    ByteArrayOutputStream       Последовательно записывает байты в массив байтов. 
    FileOutputStream	Записывает данные в локальный файл. При создании ему 
необходимо передать имя файла, объект типа File или объект типа File Descriptor. 
    SocketOutputStream	Передает    байты    в    сетевое    соединение.    Объект 
    SocketlnputStream на другом конце сетевого соединения выполняет метод 
read() и получает байты в том же порядке, в котором они были записаны. 
    PipedOutputStream	Связывается непосредственно с одним или несколькими 
объектами PipedlnputStream в пределах одной ВМ. При записи в объект 
PipedOutputStream байты сразу передаются любому связанному с ним объекту PipedlnputStream. 
    FilterOutputStream	Родительский класс для всех фильтрующих потоков вывода. 
При записи в него осуществляется вызов инкапсулированного метода write() 
класса OutputStream. Этот класс может трансформировать и транслировать поток 
байтов, а также измерять его количественные характеристики таким же образом, 
как это делает класс FilterlnputStream. 
    BufferedOutputStream         Простой класс типа FilterOutputStream. 
Представляет собой буфер для байтов, записываемых в него. Когда буфер 
заполняется, все байты одновременно передаются в инкапсулированный объект 
OutputStream. Это приводит к значительному уменьшению числа вызовов метода write(). 
    DataOutputStream	Транслирующий класс типа FilterOutputStream, аналогичный 
классу DatalnputStream. Методы writeBoolean(), writelnt() и writeUTF() 
записывают данные в инкапсулированный поток OutputStream в виде 
последовательности байтов. Записанные данные можно затем прочитать в потоке DatalnputStream. 

    256 Visual J++: основы программирования 


    Анализ потоков InputStream 

    Анализ данных потока InputStream является разновидностью операции 
трансляции. Разница между ними состоит в том, что при трансляции тип 
содержимого потока ввода известен заранее. Класс DatalnputStream не 
анализирует свой инкапсулированный поток ввода. Поэтому ему следует сообщить, 
данные какого типа необходимо прочесть. Объекту DatalnputStream явно 
указывается, каким методом он должен воспользоваться: read-BooleanQ, readlntQ 
или readUTFQ. Значит, вы заранее должны знать тип получаемых данных. 
    В качестве примера использования класса DatalnputStream можно рассмотреть 
процесс чтения потока байтов, начинающегося с целого числа, которое 
показывает, сколько строк следует прочесть. Код такого примера выглядит 
следующим образом: 

    public String[] readStrings(InputStream is) throws lOException { 
    DatalnputStream dis = new DatalnputStream(is); 
    int nStrings = dis.readlnt(}; 
    String[] astr = new String[nStrings]; 
    for(int ii=0; ii<nStrings; ii + + ) astr[ii] = dis.readUTF(); 
    return astr; } 

    Но что если формат потока ввода заранее не известен (например, когда надо 
анализировать файл исходного текста Java)? Синтаксис известен заранее, но 
неизвестно, что следует читать. Более того, файл исходного текста Java записан 
в ASCII-символах, поэтому прочитать из него, скажем, строки не удастся, так 
как класс DatalnputStream может читать строки только в формате UTF (это 
формат, в котором метод DataOutputString.writeUTFf)" записывает свои данные). 
    Для таких ситуаций в пакете java.io предусмотрен класс StreamTokeni/er. 
Объект этого класса принимает поток ввода и разбивает его на лексемы (tokens). 
Лексема — это блок данных определенного типа. Объект StreamTo-kenizer может 
читать и распознавать строки и числа из потока ввода, состоящего из 
ASCII-символов. Более того, он может определить, какой тип лексемы читается, 
поэтому при его использовании не обязательно знать точную структуру потока 
ввода, который необходимо проанализировать. 
    Итак, разница между трансляцией и анализом состоит в том, что при анализе 
точная структура потока ввода заранее не известна. При трансляции эта 
структура известна, поэтому можно сообщить фильтру, такому как класс 
DatalnputStream, каким будет тип следующих считываемых данных. 
    Класс StreamTokenizer порожден не от FilterlnputStream, поэтому в нем 
отсутствует метод readQ. Вместо этого данные потока ввода читаются в виде 
гюследовательности лексем строкового или числового типа. Метод nextTo-kenQ 
класса StreamTokenizer возвращает константу, сообщающую тип следующей лексемы, 
читаемой из InputStream. 

    Глава 8. Файлы ввша-вывооа и потоки ааиных	257 

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

    Тип константы     Описание 

    TT_NUMBER         Прочитана лексема-число. Значение числа хранится в 
переменной nval класса StreamTokenizer. 
    TT_WORD	Прочитана лексема-строка. Значение строки хранится в переменной 
sval класса StreamTokenizer. 
    TT_EOL	Достигнут конец строки. 
    TT_EOF                   Достигнут конец файла. 

    <значение>	Любое другое значение, возвращенное методом NextToken(); оно 
является фактическим ASCII-значением обычного символа. Обычные символы не 
являются частью слова или числа. 
    Для того чтобы прочитать файл, состоящий из слов и чисел, разделенных 
пробелами, надо использовать код, подобный этому: 

    public void parselnputStream(InputStream is) { 
    StreamTokenizer st = new StreamTokenizer (is); int nTokenType; 
    while(nTokenType = st.nextToken ()) != StreamTokenizer.TT_EOF) { 
switch(nTokenType) ( 
    case StreamTokenizer.TT_EOL: doSomethingWithEOL(); break; 
    case StreamTokenizer.TT_WORD: 
    doSomethingWithString(st.sval) ; break; 
    case StreamTokenizer.TT^NUMBER: 
    doSomethingWithDouble(st.nval) ; break; 
    ) ) } 

    Объекты класса StreamTokenizer легко конфигурировать. При помощи множества 
методов класса StreamTokenizer можно указать, какие символы надо включить в 
лексему TT_WORD, какие символы считать разделителями, должен ли объект 
StreamTokenizer сообщать о конце строки и т. д. 
    Кроме того, класс StreamTokenizer может автоматически анализировать и 
игнорировать различные типы комментариев. Можно указать, должен ли 
StreamTokenizer игнорировать комментарии языка С, отмеченные разделяющими 
символами "/*" и "*/", или комментарии C++, начинающиеся символами "//" и 
продолжающиеся до конца строки. 

    258	Visual J++: основы программирования 

    Между лексемами располагаются пробелы, или разделители. Объект 
StreamTokenizer рассматривает символы пробелов как незначащие и отбрасывает 
их, поэтому пробелы никогда не входят в состав лексем. Они используются лишь 
при обозначении начала и конца лексем TT_WORD и TT_NUMBER. По умолчанию при 
создании объекта StreamTokenizer все символы с кодами от 0 до 32 считаются 
пробелами. Используя метод whitespaceCharsQ класса StreamTokenizer, можно 
добавить к этому списку новый символ пробела. В качестве первого и второго 
параметров этого метода указываются первый и последний символы из диапазона 
значений ASCII. Все символы, заключенные между ними, объекты StreamTokenizer 
будут считать пробелами. Если анализируется файл, который содержит ряд чисел, 
разделенных запятыми, можно добавить в список разделителей запятую, как это 
сделано в следующем коде: 

    public int[] parseCommaDelimitedNumbers(InputStream is) { int anRet[] = 
new int[10]; int nNumbers = 0; 
    StreamTokenizer st = new StreamTokenizer(is); is.whitespaceChars(',', ','); 
    while (StreamTokenizer .TT__NUMBER == st.nextToken()) ( if(nNumbers == 
anRet.length) { 
    int an[] = new int[anRet.length*2]; System.arraycopy(anRet, 0, an, 0, 
anRet.length); anRet = an; } 
    anRet[nNumbers++] = (int)st.nval; } 
    int an[] = new int[nNumbers]; 
    System.arraycopy(anRet, 0, an, 0, nNumbers); 
    return an; 
    } 

    По умолчанию объект StreamTokenizer при создании автоматически 
устанавливается на анализ чисел. Это происходит вследствие того, что 
конструктор класса StreamTokenizer содержит вызов метода parseNumbers(), 
который указывает объекту, что ему надо анализировать числа типа double. 
Единственным способом указать объекту StreamTokenizer, что ему не надо 
анализировать числа, является вызов метода clearSyntaxQ, который делает все 
символы ASCII разделителями, не имеющими специального значения. После вызова 
этого метода необходимо задать новый синтаксис, вызывая методы wordCharQ, 
ordinaryCharQ, whitespaceCharQ, commentCharQ и quoteChar(). Это самый общий 
способ изменить поведение объекта StreamTokenizer. Нельзя удалить символ из 
списка символов, используемых для обозначения слов, не вызвав метода 
clearSyntaxQ и не воссоздав состояние объекта StreamTokenizer при помощи 
перечисленных методов. 
    Аналогично тому, как указываются символы-разделители при помощи метода 
whitespaceChars(), можно определять обыкновенные символы при 

    Глава 8. Файлы ввода-вывооа п потоки ааиных	259 

    помощи методов ordinaryCharQ и ordinaryCharsQ. Метод ordinaryCharQ 
добавляет в список отдельный символ, в то время как метод ordinaryChars() 
добавляет целый ряд символов. 
    Точно так же символы, входящие в состав слов, можно указать при помощи 
методов wordCharQ и wordsCharQ. Метод wordCharQ добавляет в список 
распознаваемых символов один символ, а метод wordsCharQ — ряд ASCII-символов. 
    Весь текст, стоящий между кавычками, читается объектом StreamTo-kenizer 
как отдельная лексема типа TT_WORD, даже если в этом тексте имеются пробелы 
или цифры. Метод quoteChar() класса StreamTokenizer используется для того, 
чтобы добавить символ в список символов, обозначающих кавычки. По умолчанию 
при создании объекта StreamTokenizer в список символов, обозначающих кавычки, 
включаются одинарные '\" и двойные '\'" кавычки. 
    Аналогично, все символы, находящиеся между двумя символами комментария, 
рассматриваются как комментарий. Метод commentCharQ класса StreamTokenizer 
добавляет в список известных символов комментария новый символ. Помимо этого 
можно настроить объект StreamTokenizer на распознавание символов комментария 
языков С ("/*" и "*/") и C++ ("//"). Если надо игнорировать все символы между 
"/*" и "*/", то используется метод slashStarCommentsO, которому передается 
значение true. Если же эти символы комментария должны анализироваться как и 
прочие символы, то данному методу необходимо передать значение false. Методу 
slashStarCommentsQ передается значение true, если надо игнорировать все 
символы, стоящие после символов "//", вплоть до конца строки. Если передать 
ему значение false, то он будет анализировать эти символы комментария так же, 
как и другие символы. 

    Пример объекта StreamTokenizer 

    В примерах, разработанных фирмой Sun и включенных в Visual J++, имеется 
пример использования объекта StreamTokenizer для анализа файла. Этот пример 
является частью апплета WireFrame, который демонстрирует трехмерную каркасную 
модель и позволяет пользователю вращать ее относительно осей с помощью мыши. 
    Один из параметров апплета WireFrame называется "model". Этот параметр 
содержит URL файла, описывающего трехмерную каркасную модель. Файл состоит из 
серии дескрипторов, каждый из которых включает одну строку. Такие дескрипторы 
бывают двух типов. Дескриптор первого типа описывает ту или иную вершину в 
трехмерной модели и выглядит следующим образом: 

    v  X  Y Z 

    Переменные X, Y, Zoпpeдeляют точку в трехмерном пространстве. Между 
символом V и тремя числами расположено несколько пробелов, а вся строка 

    26О	Visual J++: основы программирования 

    заканчивается символом конца строки (символом перевода строки). Апплет 
Wire Frame присваивает этим вершинам индексы; первая вершина имеет индекс 1, 
вторая — 2 и т. д. 
    Дескриптор второго типа определяет связи между Л^умя или большим 
количеством вершин модели. Эти строки выглядят следующим образом: 

    (f  или   fo  или  1)    vl   v2   [v3   [v4   [...]]] 

    Переменные vl, v2... — это номера вершин. Строка начинается с одного из 
трех символов: "f, "fo" или "1". Разницы между.ними нет никакой, и описываемые 
ими строки анализируются программой WireFrame одинаковым образом. (Возможно, 
на ранней стадии разработки этого апплета для обозначения трех разных типов 
дескрипторов использовались три разные строки, но позднее их объединили в 
один.) Последовательность номеров вершин означает, что следует проводить линию 
от вершины vl через вершину v2 и т.д. Как и строки, описывающие вершины, 
данные строки заканчиваются символами перевода строки. На рис. 8.2 показано 
окно апплета WireFrame с изображением куба. Куб описывается следующим файлом: 

    V	0	0	0 
    V	1	0	0 
    V	1	1	0 
    V	0	1	0 
    V	0	0	1 
    V	1	0	1 
    V	1	1	1 
    V	0	I	1 
    f	1	2	3 
    f	5	6	7 
    1	1	5 
    1	2	6 
    1	3	7 
    1	4	8 

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

    Глава 8. Файлы ввоаа-вывооа п потоки оаниых	261 


    Рис. 8.2. Окно апплета Wire Frame с изображением куба 

    Апплет WireFrame хранит каркасные модели в объекте класса Model3D. 
Конструктор класса ModeOD в качестве своего единственного параметра принимает 
поток ввода. Созданный объект ModeOD анализирует поток ввода, используя 
StreamTokenizer, и данные из потока ввода используются для инициализации 
внутренних переменных объекта ModeOD. Ниже приведен пример конструктора класса 
ModeOD, анализирующего файлы модели: 

    Model3D   (InputStream is)   throws   lOException,   FileFormatException   
{ this(); 
    StreamTokenizer  st  =  new  StreamTokenizer(is); 
st.eolIsSignificant(true); st.commentchar('#'); scan: 
    while   (true)    { 
    switch (st.nextToken()) ( 
    9	default: 
    1	•—•-  •  - . 
    ч	break scan; 
    case StreamTokenizer.TT_EOL: 
    break/case StreamTokenizer.TT_WORD: if ("v".equals(st.sval)) { 
    double x=0, y=0, z = 0; 
    if(st.nextToken() == StreamTokenizer.TT NUMBER) { 


    262 Visual J++: основы программирования 


    х = st.nval; 
    if(st.nextTokenO == StreamTokenizer.TT_NUMBER)( 
    у = st.nval; 
    if (st.nextToken () =StreamTokenizer.TT__NUMBER) { z = st.nval; 
    addVert((float) x, (float) y, (float) z); while (st.ttype != 
StreamTokenizer.TT_EOL && st.ttype != StreamTokenizer.TT_EOF) st.nextToken(); 
    } else if ("f".equals(st.sval) || "fo".equals(st.val) || 
"1".equals(st.sval)) { int start = -1; int prev = -1; int n = -1; while (true) 
if (st.nextTokenO == 
    StreamTokenizer.TT_Number) ( n = (int) st.nval; if (prev >= 0) 
    add(prev - 1, n - 1) ; if (start < 0) start = n; prev = n; } else if 
(st.ttype == '/') 
    st.nextToken (); else 
    break; if (start >= 0) 
    add(start - 1, prev - 1); if (st.ttype != 
    StreamTokenizer.TT_EOL) break scan; } else ( 
    while (st.nextTokenO ! = 
    StreamTokenizer.TT_EOL && st.ttype != StreamTokenizer.TT EOF); 
    } } } 
    is.close(); 
    if (st.ttype != StreamTokenizer.TT_EOF) throw new 
FileFormatException(st.toString()); } 


    Глава 8. Файлы ввопа-вывооа n потоки оанных	263 


    Резюме 

    Поток данных является абстрактным понятием, описывающим последовательность 
байтов. Читать из него и записывать в него данные можно только в определенной 
последовательности. Классы InputStream и OutputStream пакета java.io являются 
абстрактными классами, описывающими обобщенные потоки. Когда поток открывается 
для чтения, ему соответствует поток ввода InputStream. Когда поток открывается 
для записи, ему соответствует поток вывода OutputStream. Классы InputStream и 
OutputStream представляют собой симметричные описания различных свойств потока 
данных. Классы, порожденные от InputStream и OutputStream, используются для 
связи абстрактного потока с конкретными носителями информации. Например, класс 
FilelnputStream позволяет читать байты из локальных файлов. Класс 
FileOutputStream позволяет записывать байты в локальные файлы. Класс 
FilterlnputStream инкапсулирует другие потоки ввода и может транслировать или 
трансформировать их, а также измерять их количественные характеристики. 
Аналогично, F ilterOutputStream позволя ет транслировать или трансформировать 
инкапсулированные потоки вывода. 
    При анализе текстовых файлов формата ASCII вместо классов фильтрующих 
потоков используется класс StreamTokenizer. Задача анализа существенно 
отличается от задач трансляции или трансформации, поэтому для ее решения 
требуется совершенно другой объект. В ходе анализа поток ввода разбивается на 
ряд лексем. Это разбиение осуществляет объект класса StreamTokenizer. Он 
определяет тип и значение анализируемой лексемы, используя метод nextTokenQ. 

    264 Visual J++: основы программирования 

