Опубликован: 11.12.2003 | Уровень: специалист | Доступ: платный
Лекция 15:

Пакет java.io

< Лекция 14 || Лекция 15: 1234 || Лекция 16 >

Классы FilterInputStream и FilterOutputStream и их наследники

Задачи, возникающие при вводе/выводе весьма разнообразны - это может быть считывание байтов из файлов, объектов из файлов, объектов из массивов, буферизованное считывание строк из массивов и т.д. В такой ситуации решение с использованием простого наследования приводит к возникновению слишком большого числа подклассов. Более эффективно применение надстроек (в ООП этот шаблон называется адаптер) Надстройки – наложение дополнительных объектов для получения новых свойств и функций. Таким образом, необходимо создать несколько дополнительных объектов – адаптеров к классам ввода/вывода. В java.io их еще называют фильтрами. При этом надстройка-фильтр включает в себя интерфейс объекта, на который надстраивается, поэтому может быть, в свою очередь, дополнительно надстроена.

В java.io интерфейс для таких надстроек ввода/вывода предоставляют классы FilterInputStream (для входных потоков ) и FilterOutputStream (для выходных потоков ). Эти классы унаследованы от основных базовых классов ввода/вывода – InputStream и OutputStream, соответственно. Конструктор FilterInputStream принимает в качестве параметра объект InputStream и имеет модификатор доступа protected.

Классы FilterI/OStream являются базовыми для надстроек и определяют общий интерфейс для надстраиваемых объектов. Потоки-надстройки не являются источниками данных. Они лишь модифицируют (расширяют) работу надстраиваемого потока.

BufferedInputStream и BufferedOutputStream

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

BufferedInputStream содержит массив байт, который служит буфером для считываемых данных. То есть когда байты из потока считываются либо пропускаются (метод skip() ), сначала заполняется буферный массив, причем, из надстраиваемого потока загружается сразу много байт, чтобы не требовалось обращаться к нему при каждой операции read или skip. Также класс BufferedInputStream добавляет поддержку методов mark() и reset(). Эти методы определены еще в классе InputStream, но там их реализация по умолчанию бросает исключение IOException. Метод mark() запоминает точку во входном потоке, а вызов метода reset() приводит к тому, что все байты, полученные после последнего вызова mark(), будут считываться повторно, прежде, чем новые байты начнут поступать из надстроенного входного потока.

BufferedOutputStream предоставляет возможность производить многократную запись небольших блоков данных без обращения к устройству вывода при записи каждого из них. Сначала данные записываются во внутренний буфер. Непосредственное обращение к устройству вывода и, соответственно, запись в него, произойдет, когда буфер заполнится. Инициировать передачу содержимого буфера на устройство вывода можно и явным образом, вызвав метод flush(). Так же буфер освобождается перед закрытием потока. При этом будет закрыт и надстраиваемый поток (так же поступает BufferedInputStream ).

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

try {
   String fileName = "d:\\file1";
   InputStream inStream = null;
   OutputStream outStream = null;
 
   //Записать в файл некоторое количество байт
   long timeStart = System.currentTimeMillis();
   outStream = new FileOutputStream(fileName);
   outStream = new BufferedOutputStream(outStream);
   for(int i=1000000; --i>=0;) {
      outStream.write(i);
   }
   long time = System.currentTimeMillis() - timeStart;
   System.out.println("Writing time: " + time + " millisec");
   outStream.close();
 
   // Определить время считывания без буферизации
   timeStart = System.currentTimeMillis();
   inStream = new FileInputStream(fileName);
   while(inStream.read()!=-1){
   }
   time = System.currentTimeMillis() - timeStart;
   inStream.close();
   System.out.println("Direct read time: " + (time) + " millisec");

   // Теперь применим буферизацию
   timeStart = System.currentTimeMillis();
   inStream = new FileInputStream(fileName);
   inStream = new BufferedInputStream(inStream);
   while(inStream.read()!=-1){
   }
   time = System.currentTimeMillis() - timeStart;
   inStream.close();
   System.out.println("Buffered read time: " + (time) + " millisec");
} catch (IOException e) {
   System.out.println("IOException: " + e.toString());
   e.printStackTrace();
}
Пример 15.7.

Результатом могут быть, например, такие значения:

Writing time: 359 millisec
Direct read time: 6546 millisec
Buffered read time: 250 millisec
Пример 15.8.

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

Классы BufferedI/OStream добавляют только внутреннюю логику обработки запросов, но не добавляют никаких новых методов. Следующие два фильтра предоставляют некоторые дополнительные возможности для работы с потоками.

LineNumberInputStream

