2. Работа с файлами

Библиотека классов языка программирования Java содержит многочисленные средства, предназначенные для работы с файлами. И хотя аплеты не имеют доступа к локальным файлам, расположенным на компьютере пользователя, они могут обращаться к файлам, которые находятся в каталоге сервера Web. Автономные приложения Java могут работать как с локальными, так и с удаленными файлами (через сеть Internet или Intranet).

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

Классы Java для работы с потоками

Программист, создающий автономное приложение Java, может работать с потоками нескольких типов:

Рассмотрим кратко классы, связанные с потоками.

Стандартные потоки

Для работы со стандартными потоками в классе System имеется три статических объекта: System.in, System.out и System.err. По своему назначению эти потоки больше всего напоминают стандартные потоки ввода, вывода и вывода сообщений об ошибках операционной системы MS-DOS.

Поток System.in связан с клавиатурой, поток System.out и System.err - с консолью приложения Java.

Базовые классы для работы с файлами и потоками

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

Все основные классы, интересующие нас в этой главе, произошли от класса Object (рис. 2.1).

Рис. 2.1. Основные классы для работы с файлами и потоками

Класс InputStream

Класс InputStream является базовым для большого количества классов, на базе которых создаются потоки ввода. Именно эти, производные классы применяются программистами, так как в них имеются намного более мощные методы, чем в классе InputStream. Эти методы позволяют работать с потоком ввода не на уровне отдельных байт, а на уровне объектов различных классов, например, класса String и других.

Класс OutputStream

Аналогично, класс OutputStream служит в качестве базового для различных классов, имеющих отношение к потокам вывода.

Класс RandomAccesFile

С помощью класса RandomAccesFile можно организовать работу с файлами в режиме прямого доступа, когда программа указывает смещение и размер блока данных, над которым выполняется операция ввода или вывода. Заметим, кстати, что классы InputStream и OutputStream также можно использовать для обращения к файлам в режиме прямого доступа.

Класс File

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

Класс FileDescriptor

C помощью класса FileDescriptor вы можете проверить идентификатор открытого файла.

Класс StreamTokenizer

Очень удобен класс StreamTokenizer. Он позволяет организовать выделение из входного потока данных элементов, отделенных друг от друга заданными разделителями, такими, например, как запятая, пробел, символы возврата каретки и перевода строки.

Производные от класса InputStream

От класса InputStream производится много других классов, как это показано на рис. 2.2.

Рис. 2.2. Классы, производные от класса InputStream

Класс FilterInputStream

Класс FilterInputStream, который происходит непосредственно от класса InputStream, является абстрактным классом, на базе которого созданы классы BufferedInputStream, DataInputStream, LineNumberInputStream и PushBackInputStream. Непосредственно класс FilterInputStream не используется в приложениях Java, так как, во-первых, он является абстрактным и предназначен для переопределения методов базового класса InputStream, а во-вторых, наиболее полезные методы для работы с потоками ввода имеются в классах, созданных на базе класса FilterInputStream.

Класс BufferedInputStream

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

Класс BufferedInputStream может быть использован приложениями Java для организации буферизованных потоков ввода. Заметим, что конструкторы этого класса в качестве параметра получают ссылку на объект класса InputStream. Таким образом, вы не можете просто создать объект класса BufferedInputStream, не создав перед этим объекта класса InputStream. Подробности мы обсудим позже.

Класс DataInputStream

Составляя программы на языке программирования С, вы были вынуждены работать с потоками на уровне байт или, в лучшем случае, на уровне текстовых строк. Однако часто возникает необходимость записывать в потоки данных и читать оттуда объекты других типов, например, целые числа и числа типа double, числа в формате с плавающей десятичной точкой, массивы байт и символов и так далее.

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

Так же как и конструктор класса BufferedInputStream, конструктор класса DataInputStream должен получить через свои параметр ссылку на объект класса InputStream.

Класс LineNumberInputStream

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

Класс PushBackInputStream

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

Класс ByteArrayInputStream

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

Потоки в оперативной памяти могут быть использованы для временного хранения данных. Заметим, что так как аплеты Java не могут обращаться к локальным файлам, для создания временных файлов можно использовать потоки в оперативной памяти на базе класса ByteArrayInputStream. Другую возможность предоставляет класс StringBufferInputStream, рассмотренный ниже.

Класс StringBufferInputStream

Класс StringBufferInputStream позволяет создавать потоки ввода на базе строк класса String, используя при этом только младшие байты хранящихся в такой строке символов. Этот класс может служить дополнением для класса ByteArrayInputStream, который также предназначен для создания потоков на базе данных из оперативной памяти.

Класс FileInputStream

Этот класс позволяет создать поток ввода на базе класса File или FileDescriptor.

Класс PipedInputStream

С помощью классов PipedInputStream и PipedOutputStream можно организовать двухстороннюю передачу данных между двумя одновременно работающими задачами мультизадачного аплета.

Класс SequenceInputStream

Класс SequenceInputStream позволяет объединить несколько входных потоков в один поток. Если в процессе чтения будет достигнут конец первого потока такого объединения, в дальнейшем чтение будет выполняться из второго потока и так далее.

Производные от класса OutputStream

Класс OutputStream предназначен для создания потоков вывода. Приложения, как правило, непосредственно не используют этот класс для операций вывода, так же как и класс InputStream для операций ввода. Вместо этого применяются классы, иерархия которых показана на рис. 2.3.

Рис. 2.3. Классы, производные от класса OutputtStream

Рассмотрим кратко назначение этих классов.

Класс FilterOutputStream

Абстрактный класс FilterOutputStream служит прослойкой между классом OutputStream и классами BufferedOutputStream, DataOutputStream, а также PrintStream. Он выполняет роль, аналогичную роли рассмотренного ранее класса FilterIntputStream.

Класс BufferedOutputStream

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

Класс DataOutputStream

С помощью класса DataOutputStream приложения Java могут выполнять форматированный вывод данных. Для ввода форматированных данных вы должны создать входной поток с использованием класса DataInputStream, о котором мы уже говорили. Класс DataOutputStream реализует интерфейс DataOutput.

Класс PrintStream

Потоки, созданные с использованием класса PrintStream, предназначены для форматного вывода данных различных типов с целью их визуального представления в виде текстовой строки. Аналогичная операция в языке программирования С выполнялась функцией printf.

Класс ByteArrayOutputStream

С помощью класса ByteArrayOutputStream можно создать поток вывода в оперативной памяти.

Класс FileOutputStream

Этот класс позволяет создать поток вывода на базе класса File или FileDescriptor.

Класс PipedOutputStream

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

Работа со стандартными потоками

Приложению Java доступны три стандратных потока, которые всегда открыты: стандартный поток ввода, стандартный поток вывода и стандартный поток вывода сообщений об ошибках.

Все перечисленные выше потоки определены в классе System как статические поля с именами, соответственно, in, out и err:

public final class java.lang.System
  extends java.lang.Object
{
  public static PrintStream err;
  public static InputStream in;
  public static PrintStream out;
  . . .
}

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

Стандартный поток ввода

Стандартный поток ввода in определен как статический объект класса InputStream, который содержит только простейшие методы для ввода данных. Нужнее всего вам будет метод read:

public int read(byte b[]);

Этот метод читает данные из потока в массив, ссылка на который передается через единственный параметр. Количество считанных данных определяется размером массива, то есть значением b.length.

Метод read возвращает количество прочитанных байт данных или -1, если достигнут конец потока. При возникновении ошибок создается исключение IOException, обработку которого необходимо предусмотреть.

Стандартный поток вывода

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

Для работы со стандартным потоком вывода вы будете использовать главным образом методы print и println, хотя метод write также доступен.

В классе PrintStream определено несколько реализаций метода print с параметрами различных типов:

public void print(boolean b);
public void print(char c);
public void print(char s[]);
public void print(double d);
public void print(float f);
public void print(int i);
public void print(long l);
public void print(Object obj);
public void print(String s);

Как видите, вы можете записать в стандартный поток вывода текстовое представление данных различного типа, в том числе и класса Object.

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

public void println();
public void println(boolean b);
public void println(char c);
public void println(char s[]);
public void println(double d);
public void println(float f);
public void println(int i);
public void println(long l);
public void println(Object obj);
public void println(String s);

Реализация метода println без параметров записывает только символ перехода на следующую строку.

Стандртный поток вывода сообщений об ошибках

Стандртный поток вывода сообщений об ошибках err так же, как и стадартный поток вывода out, создан на базе класса PrintStream. Поэтому для записи сообщений об ошибках вы можете использовать только что описанные методы print и println.

Приложение Standard

Приложение Standard демонстрирует способы работы со стандартными потоками Java. Это консольное приложение, а не аплет.

При запуске приложение Standard выводит строку Hello, Java! И приглашение для ввода строки (рис. 2.4).

Рис. 2.4. Консольное окно приложения Standard

Если ввести любую текстовую строку и нажать клавишу <Enter>, введенная строка появится на консоли. Далее появится сообщение о том, что для завершения работы приложения нужно снова нажать клавишу <Enter>.

Исходный текст приложения

Исходный текст приложения приведен в листинге 2.1.

