Чтение из потоков вывода
Содержание
1 Введение
2 Использование оперативной памяти
3 Использование фильтров при чтении
4 Пример применения фильтров
5 Ссылки
Введение
Запись I/O обозначает ввод и вывод. Через ввод/вывод программа взаимодействует с внешним миром. Ввод - это чтение, а вывод - запись. На платформе JavaTM I/O основан на модели потоков. Эта модель позволяет вам читать из файлов, сетевых соединений или с консолей одним и тем же способом. Вам не нужно менять код программы в зависимости от типа устройства ввода. Это справедливо и для устройств вывода.
Основанная на потоках модель работает хорошо при чтении из входного потока и записи в выходной поток. Однако, существуют дополнительные моменты, которые нужно учитывать тогда, когда вам необходимо получить данные, записанные в поток вывода, или записать данные в поток ввода для последующего чтения. Эта статья рассматривает некоторые из этих моментов.
Потоки ввода используются для чтения. Программа открывает поток ввода для чтения информации из источника. Потоки вывода используются для записи. Чтобы записать информацию на устройство вывода, программа открывает поток вывода для этого устройства и затем записывает информацию в этот поток.
Пакет java.io package содержит различные классы потоков для чтения из потока и для записи в поток. Эти классы можно разделить на две категории: символьные потоки и байтовые потоки. Классы символьных потоков предназначены для чтения и записи символьных данных. Классы байтовых потоков предназначены для записи и чтения бинарных данных (то есть, байтов). Reader и Writer являются абстрактными суперклассами для байтовых потоков. Подклассы этих суперклассов реализуют потоки для конкретных источников и выходных устройств.
Иногда возникает вопрос, как прочитать информацию, только что записанную в поток? Например, предположим, что вы записали что-либо в поток вывода. Для этого вы передали какому-то методу библиотеки OutputStream или Writer. Как вам теперь прочитать из OutputStream или Writer то, что вы записали?
Одним из способов прочитать записанную информацию является передача методу библиотеки один из связанных с файлом потоков вывода: FileOutputStream или FileWriter. Это даст вам возможность повторно прочитать записанные в файловую систему данные. Вот пример:
import java.io.*;
public class FileRead {
public static void main(String args[]) throws IOException {
// создать временный файл для вывода
File file = File.createTempFile("zuk", ".tmp");
// удалить в конце программы
file.deleteOnExit();
// создать поток вывода для файла
Writer writer = new FileWriter(file);
// передать данные в поток вывода
save(writer);
// закрыть вывод
writer.close();
// Открыть вывод как ввод
Reader reader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(reader);
// прочитать ввод
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
private static void save(Writer generic) throws IOException {
PrintWriter out = new PrintWriter(generic);
out.println("Line One");
out.println("Line Two");
}
}
В программе FileRead создается связанный с файлом поток вывода (FileWriter), записывается две строки в поток, открывает вывод как поток ввода и выполняет чтение из потока ввода. Программа должна отобразить прочитанные повторно строки:
Line One
Line Two
Этот подход используется в случаях, когда объем выводимых данных больше доступной памяти. В этих случаях вы все равно должны записать выходные данные во внешний источник.
Использование оперативной памяти
Если промежуточная информация относительно не велика по размерам, вы можете использовать классы потоков, использующие оперативную память. Эти классы используют оперативную память для чтения и записи данных (в отличие от таких потоков как FileWriter, которые используют для этих операций внешний источник). Вы можете использовать библиотеку, передающую информацию в соответствующий класс потока, использующего оперативную память: для вывода это может быть либо ByteArrayOutputStream либо StringWriter. Затем, для чтения информации в памяти, можно преобразовать этот поток в поток ввода: ByteArrayInputStream или StringReader.
Потоки вывода ByteArrayOutputStream и StringWriter основаны на внутреннем массиве байтов и StringBuffer для хранения промежуточной информации. Во время ввода ByteArrayInputStream и StringReader работают с массивом и String для ввода данных. Это означает, что когда вам необходимо изменить режим с вывода на ввод, вы получите текущее содержимое выходного устройства для создания входного устройства.
В случае байтовых потоков ByteArrayOutputStream и ByteArrayInputStream работают вместе. После окончания записи вы получаете байты с помощью метода toByteArray(). Передавая байты в ByteArrayInputStream, вы заполните поток ввода:
// Создать поток вывода
ByteArrayOutputStream outputStream =
new ByteArrayOutputStream(initialSize);
// записать в поток
...
// после окончания получить байты
byte bytes[] = outputStream.toByteArray();
// создать поток ввода
ByteArrayInputStream inputStream =
new ByteArrayInputStream(bytes);
// прочитать из потока
...
Существует несколько моментов, которые надо учитывать при использовании ByteArrayOutputStream. Во-первых, хорошо, если вы можете оценить размер вывода. Если вы не укажете начальный размер, байтовый массив будет иметь размер 32 байта и увеличиваться в степени два каждый раз при заполнении внутреннего буфера (увеличение происходит даже более быстро, если вы записываете массив вместо символа). Если вам известно, что будет по крайней мере 2000 символов, начните с этого размера и вы избежите изменений размеров на 64, 128, 256, 512, 1024 и 2048 символов. Еще один момент - метод toByteArray() не возвращает ссылку на внутренний массив байтов. Вместо этого он возвращает копию. Это может быть и хорошо и плохо. Копирование предохраняет от изменения буфера, но существует два набора данных и тратится в два раза больше памяти.
Для символьных потоков существуют StringWriter и StringReader. StringWriter использует внутренний StringBuffer для управления чтением символов. Программа для символьных потоков похожа на код для байтовых потоков:
// создать поток вывода
StringWriter writer =
new StringWriter(initialSize);
// записать в поток
...
// после окончания получить байты
String string = writer.toString();
// создать поток ввода
StringReader reader =
new StringReader(string);
// прочитать из потока
...
StringWriter использует символьный массив для внутреннего хранения. Символьный массив находится в StringBuffer. При заполнении массива он увеличивается в размерах с тем же самым эффектом дублирования, описанным ранее для ByteArrayOutputStream. Если вы не укажете начальный размер, массив StringBuffer начнет с размера всего 16 символов. Как всегда, укажите наиболее реальный размер массива. Обратите внимание, что можно получить содержимое StringBuffer, используемого классом StringWriter, без распределения дополнительной памяти. Более подробно об этом описано в документации по методу toString класса StringBuffer.
Использование пары StringWriter-StringReader дает возможность изменить приведенный ранее пример и выполнять все обращения в памяти:
import java.io.*;
public class MemRead {
public static void main(String args[]) throws IOException {
// создать поток вывода в памяти
StringWriter writer = new StringWriter(128);
// передать данные в поток вывода
save(writer);
// закрыть вывод
writer.close();
// открыть вывод как ввод
Reader reader = new StringReader(writer.toString());
BufferedReader bufferedReader = new BufferedReader(reader);
// прочитать ввод
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
}
private static void save(Writer generic) throws IOException {
PrintWriter out = new PrintWriter(generic);
out.println("Line One");
out.println("Line Two");
}
}
Использование фильтров при чтении
Существует, по крайней мере, еще один способ чтения из потока вывода. Этот способ использует фильтры. И байтовые и символьные потоковые классы позволяют установку фильтров в потоки ввода/вывода. Оставляя без изменений основные операции чтения и записи, фильтры обогащают потоки новыми возможностями. Класс BufferedReader, использовавшийся в предыдущих программах, является примером фильтра. Он обрабатывает входные данные во внутреннем буфере и передает по запросу символы в считыватель. Вместо обращения к источнику данных для каждого запроса, BufferedReader считывает входные данные в память для увеличения производительности. Поскольку программа MemRead использует буфер, расположенный в оперативной памяти, нет различий в реальной производительности. Для программы FileRead есть отличие в производительности, хотя для данных небольшого размера оно минимально.
Фильтры обычно встраиваются в последовательность обработки следующим образом: оригинальный источник или назначение передается в конструктор, затем фильтр выполняет свою обработку перед (или после) передачей байтов или символов в оригинальный источник или назначение. Подкласс фильтра может быть либо существующим классом, либо одним из фильтрующих потоков. Классы фильтрующих потоков зависят от типа фильтруемых данных: FilterInputStream, FilterOutputStream, FilterReader или FilterWriter.
Фильтры в программе применяются так же, как было показано ранее для BufferedReader, то есть:
SourceStream source = new SourceStream(...);
AFilterStream filter = new AFilterStream(source);
// использовать фильтр
...
// закрыть фильтр, не источник
filter.close();
Ниже приведена программа, использующая фильтр для подсчета символов, цифр и пробелов. При закрытии фильтра он записывает счетчики в поток, посылаемый в конструктор:
import java.io.*;
public class CountWriter extends FilterWriter {
PrintStream out;
int chars, nums, whites;
public CountWriter(Writer destination, PrintStream out) {
super(destination);
this.out = out;
}
public void write(int c) throws IOException {
super.write(c);
check((char) c);
}
public void write(char cbuf[], int off, int len) throws IOException {
super.write(cbuf, off, len);
for (int i = off; i < len; i++) {
check(cbuf[i]);
}
}
public void write(String str, int off, int len) throws IOException {
super.write(str, off, len);
for (int i = off; i < len; i++) {
check(str.charAt(i));
}
}
private void check(char ch) {
if (Character.isLetter(ch)) {
chars++;
} else if (Character.isDigit(ch)) {
nums++;
} else if (Character.isWhitespace(ch)) {
whites++;
}
}
public void close() {
out.println("Chars: " + chars);
out.println("Nums: " + nums);
out.println("Whitespace: " + whites);
}
}
Пример применения фильтров
Изменим приведенную выше программу для использования фильтра. Обратите внимание, что вам не надо повторно считывать данные для выполнения необходимого вывода:
import java.io.*;
public class MemRead2 {
public static void main(String args[]) throws IOException {
// создать поток вывода в памяти
StringWriter writer = new StringWriter(128);
CountWriter counter = new CountWriter(writer, System.err);
// передать данные в поток вывода
save(counter);
// закрыть поток вывода
counter.close();
}
private static void save(Writer generic) throws IOException {
PrintWriter out = new PrintWriter(generic);
out.println("Line One");
out.println("Line Two");
}
}
После выполнения программы вы должны получить следующий результат:
Chars: 14
Nums: 0
Whitespace: 4
Вот фактически и все, что касается чтения из потоков вывода. Вы можете либо использовать прямой подход - чтение полностью записанного вывода, либо перехватывать вывод во время записи для выполнения вашей операции чтения.
Ссылки
Библиотеки New I/O Java 1.4 предоставляет дополнительные механизмы для создания буферов чтения/записи. Обратитесь к статье "Возможности New I/0 для Java 2 Standard Edition 1.4" для информации по работе с новыми возможностями буферизации.