Класс LineNumberInputStream во время чтения данных производит подсчет, сколько строк было считано из потока. Номер строки, на которой в данный момент происходит чтение, можно узнать путем вызова метода getLineNumber(). Также можно и перейти к определенной строке вызовом метода setLineNumber(int lineNumber).

Под строкой при этом понимается набор байт, оканчивающийся либо '\n', либо '\r', либо их комбинацией '\r\n', именно в этой последовательности.

Аналогичный класс для исходящего потока отсутствует. LineNumberInputStream, начиная с версии 1.1, объявлен deprecated, то есть использовать его не рекомендуется. Его заменил класс LineNumberReader (рассматривается ниже), принцип работы которого точно такой же.

PushbackInputStream

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

PrintStream

Этот класс используется для конвертации и записи строк в байтовый поток. В нем определен метод print(…), принимающий в качестве аргумента различные примитивные типы Java, а также тип Object. При вызове передаваемые данные будут сначала преобразованы в строку вызовом метода String.valueOf(), после чего записаны в поток. Если возникает исключение, оно обрабатывается внутри метода print и дальше не бросается (узнать, произошла ли ошибка, можно с помощью метода checkError() ). При записи символов в виде байт используется кодировка, принятая по умолчанию в операционной системе (есть возможность задать ее явно при запуске JVM).

Этот класс также является deprecated, поскольку работа с кодировками требует особого подхода (зачастую у двухбайтовых символов Java старший байт просто отбрасывается). Поэтому в версии Java 1.1 появился дополнительный набор классов, основывающийся на типах Reader и Writer. Они будут рассмотрены позже. В частности, вместо PrintStream теперь рекомендуется применять PrintWriter. Однако старый класс продолжает активно использоваться, поскольку статические поля out и err класса System имеют именно это тип.

DataInputStream и DataOutputStream

До сих пор речь шла только о считывании и записи в поток данных в виде byte. Для работы с другими примитивными типами данных Java определены интерфейсы DataInput и DataOutput и их реализации – классы-фильтры DataInputStream и DataOutputStream. Их место в иерархии классов ввода/вывода можно увидеть на рис.15.1.

Интерфейсы DataInput и DataOutput определяют, а классы DataInputStream и DataOutputStream, соответственно, реализуют методы считывания и записи значений всех примитивных типов. При этом происходит конвертация этих данных в набор byte и обратно. Чтение необходимо организовать так, чтобы данные запрашивались в виде тех же типов, в той же последовательности, как и производилась запись. Если записать, например, int и long, а потом считывать их как short, чтение будет выполнено корректно, без исключительных ситуаций, но числа будут получены совсем другие.

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

try {
   ByteArrayOutputStream out = new ByteArrayOutputStream();
   DataOutputStream outData = new DataOutputStream(out);
   outData.writeByte(128); 
   // этот метод принимает аргумент int, но записывает
   // лишь младший байт
   outData.writeInt(128);
   outData.writeLong(128);
   outData.writeDouble(128);
   outData.close();
   byte[] bytes = out.toByteArray();
   InputStream in = new ByteArrayInputStream(bytes);
   DataInputStream inData = new DataInputStream(in);
   System.out.println("Чтение в правильной последовательности: ");
   System.out.println("readByte: " + inData.readByte());
   System.out.println("readInt: " + inData.readInt());
   System.out.println("readLong: " + inData.readLong());
   System.out.println("readDouble: " + inData.readDouble());
   inData.close();
   System.out.println("Чтение в измененной последовательности:");
   in = new ByteArrayInputStream(bytes);
   inData = new DataInputStream(in);
   System.out.println("readInt: " + inData.readInt());
   System.out.println("readDouble: " + inData.readDouble());
   System.out.println("readLong: " + inData.readLong());
   inData.close();
   } catch (Exception e) {
      System.out.println("Impossible IOException occurs: " +
         e.toString());
   e.printStackTrace();
}
Пример 15.9.

Результат выполнения программы:

Чтение в правильной последовательности:

readByte: -128
readInt: 128
readLong: 128
readDouble: 128.0

Чтение в измененной последовательности:

readInt: -2147483648
readDouble: -0.0
readLong: -9205252085229027328

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

< Лекция 14 || Лекция 15: 1234 || Лекция 16 >
Вадим Кудаев
Вадим Кудаев

Добрый день! Начал проходить курс "Программирование на Java". Как я понимаю,курс создавался приблизительно в 2015 году. Не потерял ли данный курс свою актуальность? Стоит ли проходить его в 2023 году, или же лучше найти что-то более новое?

Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?

Данила Бебчик
Данила Бебчик
Россия, Ставрополь
Дмитрий Логинов
Дмитрий Логинов
Москва, Московский Институт Нефти и Газа им.Губкина, 1990