После трансляции исходного текста вы можете запустить его на выполнение непосредственно из среды разработки приложений Microsoft Visual J++. При этом, когда на экране появится диалоговая панель Information For Running Class (рис. 2.5), вы должны указать в поле Class file name имя Standard, а в поле Run project under включить переключатель Stand-alone interpreter.

Рис. 2.5. Заполнение диалоговой панели Information For Running Class

При этом приложение будет запущено под управлением автономного интерпретатора Java jview.exe.

Листинг 2.1. Файл Standard\Standard.java

// =========================================================
// Демонстрация использования стандартных потоков
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;

public class Standard
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    // Введенная строка
    String sOut;

    // Выполняем попытку вывода на консоль 
    // строки приглашения
    try
    {
      // Выводим строку приглашения
      System.out.println("Hello, Java!\n" + 
        "Enter string and press <Enter>...");
      
      // Читаем с клавиатуры строку
      System.in.read(bKbdInput);

      // Преобразуем введенные символы в строку типа String
      sOut = new String(bKbdInput, 0);

      // Выводим только что введенную строку на консоль
      System.out.println(sOut);
    }
    catch(Exception ioe)
    {
      // При возникновении исключения выводим его описание
      // на консоль
      System.err.println(ioe.toString());
    }

    // Ожидаем ввода с клавиатуры, а затем завершаем 
    // работу приложения
    try
    {
      System.out.println(
        "Press <Enter> to terminate application...");

      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.err.println(ioe.toString());
    }
  }
}

Описание исходного текста

Структура приложения Standard очень проста. В нем определен один класс с именем Standard типа public, и один метод с имененм main:

public class Standard
{
  public static void main(String args[])
  {
    . . .
  }
}

Напомним, что имена класса и файла .class должны совпадать.

Сразу после запуска автономного приложения Java управление передается функции main.

Внутри этой функции мы определили массив bKbdInput типа byte и строку sOut:

byte bKbdInput[] = new byte[256];
String sOut;

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

Первое, что делает наше приложение после создания массива, это вывод на консоль текстовой строки приглашения:

System.out.println("Hello, Java!\n" + 
  "Enter string and press <Enter>...");

Здесь вызывается метод println для статического объекта out класса PrintStream, который, как вы знаете, определен в классе System.

На следующем этапе приложение читает из стандартного потока ввода in, вызывая для этого метод read:

System.in.read(bKbdInput);

Стандартный поток ввода связан с клавиатурой, поэтому приложение перейдет в состояние ожидания до тех пор, пока пользователь не введет текстовую строку, нажав после чего клавишу <Enter>.

Введенная строка отображается на консоли, для чего она записывается в стандартный поток вывода методом println:

System.out.println(sOut);

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

catch(Exception ioe)
{
  System.err.println(ioe.toString());
}

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

Для того чтобы вы смогли посмотреть на результаты работы приложения, после отображения на консоли введенной строки приложение вновь вызывается метод read для стандартного потока ввода. Для завершения работы приложения пользователь должен нажать клавишу <Enter>.

Создание потоков, связанных с файлами

Если вам нужно создать входной или выходной поток, связанный с локальным файлом, следует воспользоваться классами из библиотеки Java, созданными на базе классов InputStream и OutputStream. Мы уже кратко рассказывали об этих классах в разделе “Классы Java для работы с потоками”. Однако методика использования перечисленных в этом разделе классов может показаться довольно странной.

В чем эта странность?

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

Поясним сказанное на примере.

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

В классе DataOutputStream предусмотрен только один конструктор, которому в качестве параметра необходимо передать ссылку на объект класса OutputStream:

public DataOutputStream(OutputStream out);

Что же касается конструктора класса OutputStream, то он выглядит следующим образом:

public OutputStream();

Так как ни в том, ни в другом конструкторе не предусмотрено никаких ссылок на файлы, то непонятно, как с использованием только одних классов OutputStream и DataOutputStream можно создать выходной поток, связанный с файлом.

Что же делать?

Создание потока для форматированного обмена данными

Оказывается, создание потоков, связанных с файлами и предназначенных для форматированного ввода или вывода, необходимо выполнять в несколько приемов. При этом вначале необходимо создать потоки на базе класса FileOutputStream или FileInputStream, а затем передать ссылку на созданный поток констркутору класса DataOutputStream или DataInputStream.

В классах FileOutputStream и FileInputStream предусмотрены конструкторы, которым в качестве параметра передается либо ссылка на объект класса File, либо ссылка на объект класса FileDescriptor, либо, наконец, текстовая строка пути к файлу:

public FileOutputStream(File file);
public FileOutputStream(FileDescriptor fdObj);
public FileOutputStream(String name);

Таким образом, если вам нужен выходной поток для записи форматированных данных, вначале вы создаете поток как объект класса FileOutputStream. Затем ссылку на этот объект следует передать конструктору класса DataOutputStream. Полученный таким образом объект класса DataOutputStream можно использовать как выходной поток, записывая в него форматированные данные.

Добавление буферизации

А что, если нам нужен не простой выходной поток, а буферизованный?

Здесь нам может помочь класс BufferedOutputStream. Вот два конструктора, предусмотренных в этом классе:

public BufferedOutputStream(OutputStream out);
public BufferedOutputStream(OutputStream out, int size);	

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

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

Вот фрагмент исходного текста программы, который создает выходной буферизованный поток для записи форматированных данных в файл с именем output.txt:

DataOutputStream OutStream;
OutStream = new DataOutputStream(
  new BufferedOutputStream(
  new FileOutputStream("output.txt")));

Аналогичным образом создается входной буферизованный поток для чтения форматированных данных из того же файла:

DataInputStream InStream;
InStream = new DataInputStream(
  new BufferedInputStream(
  new FileInputStream("output.txt")));

Исключения при создании потоков

При создании потоков на базе классов FileOutputStream и FileInputStream могут возникать исключения FileNotFoundException, SecurityException, IOException.

Исключение FileNotFoundException возникает при попытке открыть входной поток данных для несуществующего файла, то есть когда файл не найден.

Исключение SecurityException возникает при попытке открыть файл, для которого запрещен доступ. Например, если файл можно только читать, а он открывается для записи, возникнет исключение SecurityException.

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

Запись данных в поток и чтение данных из потока

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

Простейшие методы

Создав выходной поток на базе класса FileOutputStream, вы можете использовать для записи в него данных три разновидности метода write, прототипы которых представлены ниже:

public void write(byte b[]);
public void write(byte b[], int off, int len);
public void write(int b);	

Первый из этих методов записывает в поток содержимое массива, ссылка на который передается через параметр, начиная с текущей позиции в потоке. После выполнения записи текущая позиция продвигается вперед на число записанных байт, которое при успешном завершении операции равно длине массива (b.length).

Второй метод позволяет дополнительно указать начальное смещение off записываемого блока данных в массиве и количество записываемых байт len.

Третий метод просто записывает в поток один байт данных.

Если в процессе записи происходит ошибка, возникает исключение IOException.

Для входного потока, созданного на базе класса FileInputStream, определены три разновидности метода read, выполняющего чтение данных:

public int read();
public int read(byte b[]);
public int read(byte b[], int off, int len);

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

Вторая разновидность метода read читает данные в массив, причем количество прочитанных данных определяется размером массива. Метод возвращает количество прочитанных байт данных или значение -1, если в процессе чтения был достигнут конец файла.

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

Если при чтении происходит ошибка, возникает исключение IOException.

Методы для чтения и записи форматированных данных

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

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

public final void writeBoolean(boolean v);
public final void writeByte(int v);
public final void writeBytes(String s);
public final void writeChar(int v);
public final void writeChars(String s);
public final void writeDouble(double v);
public final void writeFloat(float v);	
public final void writeInt(int v);
public final void writeLong(long v);
public final void writeShort(int v);
public final void writeUTF(String s);

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

Метод writeByte записывает в поток один байт. Это младший байт слова, которое передается методу через параметр v. В отличие от метода writeByte, метод writeChar записывает в поток двухбайтовое символьное значение (напомним, что в Java символы хранятся с использованием кодировки Unicode и занимают два байта).

Если вам нужно записать в выходной поток текстовую строку, то это можно сделать с помощью методов writeBytes, writeChars или writeUTF. Первый из этих методов записывает в выходной поток только младшие байты символов, а второй - двухбайтовые символы в кодировке Unicode. Метод writeUTF предназначен для записи строки в машинно-независимой кодировке UTF-8.

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

В классе DataInputStream определены следующие методы, предназначенные для чтения форматированных данных из входного потока:

public final boolean readBoolean();
public final byte    readByte();
public final char    readChar();
public final double  readDouble();
public final float   readFloat();
public final void    readFully(byte b[]);
public final void    readFully(byte b[], int off, int len);
public final int     readInt();
public final String  readLine();
public final long    readLong();
public final short   readShort();
public final int     readUnsignedByte();
public final int     readUnsignedShort();
public final String  readUTF();
public final static String readUTF(DataInput in);
public final int     skipBytes(int n);

Обратите внимание, что среди этих методов нет тех, что специально предназначены для четния данных, записанных из строк методами writeBytes и writeChars класса DataOutputStream.

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

Для чтения строк, записанных методом writeUTF вы должны обязательно пользоваться методом readUTF.

Метод skipBytes позволяет пропустить из входного потока заданное количество байт.

