Работа с потоками ввода-вывода
Стандартные классы Java, инкапсулирующие работу с данными (оболочечные классы для числовых данных, классы String и StringBuffer, массивы и т.д.), не обеспечивают поддержку чтения этих данных из файлов или запись их в файл. Вместо этого используется весьма гибкая и современная, но не очень простая технология использования потоков (Streams).
Поток представляет накапливающуюся последовательность данных, поступающих из какого-то источника. Порция данных может быть считана из потока, при этом она из потока изымается. В потоке действует принцип очереди – "первым вошел, первым вышел".
В качестве источника данных потока может быть использован как стационарный источник данных (файл, массив, строка), так и динамический – другой поток. При этом в ряде случаев выход одного потока может служить входом другого. Поток можно представить себе как трубу, через которую перекачиваются данные, причем часто в таких "трубах" происходит обработка данных. Например, поток шифрования шифрует данные, полученные на входе, и при считывании из потока передает их в таком виде на выход. А поток архивации сжимает по соответствующему алгоритму входные данные и передает их на выход. "Трубы" потоков можно соединять друг с другом – выход одного со входом другого. Для этого в качестве параметра конструктора потока задается имя переменной, связанной с потоком - источником данных для создаваемого потока.
Буферизуемые потоки имеют хранящийся в памяти промежуточный буфер, из которого считываются выходные данные потока. Наличие такого буфера позволяет повысить производительность операций ввода-вывода, а также осуществлять дополнительные операции – устанавливать метки (маркеры) для какого-либо элемента, находящегося в буфере потока и даже делать возврат считанных элементов в поток (в пределах буфера).
Абстрактный класс InputStream ("входной поток") инкапсулирует модель входных потоков, позволяющих считывать из них данные. Абстрактный класс OutputStream ("выходной поток") - модель выходных потоков, позволяющих записывать в них данные. Абстрактные методы этих классов реализованы в классах-потомках.
Таблица
7.4.
Методы класса InputStream
Метод |
Что делает |
int available() |
Текущее количество байт, доступных для чтения из потока. |
int read() |
Читает один байт из потока и возвращает его значение в виде целого, лежащего в диапазоне от 0 до 255. При достижении конца потока возвращает -1. |
int read(byte[] b) int read(byte[] b, int offset, int count)
|
Пытается прочесть b.length байт из потока в массив b. Возвращает число реально прочитанных байт. После достижения конца потока последующие считывания возвращают -1. |
long skip(long count) |
Попытка пропускать (игнорировать) count байт из потока. count , пропуска байт нет. Возвращается реально пропущенное число байт. Если это значение , пропуска байт не было. |
boolean markSupported() |
Возвращает true в случае, когда поток поддерживает операции mark и reset, иначе – false. |
void mark(int limit) |
Ставит метку в текущей позиции начала потока. Используется для последующего вызова метода reset, с помощью которого считанные после установки метки данные возвращаются обратно в поток. Эту операцию поддерживают не все потоки. См. метод markSupported. |
void reset() |
Восстанавливает предшествующее состояние данных в начале потока, возвращая указатель начала потока на помеченный до того меткой элемент. То есть считанные после установки метки данные возвращаются обратно в поток. Попытка вызова при отсутствии метки или выходе ее за пределы лимита приводит к возбуждению исключения IOException. Эту операцию поддерживают не все потоки. См. методы markSupported и mark. |
void close() |
Закрытие потока. Последующие попытки чтения из этого потока приводят к возбуждению исключения IOException. |
Все методы класса InputStream, кроме markSupported и mark, возбуждают исключение IOException – оно возникает при ошибке чтения данных.
Таблица
7.5.
Методы класса OutputStream
Метод |
Что делает |
void write(int b) |
Записывает один байт в поток. Благодаря использованию типа int можно использовать в качестве параметра целочисленное выражение без приведения его к типу byte. Напомним, что в выражениях "короткие" целые типы автоматически преобразуются к типу int. |
void write(byte[] b) void write(byte[] b, int offset, int count)
|
Записывает в поток массив байт. Если заданы параметры offset и count, записывается не весь массив, а count байт начиная с индекса offset. |
void flush() |
Форсирует вывод данных из выходного буфера и его очистку. Необходимость этой операции связана с тем, что в большинстве случаев данные уходят "во внешний мир" на запись не каждый раз после вызова write, а только после заполнения выходного буфера. Таким образом, если операции записи разделены паузой (большим интервалом времени), и требуется, чтобы уход данных из выходного буфера совершался своевременно, после последнего вызова оператора write, предшествующего паузе, надо вызывать оператор flush. |
void close() |
Закрытие потока. Последующие попытки записи в этот поток приводят к возбуждению исключения IOException. |
Все методы этого класса возбуждают IOException в случае ошибки записи.
Не все классы потоков являются потомками InputStream/OutputStream. Для чтения строк (в виде массива символов) используются потомки абстрактного класса java.io.Reader ("читатель"). В частности, для чтения из файла – класс FileReader. Аналогично, для записи строк используются классы - потомки абстрактного класса java.io.Writer ("писатель"). В частности, для записи массива символов в файл – класс FileWriter.
Имеется еще один важный класс для работы с файлами, не входящий в иерархии InputStream/OutputStream и Reader/ Writer. Это класс RandomAccessFile ("файл с произвольным доступом"), предназначенный для чтения и записи данных в произвольном месте файла. Такой файл с точки зрения класса RandomAccessFile представляет массив байт, сохраненных на внешнем носителе. Класс RandomAccessFile не является абстрактным, поэтому можно создавать его экземпляры.
Все операции чтения и записи начинаются с позиции, задаваемой файловым указателем перед началом операции. После каждой такой операции файловый указатель сдвигается к концу файла на число позиций (байт), соответствующее типу считанных данных. В случае, когда во время записи файловый указатель выходит за конец файла, размер файла автоматически увеличивается.
Таблица
7.6.
Важнейшие методы класса RandomAccessFile
Метод |
Что делает |
Общие параметры файла |
long getFilePointer() |
Возвращает значение файлового указателя - текущую позицию в файле считывающей/записывающей головки. Индексация начинается с нуля и дается в числе байт. |
long length() |
Длина файла (в байтах). |
void setLength(long newLength) |
Изменение длины файла – она устанавливается равной newLength. Если newLength меньше текущей длины, файл укорачивается путем отбрасывания его конца. При этом если файловый указатель находился в отбрасываемой части, он устанавливается в конец файла, иначе его позиция не меняется. Если newLength больше текущей длины, файл увеличивает размер путем добавления в конец нового пространства, содержимое которого не гарантируется. |
void close() |
Закрытие потока. Последующие попытки доступа к этому потоку приводят к возбуждению исключения IOException. |
Побайтные операции установки позиции |
void seek(long pos) |
Установка файлового указателя в позицию pos. |
int skipBytes(int n) |
Пропуск n байт – передвижение считывающей/записывающей головки на n байт. Если , то вперед, к концу файла. Если , то позиция головки не меняется. Возвращается число реально пропущенных байт. Оно может быть меньше n – например, из-за достижения конца файла. |
Побайтные операции чтения |
int read() |
Читает один байт из потока и возвращает его значение в виде целого, лежащего в диапазоне от 0 до 255. При достижении конца потока возвращает -1. |
int read(byte[] b) |
Пытается прочесть b.length байт из потока в массив b. Возвращает число реально прочитанных байт. После достижения конца потока последующие считывания возвращают -1. |
int read(byte[] b, int offset, int count) |
Пытается прочесть count байт из потока в массив b, записывая их начиная с позиции offset. Возвращает число реально прочитанных байт. После достижения конца потока последующие считывания возвращают -1. |
void readFully(byte[] b) |
Считывает из файла b.length байт, начиная с текущей позиции файлового указателя, и заполняет весь буферный массив b. |
void readFully(byte[] b, int offset, int count) |
Считывает из файла в массив count байт, записывая их начиная с байта, имеющего позицию offset. |
Побайтные операции записи |
void write(int b) |
Запись в файл одного байта b. То же, что writeByte(b). |
void write(byte[] b) |
Запись в файл всего массива байт b, т.е. b.length байт. |
void write(byte[] b, int offset, int count) |
Запись в файл count байт массива b начиная с байта массива, имеющего индекс offset. |
Чтение одиночных значений примитивного типа |
boolean readBoolean() |
Считывает значение boolean
|
byte readByte() |
Считывает значение byte
|
short readShort() |
Считывает значение short
|
int readInt() |
Считывает значение int
|
long readLong() |
Считывает значение long
|
int readUnsignedByte() |
Считывает значение типа "беззнаковый байт"(тип отсутствует в Java) и преобразует в значение типа int
|
int readUnsignedShort() |
Считывает значение типа "беззнаковое короткое целое"(тип отсутствует в Java) и преобразует в значение типа int
|
char readChar() |
Считывает значение char
|
float readFloat() |
Считывает значение float
|
double readDouble() |
Считывает значение double
|
Запись одиночных значений примитивного типа |
void writeBoolean(boolean v) |
Запись в файл значение boolean
|
void writeByte(int v) |
Запись в файл значение byte. Благодаря использованию типа int можно использовать в качестве параметра целочисленное выражение без приведения его к типу byte. |
void writeShort(int v) |
Запись в файл значение short. Благодаря использованию типа int можно использовать в качестве параметра целочисленное выражение без приведения его к типу short. |
void writeInt(int v) |
Запись в файл значение int
|
void writeLong(long v) |
Запись в файл значение long
|
void writeChar(int v) |
Запись в файл значение char. Благодаря использованию типа int можно использовать в качестве параметра целочисленное выражение без приведения его к типу char. |
void writeFloat(float v) |
Запись в файл значение float
|
void writeDouble(double v) |
Запись в файл значение double
|
Чтение отдельных строк |
String readLine() |
Считывает из файла строку символов от текущей позиции файлового указателя до места переноса на новую строку (символ возврата каретки или последовательность "\n" ) или конца файла. При этом старшие байты получившихся в строке символов UNICODE оказываются равными нулю, т.е. поддерживается только часть кодовой таблицы. Метод полезен для считывания текста, записанного в кодировке ANSI. |
String readUTF |
Считывает из файла строку символов в кодировке UTF-8 от текущей позиции файлового указателя. Число последующих считываемых байт строки в кодировке UTF-8 задается первыми двумя считанными байтами. |
Запись отдельных строк |
void writeBytes(String s) |
Запись в файл строки s как последовательности байт, соответствующих символам строки. При этом каждому символу соответствует один записываемый байт, так как у каждого символа UNICODE отбрасывается старший из двух байт ( записывается младший байт). Используется для записи символов в кодировке ANSI. См. также метод writeChars, в котором записываются оба байта. |
void writeChars(String s) |
Запись в файл строки s как последовательности байт, соответствующих символам строки. При этом каждому символу UNICODE соответствует два записываемых байта. |
void writeUTF(String s) |
Запись в файл строки s как последовательности байт, соответствующих символам строки в кодировке UTF-8. |
Все методы класса RandomAccessFile, кроме getChannel(), возбуждают исключение IOException – оно возникает при ошибке записи или ошибке доступа к данным.
В конструкторе класса требуется указать два параметра. Первый – имя файла или файловую переменную, второй – строку с модой доступа к файлу. Имеются следующие варианты:
- "r"- от read,- "только читать"
- "rw"- от read and write,- "читать и писать"
- "rws"- от read and write synchronously,- "читать и писать с поддержкой синхронизации" (см. далее раздел про потоки, Threads)
- "rwd"- от read and write to device,- "читать и писать с поддержкой синхронизации устройства" (см. далее раздел про потоки, Threads)
Например:
- java.io.RandomAccessFile rf1=new java.io.RandomAccessFile("q.txt","r");
- java.io.RandomAccessFile rf2=new java.io.RandomAccessFile(file,"rw");
Рассмотрим примеры, иллюстрирующие работу с потоками.
Пример чтения текста из файла:
File file;
javax.swing.JFileChooser fileChooser=new javax.swing.JFileChooser();
javax.swing.filechooser.FileFilter fileFilter=new SimpleFileFilter(".txt");
private void openMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
fileChooser.addChoosableFileFilter(fileFilter);
if(fileChooser.showOpenDialog(null)!=fileChooser.APPROVE_OPTION){
return;//Нажали Cancel
};
file = fileChooser.getSelectedFile();
try{
InputStream fileInpStream=new FileInputStream(file);
int size=fileInpStream.available();
fileInpStream.close();
char[] buff=new char[size];
Reader fileReadStream=new FileReader(file);
int count=fileReadStream.read(buff);
jTextArea1.setText(String.copyValueOf(buff));
javax.swing.JOptionPane.showMessageDialog(null,
"Прочитано "+ count+" байт");
fileReadStream.close();
} catch(Exception e){
javax.swing.JOptionPane.showMessageDialog(null,
"Ошибка чтения из файла \n"+file.getAbsolutePath());
}
}
Переменной fileInpStream, которая будет использована для работы потока FileInputStream, мы задаем тип InputStream. Это очень характерный прием при работе с потоками, позволяющий при необходимости заменять в дальнейшем входной поток с FileInputStream на любой другой без изменения последующего кода. После выбора имени текстового файла с помощью файлового диалога мы создаем файловый поток:
fileInpStream=new FileInputStream(file);
Функция fileInpStream.available() возвращает число байт, которые можно считать из файла. После ее использования поток fileInpStream нам больше не нужен, и мы его закрываем. Поток FileReader не поддерживает метод available(), поэтому нам пришлось использовать поток типа FileInputStream.
Массив buff используется в качестве буфера, куда мы считываем весь файл.
Если требуется читать файлы большого размера, используют буфер фиксированной длины и в цикле считывают данные из файла блоками, равными длине буфера, до тех пор, пока число считанных байт не окажется меньше размера буфера. Это означает, что мы дошли до конца файла. В таком случае выполнение цикла следует прекратить. При такой организации программы нет необходимости вызывать метод available() и использовать поток типа FileInputStream.
В операторе
Reader fileReadStream=new FileReader(file);
используется тип Reader, а не FileReader, по той же причине, что до этого мы использовали InputStream, а не FileInputStream.
Построчное чтение из файла осуществляется аналогично, но содержимое блока try следует заменить на следующий код:
FileReader filReadStream=new FileReader(file);
BufferedReader bufferedIn=new BufferedReader(filReadStream);
String s="",tmpS="";
while((tmpS=bufferedIn.readLine())!=null)
s+=tmpS+"\n";
jTextArea1.setText(s);
bufferedIn.close();
Пример записи текста в файл очень похож на пример чтения, но гораздо проще, так как не требуется вводить промежуточные буферы и потоки:
private void saveAsMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
fileChooser.addChoosableFileFilter(fileFilter);
if(fileChooser.showSaveDialog(null)!=fileChooser.APPROVE_OPTION){
return;//Нажали Cancel
};
file = fileChooser.getSelectedFile();
try{
Writer filWriteStream=new FileWriter(file);
filWriteStream.write(jTextArea1.getText() );
filWriteStream.close();
} catch(Exception e){
javax.swing.JOptionPane.showMessageDialog(null,
"Ошибка записи в файл \n"+file.getAbsolutePath());
}
}