Методы класса DataInputStream, предназначенные для чтения данных, могут создавать исключения IOException и EOFException. Первое из них возникает в случае ошибки, а второе - при достижении конца входного потока в процессе чтения.

Закрывание потоков

Работая с файлами в среде MS-DOS или Microsoft Windows средствами языка программирования С вы должны были закрывать ненужные более файлы. Так как в системе интерпертации приложений Java есть процесс сборки мусора, возникает вопрос - выполняет ли он автоматическое закрывание потоков, с которыми приложение завершило работу?

Оказывается, процесс сборки мусора не делает ничего подобного!

Сборка мусора выполняется только для объектов, размещенных в оперативной памяти. Потоки вы должны закрывать явным образом, вызывая для этого метод close.

Принудительный сброс буферов

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

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

Приложение StreamDemo

Приложение StreamDemo было создано нами специально для того чтобы продемонстрировать наиболее распространенный способ работы с файлами через буферизованные форматированные потоки.

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

Исходный текст приложения

Исходный текст приложения StreamDemo представлен в листинге 2.2.

Листинг 2.2. Файл StreamDemo\StreamDemo.java

// =========================================================
// Демонстрация использования потоков для работы с файлами
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;

public class StreamDemo
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Выходной поток
    DataOutputStream OutStream;

    // Входной поток
    DataInputStream  InStream;

    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    // Введенная строка, которая будет записана в поток
    String sOut;

    // Выполняем попытку вывода на консоль строки 
    // приглашения
    try
    {
      // Выводим строку приглашения
      System.out.println("Hello, Java!\n" + 
        "Enter string and press <Enter>...");
      
      // Читаем с клавиатуры строку для записи в файл
      System.in.read(bKbdInput);

      // Преобразуем введенные символы в строку типа String
      sOut = new String(bKbdInput, 0);
    }
    catch(Exception ioe)
    {
      // При возникновении исключения выводим его описание
      // на консоль
      System.out.println(ioe.toString());
    }
    
    // Выполняем попытку записи в выходной поток
    try
    {
      // Создаем выходной буферизованный поток данных
      OutStream = new DataOutputStream(
        new BufferedOutputStream(
          new FileOutputStream("output.txt")));

      // Записываем строку sOut в выходной поток
      OutStream.writeBytes(sOut);

      // Сбрасываем содержимое буфера вывода
      OutStream.flush();

      // Закрываем выходной поток
      OutStream.close();
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
    
    // Выполняем попытку чтения из файла
    try
    {
      // Создаем входной буферизованный поток данных
      InStream = new DataInputStream(
        new BufferedInputStream(
          new FileInputStream("output.txt")));

      // Читаем одну строку из созданного входного потока
      // и отображаем ее на консоли
      System.out.println(InStream.readLine());

      // Закрываем входной поток
      InStream.close();
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }

    // Ожидаем ввода с клавиатуры, а затем завершаем 
    // работу приложения
    try
    {
      System.out.println(
        "Press <Enter> to terminate application...");

      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

Описание исходного текста приложения

Внутри метода main мы создали ссылки на выходной поток OutStream и входной поток InStream:

DataOutputStream OutStream;
DataInputStream  InStream;

Кроме того, мы предусмотрели массив для ввода информации из стандартного входного потока:

byte bKbdInput[] = new byte[256];

Способ вывода приглашения и получения строки с клавиатуры ничем не отличается от использованного в предыдущем приложении, поэтому для экономии места мы не будем на нем останавливаться. Скажем только, что введенная строка записывается в поле sOut типа String.

Создание выходного потока для записи строки выполняется следующим образом:

OutStream = new DataOutputStream(
  new BufferedOutputStream(
  new FileOutputStream("output.txt")));

Вначале с помощью конструктора создается объект класса FileOutputStream - поток, связанный с файлом output.txt. Далее на базе этого потока создается буферизованный поток типа BufferedOutputStream. И, наконец, на базе буферизованного потока создается форматированный поток OutStream класса DataOutputStream.

Заметим, что конструктор класса FileOutputStream создает файл output.txt, если он не существует, и перезаписывает существующий файл. Если вам нужно исключить случайную перезапись существующего файла, необходимо воспользоваться классом File, о котором мы еще будем рассказывать.

Для записи строки sOut в выходной поток мы вызываем метод writeBytes:

OutStream.writeBytes(sOut);

После записи выполняется сброс содержимого буферов и закрытие потока:

OutStream.flush();
OutStream.close();

При закрытии потока содержимое буферов сбрасывается автоматически. Мы вызвали метод flush только для иллюстрации.

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

catch(Exception ioe)
{
  System.out.println(ioe.toString());
}

На следующем этапе приложение открывает файл output.txt для чтения буферизованным форматированным потоком:

InStream = new DataInputStream(
  new BufferedInputStream(
  new FileInputStream("output.txt")));

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

Для чтения строки из входного потока мы применили метод readLine:

System.out.println(InStream.readLine());

Прочитанная строка сразу отображается на консоли, для чего она записывается в стандартный поток вывода.

После завершения работы со входным потоком мы закрываем его методом close:

InStream.close();

На последнем этапе приложение ожидает ввода с клавиатуры и затем завершает свою работу.

Потоки в оперативной памяти

Операционные системы Microsoft Windows 95 и Microsoft Windows NT предоставляют возможность для программиста работать с оперативной памятью как с файлом. Это очень удобно во многих случаях, в частности, файлы, отображаемые на память, можно использовать для передачи данных между одновременно работающими задачами и процессами. Подробнее об этом вы можете прочитать в 27 томе “Библиотеки системного программиста”, который называется “Операционная система Microsoft Windows NT для программиста. Часть 2”.

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

Ранее мы отмечали, что в библиотеке классов Java есть три класса, специально предназначенных для создания потоков в оперативной памяти. Это классы ByteArrayOutputStream, ByteArrayInputStream и StringBufferInputStream.

Класс ByteArrayOutputStream

Класс ByteArrayOutputStream создан на базе класса OutputStream. В нем имеется два конструктора, прототипы которых представлены ниже:

public ByteArrayOutputStream();
public ByteArrayOutputStream(int size);

Первый из этих конструкторов создает выходной поток в оперативной памяти с начальным размером буфера, равным 32 байта. Второй позволяет указать необходимый размер буфера.

В классе ByteArrayOutputStream определено несколько достаточно полезных методов. Вот некоторые из них:

public void reset();
public int size();
public byte[] toByteArray();
public void writeTo(OutputStream out);	

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

С помощью метода size можно определить количество байт данных, записанных в поток.

Метод toByteArray позволяет скопировать данные, записанные в поток, в массив байт. Этот метод возвращает адрес созданного для этой цели массива.

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

Для выполнения форматированного вывода в поток, вы должны создать поток на базе класса DataOutputStream, передав соответствующему конструктору ссылку на поток класса ByteArrayOutputStream.

Класс ByteArrayInputStream

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

public ByteArrayInputStream(byte buf[]);
public ByteArrayInputStream(byte buf[], int offset, 
  int length);

Первый конструктор получает через единственный параметр ссылку на массив, который будет использован для создания входного потока. Второй позволяет дополнительно указать смещение offset и размер области памяти length, которая будет использована для создания потока.

Вот несколько методов, определенных в классе ByteArrayInputStream:

public int available();
public int read();
public int read(byte b[], int off, int len);
public void reset();
public long skip(long n);	

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

Обычно класс ByteArrayInputStream используется вместе с классом DataInputStream, что позволяет организовать форматный ввод данных.

Класс StringBufferInputStream

Класс StringBufferInputStream предназначен для создания входного потока на базе текстовой строки класса String. Ссылка на эту строку передается конструктору класса StringBufferInputStream через параметр:

public StringBufferInputStream(String s);

В классе StringBufferInputStream определены те же методы, что и в только что рассмотренном классе ByteArrayInputStream. Для более удобной работы вы, вероятно, создадите на базе потока класса StringBufferInputStream поток класса DataInputStream.

Приложение MemStream

Аплет MemStream создает два потока в оперативной памяти - выходной и входной. Вначале во время инициализации метод init создает выходной поток и записывает в него текстовую строку “Hello, Java!”. Содержимое этого потока затем копируется в массив, и на базе этого массива создается входной поток.

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

Аплет не может работать с обычными локальными файлами, поэтому для выполнения описанных выше действий необходимы потоки в оперативной памяти.

Исходные тексты приложения

Исходный текст аплета MemStream приведен в листинге 2.3.

Листинг 2.3. Файл MemStream\MemStream.java

// =========================================================
// Работа с потоками в оперативной памяти
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.applet.*;
import java.awt.*;
import java.io.*;

public class MemStream extends Applet
{
  // Выходной поток
  DataOutputStream OutStream;

  // Входной поток
  DataInputStream  InStream;

  // Строка, которая будет записана в поток
  String sOut;

  // Массив, в который будет копироваться содержимое
  // выходного потока
  byte[] bMemStream;

  // -------------------------------------------------------
  // getAppletInfo
  // Метод, возвращающей строку информации об аплете
  // -------------------------------------------------------
  public String getAppletInfo()
  {
    return "Name: MemStream\r\n" +
      "Author: Alexandr Frolov\r\n" +
      "E-mail: frolov@glas.apc.org" +
      "WWW:    http://www.glasnet.ru/~frolov" +
      "Created with Microsoft Visual J++ Version 1.0";
  }

  // -------------------------------------------------------
  // init
  // Метод init, получает управление при инициализации 
  // аплета
  // -------------------------------------------------------
  public void init()
  {
    // Инициализируем строку для записи в поток
    sOut = "Hello, Java!";

    try
    {
      // Создаем выходной поток в оперативной памяти
      ByteArrayOutputStream baStream = 
        new ByteArrayOutputStream(255);

      // Создаем буферизованный форматированный поток
      // на базе потока baStream 
      OutStream = new DataOutputStream(
        new BufferedOutputStream(baStream));

      // Записываем строку sOut в выходной поток
      OutStream.writeBytes(sOut);

      // Сбрасываем содержимое буфера
      OutStream.flush();

      // Копируем содержимое потока в массив bMemStream 
      bMemStream = baStream.toByteArray();

      // Закрываем выходной поток
      OutStream.close();
    }
    catch(Exception ioe)
    {
    }
  }

  // -------------------------------------------------------
  // paint
  // Метод paint, выполняющий рисование в окне аплета
  // -------------------------------------------------------
  public void paint(Graphics g)
  {
    // Определяем текущие размеры окна аплета
    Dimension dimAppWndDimension = size();
    
    // Выбираем в контекст отображения желтый цвет
    g.setColor(Color.yellow);
    
    // Закрашиваем внутреннюю область окна аплета
    g.fillRect(0, 0, 
      dimAppWndDimension.width  - 1, 
      dimAppWndDimension.height - 1);

    // Выбираем в контекст отображения черный цвет
    g.setColor(Color.black);

    // Рисуем рамку вокруг окна аплета
    g.drawRect(0, 0, 
      dimAppWndDimension.width  - 1, 
      dimAppWndDimension.height - 1);

    try
    {
      // Создаем входной буферизованный поток данных
      InStream = new DataInputStream(
        new BufferedInputStream(
        new ByteArrayInputStream(bMemStream)));

      // Читаем одну строку из созданного входного потока
      // и отображаем ее
      g.drawString(InStream.readLine(), 10, 20);

      // Закрываем входной поток
      InStream.close();
    }
    catch(Exception ioe)
    {
    }
  }
}

В листинге 2.4 вы найдете исходный текст документа HTML, созданный автоматически для нашего аплета системой Microsoft Visual J++.

Листинг 2.4. Файл MemStream\MemStream.html

<html>
<head>
<title>MemStream</title>
</head>
<body>
<hr>
<applet
    code=MemStream.class
    id=MemStream
    width=320
    height=240 >
</applet>
<hr>
<a href="MemStream.java">The source.</a>
</body>
</html>

Описание исходных текстов

Мы рассмотрим только самые важные методы нашего аплета - init и paint.

Метод init

В начале своей работы метод init записывает в поле sOut текстовую строку, которая будет записана в выходной поток:

String sOut;
sOut = "Hello, Java!";

Далее метод init создает поток baStream класса ByteArrayOutputStream, устанавливая начальный размер выделенного для него массива равным 255 байт:

ByteArrayOutputStream baStream = 
  new ByteArrayOutputStream(255);

Для выполнения форматированного вывода нам нужен поток класса DataOutputStream, который мы и создаем на базе потока baStream:

OutStream = new DataOutputStream(
  new BufferedOutputStream(baStream));

Для записи строки в выходной поток мы воспользовались методом writeBytes:

OutStream.writeBytes(sOut);

Так как наш выходной поток буферизован, после вызова метода writeBytes данные могут остаться в промежуточном буфере, не достигнув массива, выделенного для хранения потока. Чтобы переписать данные из буфера в массив, мы выполняем сброс буфера методом flush:

OutStream.flush();

После сброса буфера (и только после этого) можно копировать содержимое потока методом toByteArray:

bMemStream = baStream.toByteArray();

Этот метод возвращает ссылку на созданный массив, которую мы записываем в поле bMemStream. В дальнейшем на базе этого массива мы создадим поток ввода.

Перед завершением своей работы метод init закрывает входной поток,вызывая метод close:

OutStream.close();

Метод paint

После традиционного для наших аплетов раскрашивания окна и рисования рамки метод paint создает входной буферизованный поток на базе массива bMemStream:

InStream = new DataInputStream(
  new BufferedInputStream(
  new ByteArrayInputStream(bMemStream)));

Поток создается в три этапа с помощью классов ByteArrayInputStream, BufferedInputStream и DataInputStream.

Далее мы читаем из созданного таким образом входного потока одну строку, вызывая для этого метод readLine:

g.drawString(InStream.readLine(), 10, 20);

Прочитанная строка отображается в окне аплета методом drawString.

После завершения работы с потоком мы его закрываем, для чего вызываем метод close:

InStream.close();

Класс StreamTokenizer для разбора входных потоков

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

Конструктор класса StreamTokenizer

Для создание объектов класса StreamTokenizer предусмотрен всего один конструктор:

public StreamTokenizer(InputStream istream);

В качестве параметра этому конструктору необходимо передать ссылку на заранее созданный входной поток.

Методы класса StreamTokenizer

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

Методы для настройки параметров разборщика

Ниже мы привели прототипы методов, предназначенных для настройки параметров разборщика:

public void commentChar(int ch);
public void slashSlashComments(boolean flag);
public void slashStarComments(boolean flag);
public void quoteChar(int ch);

public void eolIsSignificant(boolean flag);
public void lowerCaseMode(boolean fl);

public void ordinaryChar(int ch);
public void ordinaryChars(int low, int hi);
public void resetSyntax();

public void parseNumbers();
public void whitespaceChars(int low, int hi);
public void wordChars(int low, int hi);	

Несколько методов определяют, будет ли разборщик выделять во входном потоке строки комментария и если будет, то какми образом.

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

Методы SlashSlashComments и slashStarComments позволяют указать, что для входного текста используются разделители комментариев в виде двойного символа ‘/’ и ‘/* … */’, соответственно. Это соответствует способу указания комментариев в программах, составленных на языках программирования С++ и С. Для включения режима выделения комментариев обоим методам в качетстве параметра необходимо передать значение true, а для отключения - false.

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

Если передать методу eolIsSignificant значение true, разделители строк будут интерпретироваться как отдельные элементы. Если же этому методу передать значение false, разделители строк будут использоваться аналогично пробелам для разделения элементов входного потока.

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

Методы ordinaryChar и ordinaryChars позволяют указать символы, которые должны интерпретироваться как обычные, из которых составляются слова или цифры. Например, если передать методу ordinaryChar символ ‘.’, то слово java.io будет восприниматься как один элемент. Если же этого не сделать, то разборщик выделит из него три элемента - слово java, точку ‘.’ и слово io. Метод ordinaryChars позволяет указать диапазон значений символов, которые должны интерпретироваться как обычные.

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

Метод parseNumbers включает режим разбора чисел, при котором распознаются и преобразуются числа в формате с плавающей десятичной точкой.

Метод whitespaceChars задает диапазон значений для символов-разделителей отдельных слов в потоке.

Метод wordChars позволяет указать символы. Которые являются составными частями слов.

Методы для разбора входного потока

После того как вы создали разборщик входного потока на базе класса StreamTokenizer и установили его параметры с помощью описанных выше методов, можно приступать собственно к разборке потока. Обычно для этого организуется цикл, в котором вызывается метод nextToken:

public int nextToken();

Этот метод может вернуть одно из следующих значений:

Значение Описание
TT_WORD Из потока было извлечено слово
TT_NUMBER Из потока было извлечено численное значение
TT_EOL Обнаружен конец строки. Это значение возвращается только в том случае, если при настройке параметров разборщика был вызван метод eolIsSignficant
TT_EOF Обнаружен конец файла

Если метод nextToken вернул значение TT_EOF, следует завершить цикл разбора входного потока.

Как извлечь считанные элементы потока?

В классе StreamTokenizer определено три поля:

public String sval;
public double nval;
public int    ttype;

Если метод nextToken вернул значение TT_WORD, в поле sval содержится извлеченный элемент в виде текстовой строки. В том случае, когда из входного потока было извлечено числовое значение, оно будет храниться в поле nval типа double. Обычные символы записываются в поле ttype.

Заметим, что если в потоке обнаружены слова, взятые в кавычки, то символ кавычки записывается в поле ttype, а слова - в поле sval. По умолчанию используется символ кавычек ‘”’, однако с помощью метода quoteChar вы можете задать любой другой символ.

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

public int lineno();

После вызова метода pushBack следующий вызов метода nextToken приведет к тому, что в поле ttype будет записано текущее значение, а содержимое полей sval и nval не изменится. Прототип метода pushBack приведен ниже:

public void pushBack();

Метод toString возвращает текстовую строку, представляющую текущий элемент, выделенный из потока:

public String toString();

Приложение StreamToken

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

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

Рис. 2.6. Разбор входного потока в приложении StreamToken

Обратите внимание, что в процессе разбора значение 3.14 было воспринято как числовое, а 3,14 - нет. Это потому, что при настройке разборщика мы указали, что символ ‘.’ является обычным.

Исходный текст приложения

Исходный текст приложения StreamToken представлен в листинге 2.5.

Листинг 2.5. Файл StreamToken\StreamToken.java

// =========================================================
// Разбор входного потока при помощи класса 
// StreamTokenizer 
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;

// =========================================================
// Класс StreamToken
// Главный класс приложения
// =========================================================
public class StreamToken
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Выходной поток
    DataOutputStream OutStream;

    // Входной поток
    DataInputStream  InStream;

    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    // Введенная строка, которая будет записана в поток
    String sOut;

    try
    {
      // Выводим строку приглашения
      System.out.println("Enter string to parse...");
      
      // Читаем с клавиатуры строку для записи в файл
      System.in.read(bKbdInput);

      // Преобразуем введенные символы в строку типа String
      sOut = new String(bKbdInput, 0);
    
      // Создаем выходной буферизованный поток данных
      OutStream = new DataOutputStream(
        new BufferedOutputStream(
          new FileOutputStream("output.txt")));

      // Записываем строку sOut в выходной поток
      OutStream.writeBytes(sOut);

      // Закрываем выходной поток
      OutStream.close();

      // Создаем входной буферизованный поток данных
      InStream = new DataInputStream(
        new BufferedInputStream(
          new FileInputStream("output.txt")));

      // Создаем объект для разбора потока
      TokenizerOfStream tos = new TokenizerOfStream();

      // Выполняем разбор
      tos.TokenizeIt(InStream);

      // Закрываем входной поток
      InStream.close();

      System.out.println("Press <Enter> to terminate...");
      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

// =========================================================
// Класс TokenizerOfStream
// Класс для разбора входного потока
// =========================================================
class TokenizerOfStream
{
  public void TokenizeIt(InputStream is)
  {
    // Создаем разборщик потока
    StreamTokenizer stok;

    // Временная строка
    String str;

    try
    {
      // Создаем разборщик потока
      stok = new StreamTokenizer(is);

      // Задаем режим исключения комментариев,
      // записанных в стиле С++ (два символа '/')
      stok.slashSlashComments(true);

      // Указываем , что символ '.' будет обычным символом
      stok.ordinaryChar('.');

      // Запускаем цикл разбора потока,
      // который будет завершен при достижении
      // конца потока
      while(stok.nextToken() != StreamTokenizer.TT_EOF)
      {
        // Определяем тип выделенного элемента
        switch(stok.ttype)
        {
          // Если это слово, записываем его во
          // временную строку
          case StreamTokenizer.TT_WORD:
          {
            str = new String("\nTT_WORD >" + stok.sval);
            break;
          }

          // Если это число, преобразуем его
          // в строку
          case StreamTokenizer.TT_NUMBER:
          {
            str = "\nTT_NUMBER >" + 
              Double.toString(stok.nval);
            break;
          }

          // Если найден конец строки, 
          // выводим строку End of line
          case StreamTokenizer.TT_EOL:
          {
            str = new String("> End of line");
            break;
          }

          // Выводим прочие символы
          default:
          {
            if((char)stok.ttype == '"')
            {
              str = new String("\nTT_WORD >" + stok.sval);
            }

            else
              str = "> " + 
                 String.valueOf((char)stok.ttype);
          }

          // Выводим на консоль содержимое временной строки
          System.out.println(str);
      }
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

Описание исходного текста приложения

После ввода строки с клавиатуры и записи ее в файл через поток наше приложение создает входной буферизованный поток, как это показано ниже:

InStream = new DataInputStream(
  new BufferedInputStream(
  new FileInputStream("output.txt")));

Далее для этого потока создается разборщик, который оформлен в отдельном классе TokenizerOfStream, определенном в нашем приложении:

TokenizerOfStream tos = new TokenizerOfStream();

Вслед за этим мы вызываем метод TokenizeIt, определенный в классе TokenizerOfStream, передавая ему в качестве параметра ссылку на входной поток:

tos.TokenizeIt(InStream);

Метод TokenizeIt выполняет разбор входного потока, отображая результаты разбора на консоли. После выполнения разбора входной поток закрывается методом close:

InStream.close();

Самое интересное в нашем приложении связано, очевидно, с классом TokenizerOfStream, поэтому перейдем к его описанию.

В этом классе определен только один метод TokenizeIt:

public void TokenizeIt(InputStream is)
{
  . . .
}

Получая в качестве параметра ссылку на входной поток, он прежде всего создает для него разборщик класса StreamTokenizer:

StreamTokenizer stok;
stok = new StreamTokenizer(is);

Настройка параметров разборщика очень проста и сводится к вызовам всего двух методов:

stok.slashSlashComments(true);
stok.ordinaryChar('.');

Метод slashSlashComments включает режим распознавания комментариев в стиле языка программирования С++, а метод ordinaryChar объявляет символ ‘.’ обычным символом.

После настройки запускается цикл разбора входного потока, причем условием завершения цикла является достижение конца этого потока:

while(stok.nextToken() != StreamTokenizer.TT_EOF)
{
  . . .
}

В цикле анализируется содержимое поля ttype, которое зависит от типа элемента, обнаруженного во входном потоке:

switch(stok.ttype)
{
  case StreamTokenizer.TT_WORD:
  {
    str = new String("\nTT_WORD >" + stok.sval);
    break;
  }
  case StreamTokenizer.TT_NUMBER:
  {
    str = "\nTT_NUMBER >" + Double.toString(stok.nval);
    break;
  }
  case StreamTokenizer.TT_EOL:
  {
    str = new String("> End of line");
    break;
  }
  default:
  {
    if((char)stok.ttype == '"')
      str = new String("\nTT_WORD >" + stok.sval);
    else
      str = "> " + String.valueOf((char)stok.ttype);
  }
}

На слова и численные значения мы реагируем очень просто - записываем их текстовое представление в рабочую переменную str типа String. При обнаружении конца строки в эту переменную записывается строка End of line.

Если же обнаружен обычный символ, мы сравниваем его с символом кавычки. При совпадении в переменную str записывается содержимое поля sval, в котором находятся слова, обнаруженные внутри кавычек. Если же обнаруженный символ не является символом кавычки, он преобразуется в строку и записывается в переменную str.

В заключении метод выводит строку str в стандартный поток вывода, отображая на консоли выделенный элемент потока:

System.out.println(str);

Класс StringTokenizer

Рассказывая о классе StreamTokenizer, нельзя не упомянуть о другом классе с похожим названием и назначением, а именно о классе StringTokenizer.

Определение этого класса достаточно компактно, поэтому мы приведем его полностью:

public class java.util.StringTokenizer
  extends java.lang.Object
  implements java.util.Enumeration
{
  // ---------------------------------------------------
  // Конструкторы класса
  // ---------------------------------------------------
  public StringTokenizer(String str);
  public StringTokenizer(String str, String delim);
  public StringTokenizer(String str, String delim, 
    boolean returnTokens);

  // ---------------------------------------------------
  // Методы
  // ---------------------------------------------------
  public String nextToken();
  public String nextToken(String delim);
  public int countTokens();
  public boolean hasMoreElements();
  public boolean hasMoreTokens();
  public Object nextElement();
}

Класс StringTokenizer не имеет никакого отношения к потокам, так как предназначен для выделения отдельных элементов из строк типа String.

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

Рассмотрим кратко методы класса StringTokenizer.

Для разбора строки приложение должно организовать цикл, вызывая в нем метод nextToken. Условием завершения цикла может быть либо возникновение исключения NoSuchElementException, либо возврат значения false методами hasMoreElements или hasMoreTokens.

Метод countTokens позволяет определить, сколько раз был вызван метод nextToken перед возникновением исключения NoSuchElementException.

Приложение StringToken

Приложение StringToken получает одну строку из стандартного потока ввода и выполняет ее разбор с помощью класса StringTokenizer. Отдельные элементы строки выводятся на консоль в столбик (рис. 2.7).

Рис. 2.7. Разбор строки в приложении StringToken

В качестве разделителя заданы символы ",.; ", то есть запятая, точка, точка с запятой и пробел.

Исходный текст приложения

Исходный текст приложения StringToken вы найдете в листинге 2.6.

Листинг 2.6. Файл StringToken\StringToken.java

// =========================================================
// Разбор текстовой строки при помощи класса 
// StringTokenizer 
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;
import java.util.*;

// =========================================================
// Класс StringToken
// Главный класс приложения
// =========================================================
public class StringToken
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    // Введенная строка, которая будет записана в поток
    String sOut;
    String str;

    try
    {
      // Выводим строку приглашения
      System.out.println("Enter string to parse...");
      
      // Читаем с клавиатуры строку для записи в файл
      System.in.read(bKbdInput);

      // Преобразуем введенные символы в строку типа String
      sOut = new String(bKbdInput, 0);
      
      // Создаем разборщик текстовой строки
      StringTokenizer st;

      st = new StringTokenizer(sOut, ",.; ");

      // Запускаем цикл разборки строки
      while(st.hasMoreElements())
      {
        // Получаем очередной жлемент
        str = new String((String)st.nextElement());

        // Записываем его в стандартный поток вывода
        System.out.println(str);
      }
      
      System.out.println("Press <Enter> to terminate...");
      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

Описание исходного текста

После ввода текстовой строки и ее записи в поле sOut наше приложение создает на базе этой строки объект st класса StringTokenizer:

StringTokenizer st;
st = new StringTokenizer(sOut, ",.; ");

Далее он в цикле получает все элементы строки, вызывая для этого метод nextElement:

while(st.hasMoreElements())
{
  str = new String((String)st.nextElement());
  System.out.println(str);
}

Для проверки условия завершения цикла вызывается метод hasMoreElements. Когда он возвращает значение false, цикл завершается.

Выделенные в цикле элементы строки записываются в переменную str и отображаются на консоли.

Работа с файлами и каталогами при помощи класса File

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

Создание объекта класса File

У вас есть три возможности создать объект класса File, вызвав для этого один из трех конструкторов:

public File(String path);
public File(File dir, String name);
public File(String path, String name);

Первый из этих конструкторов имеет единственный параметр - ссылку на строку пути к файлу или каталогу. С помощью второго конструктора вы можете указать отдельно каталог dir и имя файла, для которого создается объект в текущем каталоге. И, наконец, третий конструктор позволяет указать полный путь к каталогу и имя файла.

Если первому из перечисленных конструкторов передать ссылку со значением null, возникнет исключение NullPointerException.

Пользоваться конструкторам очень просто. Вот, например, как создать объект класса File для файла c:\autoexec.bat и каталога d:\winnt:

f1 = new File("c:\\autoexec.bat");
f2 = new File("d:\\winnt");

Определение атрибутов файлов и каталогов

После того как вы создали объект класса File, нетрудно определить атрибуты этого объекта, воспользовавшись соответствующими методами класса File.

Проверка существования файла или каталога

С помощью метода exists вы можете проверить существование файла или катлога, для которого был создан объект класса File:

public boolean exists();

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

Проверка возможности чтения и записи

Методы canRead и canWrite позволяют проверить возможность чтения из файла и записи в файл, соответственно:

public boolean canRead();
public boolean canWrite();

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

Определение типа объекта - файл или каталог

С помощью методов isDirectory и isFile вы можете проверить, чему соответствует созданный объект класса File - каталогу или файлу:

public boolean isDirectory();
public boolean isFile();

Получение имени файла или каталога

Метод getName возвращает имя файла или каталога для заданного объекта класса File (имя выделяется из пути):

public String getName();

Получение абсолютного пути к каталогу

Метод getAbsolutePath возвращает абсолютный путь к файлу или каталогу, который может быть машинно-зависимым:

public String getAbsolutePath();

Определение типа указанного пути - абсолютный или относительный

С помощью метода isAbsolute вы можете определить, соответствует ли данный объект класса File файлу или каталогу, заданному абсолютным (полным) путем, либо относительным путем:

public boolean isAbsolute();

Определение пути к файлу или каталогу

Метод getPath позволяет определить машинно-независимый путь файла или каталога:

public String getPath();

Определение родительского каталога

Если вам нужно определить родительский каталог для объекта класса File, то это можно сделать методом getParent:

public String getParent();

Определение длины файла в байтах

Длину файла в байтах можно определить с помощью метода length:

public long length();

Определение времени последней модификации файла или каталога

Для определения времени последней модификации файла или каталога вы можете вызвать метод lastModified:

public long lastModified();

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

Получение текстового представления объекта

Метод toString возвращает текстовую строку, представляющую объект класса File:

public String toString();	

Получение значения хэш-кода

Метод hashCode возвращает значение хэш-кода, соответствующего объекту File:

public int hashCode();

Удаление файлов и каталогов

Для удаления ненужного файла или каталога вы должны создать соответствующий объект File и затем вызвать метод delete:

public boolean delete();

Создание каталогов

С помощью методов mkdir и mkdirs можно создавать новые каталоги:

public boolean mkdir();
public boolean mkdirs();

Первый из этих методов создает один каталог, второй - все подкаталоги, ведущие к создаваемому каталогу (то есть полный путь).

Переименование файлов и каталогов

Для переименования файла или каталога вы должны создать два объекта класса File, один из которых соответствует старому имени, а второй - новому. Затем для перовго из этих объектов нужно вызвать метод renameTo, указав ему в качестве параметра ссылку на второй объект:

public boolean renameTo(File dest);

В случае успеха метод возвращает значение true, при возникновении ошибки - false. Может также возникать исключение SecurityException.

Сравнение объектов класса File

Для сравнения объектов класса File вы должны использовать метод equals:

public boolean equals(Object obj); 

Заметим, что этот метод сравнивает пути к файлам и каталогам, но не сами файли или каталоги.

Получение списка содержимого каталога

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

public String[] list();
public String[] list(FilenameFilter filter); 

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

Пример приложения, которое просматривает содержимое каталога и использует при этом фильтр, вы найдете ниже в разделе “Приложение DirList”.

Приложение FileInfo

В приложении FileInfo мы демонстрируем способы работы с классом File.

После запуска наше приложение предлагает ввести путь к файлу (рис. 2.8). Вы также можете ввести путь к каталогу.

Рис. 2.8. Работа приложения FileInfo

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

На рис. 2.8 видно, что в ответ на прглашение был введен путь к файлу autoexec.bat. Приложение вывело родительский каталог, в котором находится этот файл (диск c:), длину файла в байтах (235 байт), а также сообщило нам, что для файла разрешены операции чтения и записи.

Исходный текст приложения FileInfo

Исходный текст приложения FileInfo приведен в листинге 2.7.

Листинг 2.7. Файл FileInfo\FileInfo.java

// =========================================================
// Просмотр атрибутов файла при помощи класса File 
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;
import java.util.*;

// =========================================================
// Класс FileInfo
// Главный класс приложения
// =========================================================
public class FileInfo
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    // Введенная строка
    String sFilePath;

    try
    {
      // Выводим строку приглашения
      System.out.println("Enter file path...");
      
      // Читаем с клавиатуры строку для записи в файл
      System.in.read(bKbdInput);

      // Преобразуем введенные символы в строку типа String
      sFilePath= new String(bKbdInput, 0);

      // Отбрасываем символ конца строки
      StringTokenizer st;
      st = new StringTokenizer(sFilePath, "\r\n");
      sFilePath = new String((String)st.nextElement());

      // Создаем объект класса File, соответствующий
      // введенному пути
      File fl = new File(sFilePath);

      // Если указанный файл или каталог не существует,
      // выводим сообщение и завершаем работу
      if(!fl.exists())
      {      
        System.out.println("File not found: " + sFilePath);
      }
      
      // Если путь существует, определяем параметры
      // соответствующего файла или каталога
      else
      {
        // Проверяем, был указан файл или каталог
        if(fl.isDirectory())
          System.out.println("File " + sFilePath + 
             " is directory");

        else if (fl.isFile())
          System.out.println("File " + sFilePath + 
             " is file");

        // Получаем и выводим атрибуты файла или каталога
        System.out.println(
          "Parent: " + fl.getParent() +
          "\nLength: " + fl.length()    +
          "\nRead op. available: " + fl.canRead() +
          "\nWrite op. available: " + fl.canWrite());
      }

      System.out.println("Press <Enter> to terminate...");
      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

Описание исходного текста

После ввода с клавиатуры пути к файлу или каталогу приложение записывает введенный путь в строку sFilePath класса String.

Так как в этой строке имеется символ конца строки, нам нужно его отрезать. Для этого мы воспользуемся классом StringTokenizer, задав для него в качестве разделителя символ конца строки:

StringTokenizer st;
st = new StringTokenizer(sFilePath, "\r\n");
sFilePath = new String((String)st.nextElement());

Первый же вызов метода nextElement возвращает нам строку пути, которую мы и сохраняем в поле sFilePath.

Далее мы создаем объект класса File, передавая конструктору этого класса строку sFilePath:

File fl = new File(sFilePath);

Так как при вводе пути файла или каталога вы можете допустить ошибку, приложение, прежде чем продолжать свою работу, проверяет существование указанного файла или каталога. Для проверки вызывается метод exists, определенный в классе File:

if(!fl.exists())
{      
  System.out.println("File not found: " + sFilePath);
}

На следующем этапе приложение проверяет, является ли объект класса File каталогом, вызвая метод isDirectory:

if(fl.isDirectory())
  System.out.println("File " + sFilePath + " is directory");

Аналогичная проверка выполняется методом isFile на принадлежность объекта к файлам:

else if (fl.isFile())
  System.out.println("File " + sFilePath + " is file");

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

System.out.println(
  "Parent: " + fl.getParent() +
  "\nLength: " + fl.length()    +
  "\nRead op. available: " + fl.canRead() +
  "\nWrite op. available: " + fl.canWrite());

Параметры отображаются на консоли методом println.

Приложение DirList

В приложении DirList мы используем класс File для получения списка всех файлов и каталогов, расположенных в заданном каталоге.

После запуска приложение DirList предлагает ввести путь к каталогу и маску для имени файла (рис. 2.9).

Рис. 2.9. Работа приложения DirList

Если вместо маски задать символ ‘*’, как мы сделали это на рис. 2.9, приложение выведет полный список файлов и каталогов, выделив каталоги прописными буквами. В том случае, если будет задна другая маска, в окне появятся только такие файлы, которые содержат эту маску как подстроку (рис. 2.10).

Рис. 2.10. Просмотр содержимого каталога c:\dos с маской com

Исходный текст приложения

Исходный текст приложения DirList представлен в листинге 2.8.

Листинг 2.8. Файл DirList\DirList.java

// =========================================================
// Просмотр содержимого каталога при помощи класса File 
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;
import java.util.*;

// =========================================================
// Класс DirList
// Главный класс приложения
// =========================================================
public class DirList
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    // Путь к каталогу, содержимое которого
    // мы будем просматривать
    String sDirPath;

    // Маска для просмотра
    String sMask;

    // Массив строк содержимого каталога
    String[] dirlist;

    try
    {
      // Выводим строку приглашения для ввода пути
      // к каталогу, содержимое которого будем просматривать
      System.out.println("Enter directory path...");
      System.in.read(bKbdInput);
      sDirPath = new String(bKbdInput, 0);
      StringTokenizer st;
      st = new StringTokenizer(sDirPath, "\r\n");
      sDirPath = new String((String)st.nextElement());

      // Вводим строку маски
      System.out.println("Enter mask...");
      System.in.read(bKbdInput);
      sMask = new String(bKbdInput, 0);
      st = new StringTokenizer(sMask, "\r\n");
      sMask = new String((String)st.nextElement());
      
      // Создаем объект класса File, соответствующий
      // введенному пути
      File fdir = new File(sDirPath);

      // Если указанный каталог не существует,
      // выводим сообщение и завершаем работу
      if(!fdir.exists())
      {      
        System.out.println("Directory not found: " 
          + sDirPath);
      }
      
      // Если путь существует, определяем параметры
      // соответствующего файла или каталога
      else
      {
        // Проверяем, был указан файл или каталог
        if(!fdir.isDirectory())
          System.out.println("File " + sDirPath + 
            " is not directory");

        // Если указан каталог, получаем его содержимое
        else
        {
          // Если маска не задана, вызываем метод list
          // без параметров
          if(sMask == null)
            dirlist = fdir.list();
          
          // Вызываем метод list, создавая фильтр
          else
            dirlist = fdir.list(new MaskFilter(sMask));

          // Сканируем полученный массив
          for (int i = 0; i < dirlist.length; i++)
          {
            // Для каждого объекта, обнаруженного в 
            // каталоге, создаем объект класса File
            File f = new File(sDirPath + "\\" + dirlist[i]);
            
            // Имена файлов отображаем строчными буквами
            if(f.isFile())
              System.out.println(dirlist[i].toLowerCase());
            
            // Имена каталогов оставляем без изменения
            else
              System.out.println(dirlist[i]);
          }
        }
      }
      System.out.println("Press <Enter> to terminate...");
      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

// =========================================================
// Класс MaskFilter
// Фильтр для просмотра каталога
// =========================================================
class MaskFilter implements FilenameFilter
{
  // Поле для хранения маски имени
  String sNameMask;

  // -------------------------------------------------------
  // Конструктор класса MaskFilter
  // Сохраняет маску фильтра
  // -------------------------------------------------------
  MaskFilter(String sMask)
  {
    // Записываем маску прописными буквами
    sNameMask = sMask.toUpperCase();
  }

  // -------------------------------------------------------
  // Метод accept
  // Проверка имени по маске
  // -------------------------------------------------------
  public boolean accept(File dir, String name)
  {
    // Если маска указана как символ *, подходит любое имя
    if(sNameMask.equals("*"))
      return true;

    // Если имя содержит маску, возвращаем значение true
    return (name.indexOf(sNameMask) != -1);
  }
}

Описание исходного текста

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

System.out.println("Enter directory path...");
System.in.read(bKbdInput);
sDirPath = new String(bKbdInput, 0);
StringTokenizer st;
st = new StringTokenizer(sDirPath, "\r\n");
sDirPath = new String((String)st.nextElement());

Строка пути записывается в поле sDirPath.

Аналогичным образом вводится и обрабатывается маска, которая записывается в поле sMask.

Далее создается объект класса File, соответствующий каталогу sDirPath, содержимое которого нужно просмотреть:

File fdir = new File(sDirPath);

После этого выполняется проверка существования пути, а также проверка, указывает ли этот путь на каталог. Для проверки мы применяем методы exists и isDirectory, рассмотренные ранее.

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

if(sMask == null)
  dirlist = fdir.list();

Если же маска определена, вызывается второй вариант этого же метода:

else
  dirlist = fdir.list(new MaskFilter(sMask));

Здесь в качестве параметра методу list мы передаем вновь созданный объект класса MaskFilter (фильтр), передав соответствующему конструктору строку маски.

В любом случае метод list заполняет полученным списком массив строк dirlist. Содержимое этого массива перебирается в цикле:

for (int i = 0; i < dirlist.length; i++)
{
  File f = new File(sDirPath + "\\" + dirlist[i]);
  if(f.isFile())
    System.out.println(dirlist[i].toLowerCase());
  else
    System.out.println(dirlist[i]);
}

Для каждого элемента массива мы создаем объект класса File, передавая конструктору путь каталога, добавив к нему разделитель и строку элемента массива. Затем если данная строка соответсвует файлу, а не каталогу, имя выводится строчными буквами. Для преобразования мы вызываем метод toLowerCase, определенный в классе String.

Рассмотрим теперь класс MaskFilter, предназначенный для фильтрации имен, которые метод list возвращает вызвавшему его методу.

Класс MaskFilter определен следующим образом:

class MaskFilter implements FilenameFilter
{
  . . .
}

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

Маска передается конструктору класса MaskFilter, преобразуется им в прописные буквы и сохраняется в поле sNameMask для использования в процессе проверки методом accept:

MaskFilter(String sMask)
{
  sNameMask = sMask.toUpperCase();
}

Что же касается метода accept, то он выглядит достаточно просто:

public boolean accept(File dir, String name)
{
  if(sNameMask.equals("*"))
    return true;
  return (name.indexOf(sNameMask) != -1);
}

В качестве первого параметра этому методу передается путь к каталогу, а в качестве второго - имя файла. Метод accept вызывается для каждого файла и каталога, расположенного в каталоге dir.

Наша реализация этого метода вначале проверяет маску. Если маска задана как строка “*”, подходит любое имя, поэтому метод accept всегда возвращает значение true.

Если же используются другие маски, то наш метод выполняет ее поиск в строке имени с помощью метода indexOf. Если строка маски найдена как подстрока имени файла или каталога, такое имя нам подходит и метод accept возвращает значение true. В противном случае возвращается значение false.

Произвольный доступ к файлам

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

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

В классе RandomAccessFile определено два конструктора, прототипы которых показаны ниже:

public RandomAccessFile(String name, String mode);	
public RandomAccessFile(File file, String mode);

Первый из них позволяет указывать имя файла, и режим mode, в котором открывается файл. Второй конструктор вместо имени предполагает использование объекта класса File.

Если файл открывается только для чтения, вы должны передать конструктору текстовую строку режима "r". Если же файл открывается и для чтения, и для записи, конструктору передается строка "rw".

Позиционирование внутри файла обеспечивается методом seek, в качестве параметра pos которому передается абсолютное смещение файла:

public void seek(long pos);

После вызова этого метода текущая позиция в файле устанавливается в соответствии со значением параметра pos.

В любой момент времени вы можете определить текущую позицию внутри файла, вызвав метод getFilePointer:

public long getFilePointer();

Еще один метод, который имеет отношение к позиционированию, называется skipBytes:

public int skipBytes(int n); 

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

С помощью метода close вы должны закрывать файл, после того как работа с им завершена:

public void close();

Метод getFD позволяет получить дескриптор файла:

public final FileDescriptor getFD();

С помощью метода length вы можете определить текущую длину файла:

public long length();

Ряд методов предназначен для выполнения как обычного, так и форматированного ввода из файла. Этот набор аналогичен методам, определенным для потоков:

public int read();
public int read(byte b[]);
public int read(byte b[], int off, int len); 
public final boolean readBoolean();
public final byte    readByte();
public final char    readChar();
public final double  readDouble();
public final float   readFloat();
public final void    readFully(byte b[]);
public final void    readFully(byte b[], int off, int len); 
public final int     readInt();
public final String  readLine();
public final long    readLong();
public final short   readShort();
public final int     readUnsignedByte();
public final int     readUnsignedShort();
public final String  readUTF();

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

public void write(byte b[]);
public void write(byte b[], int off, int len); 
public void write(int b); 
public final void writeBoolean(boolean v); 
public final void writeByte(int v); 
public final void writeBytes(String s); 
public final void writeChar(int v); 
public final void writeChars(String s); 
public final void writeDouble(double v); 
public final void writeFloat(float v); 
public final void writeInt(int v); 
public final void writeLong(long v); 
public final void writeShort(int v); 
public final void writeUTF(String str);	

Имена приведенных методов говорят сами за себя, поэтому мы не будем их описывать.

Приложение DirectFileAccess

Для иллюстрации способов работы с классом RandomAccessFile мы подготовили приложение DirectFileAccess, в котором создается небольшая база данных. Эта база данных состоит из двух файлов: файла данных и файла индекса.

В файле данных хранятся записи, сосотящие из двух полей - текстового и числового. Текстовое поле с названием name хранит строки, закрытые смиволами конца строки “\r\n”, а числовое с названием account - значения типа int.

Дамп файла данных, создаваемого при первом запуске приложения DirectFileAccess, приведен на рис. 2.11.

Рис. 2.11. Дамп файла данных

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

Номер записи Смещение в файле данных Поле name Поле account
0 0 Ivanov 1000
1 12 Petrov 2000
2 24 Sidoroff 3000

При последующих запусках каждый раз в файл данных будут добавляться приведенные выше записи.

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

Рис. 2.12. Дамп файла индекса

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

После добавления трех записей в базу данных приложение извлекает три записи в обратном порядке, то есть сначала запись с номером 2, затем с номером 1, и, наконец, с номером 0. Извлеченные записи отображаются в консольном окне приложения (рис. 2.13).

Рис. 2.13. Отображение записей базы данных приложением DirectFileAccess

Исходные тексты приложения

Исходные тексты приложения DirectFileAccess приведены в листинге 2.9.

Листинг 2.9. Файл DirectFileAccess\DirectFileAccess.java

// =========================================================
// Прямой доступ к файлу с помощью класса RandomAccessFile
//
// (C) Фролов А.В, 1997
//
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//            или
//         http://www.dials.ccas.ru/frolov
// =========================================================
import java.io.*;
import java.util.*;

// =========================================================
// Класс DirectFileAccess
// Главный класс приложения
// =========================================================
public class DirectFileAccess
{
  // -------------------------------------------------------
  // main
  // Метод, получающий управление при запуске приложения
  // -------------------------------------------------------
  public static void main(String args[])
  {
    // Массив для ввода строки с клавиатуры
    byte bKbdInput[] = new byte[256];

    try
    {
      // Создаем новую базу данных
      SimpleDBMS db = new SimpleDBMS(
        "dbtest.idx", "dbtest.dat");
      
      // Добавляем в нее три записи
      db.AddRecord("Ivanov",   1000);
      db.AddRecord("Petrov",   2000);
      db.AddRecord("Sidoroff", 3000);
 
      // Получаем и отображаем содержимое первых трез
      // записей с номерами 2, 1 и 0
      System.out.println(db.GetRecordByNumber(2));
      System.out.println(db.GetRecordByNumber(1));
      System.out.println(db.GetRecordByNumber(0));

      // Закрываем базу данных
      db.close();
      
      // После ввода любой строки завершаем работу программы
      System.out.println("Press <Enter> to terminate...");
      System.in.read(bKbdInput);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }
}

// =========================================================
// Класс SimpleDBMS
// Простейшая база данных
// =========================================================
class SimpleDBMS
{
  // Файл индексов
  RandomAccessFile idx;

  // Файл данных
  RandomAccessFile dat;

  // Значение указателя на текущую запись
  long idxFilePointer = 0;

  // -------------------------------------------------------
  // SimpleDBMS
  // Конструктор. Создает и открывает файлы базы данных
  // -------------------------------------------------------
  public SimpleDBMS(String IndexFile, String DataFile)
  {
    try
    {
      // Создаем и открываем файл индексов
      idx = new RandomAccessFile(IndexFile, "rw");

      // Создаем и открываем файл данных
      dat = new RandomAccessFile(DataFile, "rw");
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }

  // -------------------------------------------------------
  // close
  // Метод close. Закрывает файлы базы данных
  // -------------------------------------------------------
  public void close()
  {
    try
    {
      // Закрываем файл индексов
      idx.close();

      // Закрываем файл данных
      dat.close();
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }

  // -------------------------------------------------------
  // AddRecord
  // Добавление записи в базу данных
  // -------------------------------------------------------
  public void AddRecord(String name, int account)
  {
    try
    {
      // Устанавливаем текущую позицию в файлах
      // индекса и данных на конец файла
      idx.seek(idx.length());
      dat.seek(dat.length());

      // Получаем смещение в файле данных места,
      // куда будет добавлена новая запись
      idxFilePointer = dat.getFilePointer();

      // Сохраняем это смещение в файле индексов
      idx.writeLong(idxFilePointer);

      // Сохраняем в файле дайнных два поля новой записи
      dat.writeBytes(name + "\r\n");
      dat.writeInt(account);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }
  }

  // -------------------------------------------------------
  // GetRecordByNumber
  // Извлечение записи по ее порядковому номеру
  // -------------------------------------------------------
  public String GetRecordByNumber(long nRec)
  {
    // Строка, в которой будет сохранена извлеченная запись
    String sRecord = "<empty>";
 
    try
    {
      // Значение поля account
      Integer account;

      // Значение поля name
      String str = null;

      // Вычисляем смещение в файле индексов по порядковому
      // номеру записи
      idx.seek(nRec * 8);

      // Извлекаем из файла индексов смещение записи
      // в файле данных
      idxFilePointer = idx.readLong();

      // Выполняем позиционирование на нужную запись
      // в файле данных
      dat.seek(idxFilePointer);
  
      // Извлекаем поля записи
      str = dat.readLine();
      account = new Integer(dat.readInt());

      // Объединяем значения полей в текстовую строку
      sRecord = new String("> " + account + ", " + str);
    }
    catch(Exception ioe)
    {
      System.out.println(ioe.toString());
    }

    // Возвращаем извлеченную запись
    return sRecord;
  }
}

Описание исходных текстов

Для работы с базой данных мы создали класс SimpleDBMS, определив в нем конструктор, методы для добавления записей, извлечения записей по их порядковому номеру, а также метод для закрытия базы данных.

Метод main

Сразу после запуска метод main приложения DirectFileAccess создает базу данных, передавая конструктору имена файла индекса dbtest.idx и файла данных dbtest.dat:

SimpleDBMS db = new SimpleDBMS("dbtest.idx", "dbtest.dat");

После этого с помощью метода AddRecord, определенного в классе SimpleDBMS, в базу добавляются три записи, состоящие из текстового и числового полей:

db.AddRecord("Ivanov",   1000);
db.AddRecord("Petrov",   2000);
db.AddRecord("Sidoroff", 3000);

Сразу после добавления записей приложение извлекает три записи с номерами 2, 1 и 0, вызывая для этого метод GetRecordByNumber, также определенный в классе SimpleDBMS:

System.out.println(db.GetRecordByNumber(2));
System.out.println(db.GetRecordByNumber(1));
System.out.println(db.GetRecordByNumber(0));

Извлеченные записи отображаются на системной консоли.

После завершения работы с базой данных она закрывается методом close из класса SimpleDBMS:

db.close();

Класс SimpleDBMS

Рассмотрим теперь класс SimpleDBMS.

В этом классе определено три поля с именами idx, dat и idxFilePointer, а также три метода.

Поля класса SimpleDBMS

Поля idx dat являются объектами класса RandomAccessFile и представляют собой, соответственно, ссылки на файл индекса и файл данных. Поле idxFilePointer типа long используется как рабочее и хранит текущее смещение в файле.

Конструктор класса SimpleDBMS

Конструктор класса SimpleDBMS выглядит достаточно просто. Все, что он делает, - это создает два объекта класса RandomAccessFile, соответственно, для индекса и данных:

idx = new RandomAccessFile(IndexFile, "rw");
dat = new RandomAccessFile(DataFile, "rw");

Так как в качестве второго параметра конструктору класа RandomAccessFile передается строка "rw", файлы открываются и для чтения, и для записи.

Метод close

Метод close закрывает файлы индекса и данных, вызывая метод close из класса RandomAccessFile:

idx.close();
dat.close();

Метод AddRecord

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

Для установки мы применили метод seek из класса RandomAccessFile, передав ему в качестве параметра значение длины файла в байтах, определенное при помощи метода length из того же класса:

idx.seek(idx.length());
dat.seek(dat.length());

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

idxFilePointer = dat.getFilePointer();
idx.writeLong(idxFilePointer);

Далее метод AddRecord выполняет сохранение полей записи в файле данных. Для записи строки вызывается метод writeBytes, а для записи численного значения типа int - метод writeInt:

dat.writeBytes(name + "\r\n");
dat.writeInt(account);

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

Метод GetRecordByNumber

Метод GetRecordByNumber позволяет извлечь произвольную запись из файла данных по ее порядковому номеру.

Напомним, что смещения всех записей хранятся в файле индексов и имеют одинаковую длину 8 байт. Пользуясь этим, метод GetRecordByNumber вычисляет смещение в файле индекса простым умножением порядкового номера записи на длину переменной типа long, то есть на 8 байт, а затем выполняет позиционирование:

idx.seek(nRec * 8);

После этого метод GetRecordByNumber извлекает из файла индексов смещение нужной записи в файле данных, вызывая для этого метод readLong, а затем выполняет позиционирование в файле данных:

idxFilePointer = idx.readLong();
dat.seek(idxFilePointer);

Поля записи читаются из файла данных в два приема. Вначале читается строка текстового поля, а затем - численное значение, для чего вызываются, соответственно, методы readLine и readInt:

str = dat.readLine();
account = new Integer(dat.readInt());

Полученные значения полей объединяются в текстовой строке и записываются в переменную sRecord:

sRecord = new String("> " + account + ", " + str);

Содержимое этой переменной метод GetRecordByNumber возвращает в качестве извлеченной строки записи базы данных.