Основные навыки и понятия
- Представление о потоках ввода-вывода
- Отличия байтовых и символьных потоков
- Классы для поддержки байтовых потоков
- Классы для поддержки символьных потоков
- Представление о встроенных потоках
- Применение байтовых потоков
- Использование байтовых потоков для файлового ввода-вывода
- Автоматическое закрытие файлов с помощью оператора try с ресурсами
- Чтение и запись двоичных данных
- Манипулирование файлами с произвольным доступом
- Применение символьных потоков
- Использование символьных потоков для файлового ввода-вывода
- Применение оболочек типов Java для преобразования символьных строк в числа
В примерах программ, приводившихся в предыдущих главах, уже применялись отдельные части системы ввода-вывода в Java, в частности метод println(), но делалось это без каких-либо формальных пояснений. Система ввода-вывода основана в Java на иерархии классов, поэтому ее функции и особенности нельзя было представлять до тех пор, пока не были рассмотрены классы, наследование и исключения. А теперь настал черед и для средств ввода-вывода.
Приступая к изучению системы ввода вырода Java, приготовьтесь к длительной и кропотливой работе. Система ввода-вывода в Java довольно обширна и содержит немало классов, интерфейсов и методов. Объясняется это, в частности, тем, что в Java, по существу, определены две полноценные системы ввода-вывода: одна — для обмена байтами, другая — для обмена символами. Здесь нет возможности рассмотреть все аспекты ввода-вывода в Java, ведь для этого бы потребовалась отдельная книга. Поэтому в данной главе будут рассмотрены лишь наиболее важные и часто используемые языковые средства ввода-вывода. Правда, элементы системы ввода-вывода в Java тесно взаимосвязаны, и поэтому, уяснив основы, вы легко освоите все остальные свойства этой системы.
Прежде чем приступать к рассмотрению системы ввода-вывода, необходимо сделать следующее замечание. Классы, описанные в этой главе, предназначены для консольного и файлового ввода-вывода. Они не применяются для создания графических пользовательских интерфейсов. Поэтому ими не имеет смысла пользоваться при создании оконных приложений. Для графических интерфейсов предусмотрены другие языковые средства. Они будут представлены в главе 14 при рассмотрении апплетов, а также в главе 15, служащей введением в библиотеку Swing. (Swing — это современный набор инструментальных средств, ориентированных на создание графических пользовательских интерфейсов приложений.)
Ввод-вывод в программах на Java осуществляется посредством потоков. Поток — это некая абстракция производства или потребления информации. С физическим устройством поток связывает система ввода-вывода. Все потоки действуют одинаково — даже если они связаны с разными физическими устройствами. Поэтому классы и методы ввода-вывода могут применяться к самым разным типам устройств. Например, методами вывода на консоль можно пользоваться и для вывода в файл на диске. Для реализации потоков используется иерархия классов, содержащихся в пакете java.io.
В современных версиях Java определены два типа потоков: байтовые и символьные. (В первоначальных версиях Java были доступны только байтовые потоки, тогда как символьные потоки были реализованы в дальнейшем.) Байтовые потоки предоставляют удобные средства для ввода и вывода байтов. Они используются, например, при чтении и записи двоичных данных. В особенности они полезны для обращения с файлами. А символьные потоки ориентированы на обмен символьными данными. В них применяется кодировка в уникоде (Unicode), а следовательно, программы, в которых используются символьные потоки, легко поддаются локализации на разные языки мира. В некоторых случаях символьные потоки обеспечивают более высокую эффективность по сравнению с байтовыми.
Необходимость поддерживать два разных типа потоков ввода-вывода привела к созданию двух иерархий классов (одна для байтовых, другая для символьных данных). Из-за того что число классов достаточно велико, на первый взгляд система ввода-вывода кажется сложнее, чем она есть на самом деле. Но не следует забывать, что функциональные возможности для байтовых потоков дублируются соответствующими средствами для символьных потоков.
Следует также иметь в виду, что на самом нижнем уровне все средства ввода-вывода имеют байтовую организацию. А символьные потоки лишь предоставляют удобные и эффективные инструменты для обработки символов.
Для определения байтовых потоков служат две иерархии классов. На их вершине находятся два абстрактных класса: InputStream и OutputStream. В классе InputStream определены свойства, общие для байтовых потоков ввода, а в классе OutputStream — свойства, общие для байтовых потоков вывода.
Производными от классов InputStream и OutputStream являются конкретные подклассы, реализующие различные функциональные возможности и учитывающие особенности обмена данными с разными устройствами, например ввода-вывода в файлы на диске. Классы байтовых потоков приведены в табл. 10.1. Не следует пугаться большого количества этих классов: изучив один из них, легко освоить остальные.
Таблица 10.1. Классы байтовых потоков
Класс байтового потока | Описание |
---|---|
BufferedlnputStream | Буферизованный поток ввода |
BufferedOutputStream | Буферизованный поток вывода |
ByteArrayInputStream | Поток ввода для чтения из байтового массива |
ByteArrayOutputStream | Поток вывода для записи в байтовый массив |
DatalnputStream | Поток ввода с методами для чтения стандартных типов данных Java |
DataOutputStream | Поток вывода с методами для записи стандартных типов данных Java |
FileInputStream | Поток ввода для чтения из файла |
FileOutputStream | Поток вывода для записи в файл |
FilterlnputStream | Подкласс, производный от класса InputStream |
FilterOutputStream | Подкласс, производный от класса OutputStream |
InputStream | Абстрактный класс, описывающий потоковый ввод |
ObjectInputStream | Поток для ввода объектов |
ObjectOutputStream | Поток для вывода объектов |
OutputStream | Абстрактный класс, описывающий потоковый вывод |
PipedlnputStream | Поток конвейерного ввода |
PipedOutputStream | Поток конвейерного вывода |
PrintStream | Поток вывода с методами print() и println() |
PushbacklnputStream | Поток ввода с возвратом прочитанных байтов в поток |
RandomAccessFile | Класс, поддерживающий файловый ввод-вывод с произвольным доступом |
SequenceInputStream | Поток ввода, сочетающий в себе несколько потоков ввода для поочередного чтения данных из них |
Для определения символьных потоков служат две иерархические структуры классов, на вершине которых находятся абстрактные классы Reader и Writer соответственно. Класс Reader и его подклассы используются для чтения, а класс Writer и его подклассы — для записи данных. Конкретные классы, производные от классов Reader и Writer, оперируют символами в уникоде.
Классы, производные от классов Reader и Writer, предназначены для выполнения различных операций ввода-вывода символов. Символьные классы присутствуют в Java параллельно с байтовыми классами. Классы символьных потоков приведены в табл. 10.2. Таблица 10.2. Классы символьных потоков
Класс символьного потока | Описание |
---|---|
BufferedReader | Буферизованный поток ввода символов |
BufferedWriter | Буферизованный поток вывода символов |
CharArrayReader | Поток ввода для чтения из символьного массива |
CharArrayWriter | Поток вывода для записи в символьный массив |
FileReader | Поток ввода для чтения символов из файла |
FileWriter | Поток вывода для записи символов в файл |
FilterReader | Класс для чтения символов с фильтрацией |
FilterWriter | Класс для записи символов с фильтрацией |
InputStreamReader | Поток ввода с преобразованием байтов в символы |
LineNumberReader | Поток ввода с подсчетом символьных строк |
OutputStreamWriter | Поток вывода с преобразованием символов в байты |
PipedReader | Поток конвейерного ввода |
PipedWriter | Поток конвейерного вывода |
PrintWriter | Поток вывода с методами print() и println() |
PushbackReader | Поток ввода с возвратом прочитанных символов в поток |
Reader | Абстрактный класс, описывающий потоковый ввод символов |
StringReader | Поток ввода для чтения из символьной строки |
StringWriter | Поток вывода для записи в символьную строку |
Writer | Абстрактный класс, описывающий потоковый вывод символов |
Как вам должно быть уже известно, во все программы на Java автоматически импортируется пакет java. lang. В этом пакете определен класс System, инкапсулирующий некоторые элементы среды выполнения программ. Помимо прочего, в нем содержатся предопределенные переменные in, out и err, представляющие стандартные потоки ввода-вывода. Эти поля объявлены как public, final и static. А это означает, что ими можно пользоваться в любой другой части программы, не ссылаясь на конкретный объект типа System.
Переменная System.out ссылается на поток стандартного вывода. По умолчанию этот поток связан с консолью. А переменная System, in ссылается на поток стандартного ввода (по умолчанию с клавиатуры). И наконец, переменная System.err ссылается на поток стандартных сообщений об ошибках, которые по умолчанию выводятся на консоль. По мере необходимости все эти потоки могут быть перенаправлены на другие совместимые устройства ввода-вывода.
Поток System.in представляет собой объект типа InputStream, а потоки System.out и System.err — объекты типа PrintStream. Хотя эти потоки обычно используются для чтения и записи символов, они на самом деле являются байтовыми потоками. Дело в том, что эти потоки были определены в первоначальной спецификации Java, где символьные потоки вообще не были предусмотрены. Как станет ясно в дальнейшем, для этих потоков можно по необходимости создать оболочки, превратив их в символьные потоки.
Начнем рассмотрение системы ввода-вывода в Java с байтовых потоков. Как пояснялось ранее, на вершине иерархии байтовых потоков находятся классы InputStream и OutputStream. Методы из класса InputStream приведены в табл. 10.3, а методы из класса OutputStream — в табл. 10.4. При возникновении ошибок в процессе выполнения методы из классов InputStream и OutputStream могут генерировать исключения типа IOException. Методы, определенные в этих двух абстрактных классах, доступны во всех подклассах. Таким образом, они формируют минимальный набор функций ввода-вывода, общих для всех байтовых потоков.
Таблица 10.3. Методы, определенные в классе InputStream
Метод | Описание |
---|---|
int available() |
Возвращает количество байтов, доступных для чтения |
void close() |
Закрывает поток ввода. При последующей попытке чтения из потока генерируется исключение IOException |
void mark(int numBytes) |
Ставит отметку на текущей позиции в потоке. Отметка доступна до тех пор, пока на будет прочитано количество байтов, определяемое параметром numBytes |
boolean markSupported() |
Возвращает логическое значение true, если методы mark() и reset() поддерживаются в вызывающем потоке |
int read() |
Возвращает целочисленное представление следующего байта в потоке. Если достигнут конец потока, возвращается значение -1 |
int read(byte buffer[]) |
Предпринимает попытку прочитать количество байтов, определяемое выражением buffer, length, в массив buffer и возвращает фактическое количество успешно прочитанных байтов. Если достигнут конец потока, возвращается значение -1 |
int read(byte buffer[], int offset, int numBytes) |
Предпринимает попытку прочитать количество байтов, определяемое параметром numBytes, в массив buffer, начиная с элемента buffer[offset]. Если достигнут конец потока, возвращается значение -1 |
void reset() |
Устанавливает указатель ввода на помеченной ранее позиции |
long skip (long numBytes) |
Пропускает количество байтов, определяемое параметром numBytes, в потоке ввода. Возвращает фактическое количество пропущенных байтов |
Таблица 10.4. Методы, определенные в классе OutputStream
Метод | Описание |
---|---|
void close() |
Закрывает выходной поток. При последующей попытке записи в поток генерируется исключение IOExceptionВыводит содержимое выходного буфера вывода в |
void flush() |
Выводит содержимое выходного буфера вывода в целевой поток. По завершении этой операции выходной буфер очищается |
void write(int b) |
Записывает один байт в поток вывода. Параметр b относится к типу int, что позволяет вызывать данный метод в выражениях, не приводя результат их вычисления к типу byte |
void write(byte buffer[]) |
Записывает массив в поток вывода |
void write(byte buffer[], int offset, int numBytes) |
Записывает в поток вывода часть массива buffer длиной numBytes байтов, начиная с элемента buffer[offset] |
Первоначально единственный способ реализовать консольный ввод в Java состоял в использовании байтовых потоков, и во многих программах на Java до сих пор используются исключительно потоки данного типа. В настоящее время для разработки прикладных программ доступны как байтовые, так и символьные потоки. В коммерческих приложениях для чтения данных с консоли в основном используются символьные потоки. Такой подход упрощает локализацию программ и их сопровождение. Ведь намного удобнее оперировать непосредственно символами и не тратить время и труд на преобразование символов в байты, а байтов — в символы. Но в простых служебных и прикладных программах, где данные, введенные с клавиатуры, обрабатываются непосредственно, удобно пользоваться байтовыми потоками. Именно поэтому они здесь и рассматриваются.
Поток System, in является экземпляром класса InputStream, и благодаря этому обеспечивается автоматический доступ к методам, определенным в классе InputStream. К сожалению, в классе InputStream определен только один метод ввода, read(), предназначенный для чтения байтов. Ниже приведены разные формы объявления этого метода.
int read() throws IOException
int read(byte data[]) throws IOException
int read(byte data[], int start, int max) throws IOException
В главе 3 было показано, как пользоваться первой формой метода read() для ввода отдельных символов с клавиатуры (а по существу, из потока стандартного ввода System, in). Достигнув конца потока, этот метод возвращает значение -1. Вторая форма метода read() предназначена для чтения данных из потока ввода в массив data. Чтение завершается по достижении конца потока, па заполнении массива или при возникновении ошибки. Метод возвращает количество прочитанных байтов или значение -1, если достигнут конец потока. И третья форма данного метода позволяет разместить прочитанные данные в массиве data, начиная с элемента, обозначаемого индексом start. Максимальное количество байтов, которые могут быть введены в массив, определяется параметром max. Метод возвращает число прочитанных байтов или значение -1, если достигнут конец потока. При возникновении ошибки в каждой из этих форм метода read() генерируется исключение IOException. Условие конца потока ввода System, in устанавливается при нажатии клавиши .
Ниже приведен краткий пример программы, демонстрирующий чтение байтов из потока ввода System, in в массив. Следует иметь в виду, что исключения, которые могут быть сгенерированы при выполнении данной программы, обрабатываются за пределами метода main(). Такой подход часто используется при чтении данных с консоли. По мере необходимости вы сможете самостоятельно организовать обработку ошибок.
// Чтение байтов с клавиатуры в массив,
import java.io.*;
class ReadBytes {
public static void main(String args[])
throws IOException {
byte data[] = new byte[10];
System.out.println("Enter some characters.");
// Чтение данных, введенных с клавиатуры,
// и размещение их в байтовом массиве.
System.in.read(data);
System.out.print("You entered: ");
for(int i=0; i < data.length; i++)
System.out.print((char) data[i]);
}
}
Выполнение этой программы дает например, следующий результат:
Enter some characters.
Read Bytes
You entered: Read Bytes
Как и для консольного ввода, в Java для консольного вывода первоначально были предусмотрены только байтовые потоки. Но уже в версии Java 1.1 были реализованы символьные потоки. Именно их и рекомендуется применять в прикладных программах, особенно в тех случаях, когда необходимо добиться переносимости кода. Но поскольку System, out является байтовым потоком вывода, он по-прежнему широко используется для побайтового вывода данных на консоль. Именно такой подход до сих пор применялся в примерах, представленных в этой книге. Поэтому он здесь и рассматривается.
Вывести данные на консоль проще всего с помощью уже знакомых вам методов print() и println(). Эти методы определены в классе PrintStream (на объект данного типа ссылается переменная потока стандартного вывода System.out). Несмотря на то что System, out является байтовым потоком вывода, пользоваться им вполне допустимо для организации элементарного вывода данных на консоль.
Класс PrintStream представляет собой выходной поток, производный от класса OutputStream, и поэтому в нем также реализуется метод write() низкоуровневого вывода. Следовательно, этот метод может быть использован для вывода данных на консоль. Самая простая форма метода write(), определенного в PrintStream, имеет следующий вид:
void write(int byteval)
Этот метод записывает в поток байтовое значение, указываемое в качестве параметра byteval. Несмотря на то что этот параметр объявлен как int, учитываются только 8 младших битов его значения. Ниже приведен простой пример программы, где метод write() используется для вывода символов S и новой строки на консоль.
// Применение метода System.out.write() .
class WriteDemo {
public static void main(String args[]) {
int b;
b = 'S';
// Вывод байтов на экран.
System.out.write(b);
System.out.write('\n');
}
}
На практике для вывода на консоль метод write.() применяется достаточно редко. Для этой цели намного удобнее пользоваться методами print() и println(). В классе PrintStream реализованы два дополнительных метода, printf() и format(), которые позволяют управлять форматом выводимых данных. Например, при выводе можно указать количество десятичных цифр, минимальную ширину поля или способ представления отрицательных числовых значений. И хотя эти методы не используются в примерах, представленных в данной книге, вам стоит обратить на них пристальное внимание, поскольку они могут оказаться очень полезными при написании прикладных программ.
В Java предоставляется большое количество классов и методов, позволяющих читать и записывать данные в файлы. Разумеется, чаще всего приходится обращаться к файлам, хранящимся на дисках. В Java все файлы имеют байтовую организацию, и поэтому для побайтового чтения и записи данных в такие файлы предоставляются соответствующие методы. Следовательно, организовывать чтение и запись данных в файлы из байтовых потоков приходится довольно часто. Кроме того, для байтовых потоков ввода-вывода в файлы в Java разрешено создавать специальные оболочки в виде символьных объектов. Такие оболочки будут рассмотрены далее в этой главе.
Для того чтобы создать байтовый поток и связать его с файлом, следует воспользоваться классом FilelnputStream или FileOutputStream. А для открытия файла достаточно создать объект одного из этих классов, передав имя файла конструктору в качестве параметра. В открытый файл можно записывать данные или читать их из него.
Файл открывается для ввода созданием объекта типа FilelnputStream. Для этой цели чаще всего используется приведенная ниже форма объявления конструктора данного класса. FilelnputStream(String имя_файла) throws FileNotFoundException
В качестве параметра этому конструктору передается имя_файла, который требуется открыть. Если указанный файл не существует, генерируется исключениеFileNotFoundException.
Для чтения данных из файла служит метод read(). Ниже приведена форма объявления этого метода, которой мы будем пользоваться в дальнейшем,
int read() throws IOException
При каждом вызове метод read() читает байт из файла и возвращает его как целочисленное значение. По достижении конца файла этот метод возвращает значение -1. При возникновении ошибки метод генерирует исключение IOException. Как видите, в этой форме метод read() выполняет те же самые действия, что и одноименный метод для ввода данных с консоли.
Завершив операции с файлом, следует закрыть его с помощью метода close(), общая форма объявления которого выглядит следующим образом:
void close() throws IOException
При закрытии файла освобождаются связанные с ним системные ресурсы, чтобы использовать их для работы с другим файлом. Если же файл не будет закрыт, могут произойти “утечки памяти” из-за того, что часть памяти остается выделенной для неиспользуемых ресурсов. Ниже приведен пример программы, где метод read() используется для ввода содержимого текстового файла. Имя файла задается с помощью параметра в командной строке при запуске программы на выполнение. Полученные данные выводятся на экран. Обратите внимание на то, что ошибки ввода-вывода обрабатываются с помощью блока try/catch.
/* Отображение текстового файла.
При вызове этой программы следует указать имя файла,
содержимое которого требуется просмотреть.
Например, для вывода на экран содержимого файла TEST.TXT,
в командной строке нужно указать следующее:
java ShowFile TEST.TXT
*/
import java.io.*;
class ShowFile {
public static void main(String args[])
{
int i;
FilelnputStream fin;
// Прежде всего следует убедиться, что файл был указан,
if(args.length != 1) {
System.out.println("Usage: ShowFile File");
return;
}
try {
// Открытие файла.
fin = new FilelnputStream(args[0]);
} catch(FileNotFoundException exc) {
System.out.println("File Not Found");
return;
}
try {
// читать из файла до тех пор, пока не встретится знак EOF.
do {
// Чтение из файла.
i = fin.read();
if(i != -1) System.out.print((char) i) ;
// Если значение переменной i равно -1,значит,
// достингут конец файла.
} while (i != -1);
} catch(IOException exc) {
System.out.println("Error reading file.");
}
try {
// Закрытие файла.
fin.close();
} catch(IOException exc) {
System.out.println("Error closing file.");
}
}
}
В приведенном выше примере поток ввода из файла закрывается после того, как чтение данных из файла завершается в блоке try. Такой способ оказывается удобным не всегда, и поэтому в Java предоставляется более совершенный и чаще употребляемый способ. А состоит он в вызове метода close() в блоке finally. В этом случае все методы, получающие доступ к файлу, помещаются в блок try, а для закрытия файла используется блок finally. Благодаря этому файл закрывается независимого от того, как завершится блок try. Если продолжить предыдущий пример, то блок try, в котором выполняется чтение из файла, можно переписать следующим образом:
try {
do {
i = fin.read();
if(i != -1) System.out.print((char) i) ;
} while(i != —1) ;
} catch(IOException exc) {
System.out.println("Error Reading File");
// Блок finally используется для закрытия файла.
} finally {
// закрыть файл при выходе из блока try.
try {
fin.close();
} catch(IOException exc) {
System.out.println("Error Closing File");
}
}
Преимущество рассмотренного выше способа состоит, в частности, в том, что если программа, получающая доступ к файлу, завершается аварийно из-за какой-нибудь ошибки ввода-вывода, генерирующей исключение, файл все равно закрывается в блоке finally. И если с аварийным завершением простых программ, как в большинстве примеров в этой книге, из-за неожиданно возникающей исключительной ситуации еще можно как-то мириться, то в крупных программах подобная ситуация вряд ли вообще допустима. Именно ее и позволяет исключить блок finally.
Иногда оказывается проще заключить в оболочку те части программы, в которых открывается файл, чтобы получить доступ к нему из единственного блока try, не разделяя его на два блока, а для закрытия файла использовать отдельный блок finally. В качестве примера ниже приведена переделанная версия рассмотренной выше программы ShowFile.
/* В этой версии программы отображения текстового файла код,
открывающий файл и получающий к нему доступ, заключается
в единственный блок try. А закрывается файл в блоке finally.
*/
import java.io.*;
class ShowFile {
public static void main(String args[])
{
int i;
FilelnputStream fin = null;
// Прежде всего следует убедиться, что файл был указан,
if (args.length != 1) {
System.out.println("Usage: ShowFile filename");
return;
}
// В следующем коде открывается файл, из которого читаются
// символы до тех пор, пока не встретится знак EOF, а затем
// файл закрывается в блоке finally,
try {
fin = new FilelnputStream(args[0]);
do {
i = fin.read() ;
if(i != -1) System.out.print((char) i);
} while(i != -1);
} catch(FileNotFoundException exc) {
System.out.println("File Not Found.");
} catch(IOException exc) {
System.out.println("An I/O Error Occurred");
} finally {
// Файл закрывается в любом случае,
try {
if (fin != null) fin.closeO;
} catch(IOException exc) {
System.out.println("Error Closing File");
}
}
}
}
Обратите внимание на то, что переменная fin инициализируется пустым значением null. А в блоке finally файл закрывается только в том случае, если значение переменной fin не является пустым. Такой способ оказывается вполне работоспособным, поскольку переменная fin не будет содержать пустое значение лишь в том случае, если файл был успешно открыт. Следовательно, метод close() не будет вызываться, если во время открытия файла возникнет исключение.
В приведенном выше примере блок try/catch можно сделать более компактным. Ведь исключение FileNotFoundException является подклассом исключения IOException, и поэтому его не нужно перехватывать отдельно. В качестве примера ниже приведен блок оператора catch, которым можно воспользоваться для перехвата обоих этих исключений, не прибегая к перехвату исключения FileNotFoundException в отдельности. В данном случае выводится стандартное сообщение о возникшем исключении с описанием характера ошибки.
} catch(IOException exc) {
System.out.println("I/O Error: " + exc);
} finally {
...
В рассматриваемом здесь способе любая ошибка, в том числе и ошибка открытия файла, будет обработана единственным оператором catch. Благодаря своей компактности именно такой способ применяется в большинстве примеров ввода-вывода, представленных в этой книге. Следует, однако, иметь в виду, что он может оказаться не вполне пригодным в тех случаях, когда требуется отдельно обрабатывать ошибку открытия файла, например, вследствие того, что пользователь введет имя файла с опечаткой. В подобных случаях рекомендуется выдать сначала приглашение правильно ввести имя файла, а затем перейти к блоку try для доступа к файлу.
### Вывод в файл
Для того чтобы открыть файл для вывода, следует создать объект типа FileOutputStream. Ниже приведены два наиболее часто употребляемых конструктора этого класса.
FileOutputStream(String имя_файла) throws FileNotFoundException FileOutputStream(String имя_файлаг boolean append) throws FileNotFoundException
Если файл не может быть создан, возникает исключение FileNotFoundException. В первой форме конструктора при открытии файла удаляется существовавший ранее файл с таким именем. Вторая форма отличается наличием параметра append. Если этот параметр принимает логическое значение true, записываемые данные добавляются в конец файл. В противном случае старые данные в файле перезаписываются новыми.
Для того чтобы записать данные в файл, следует вызвать метод write(). Наиболее простая форма этого метода приведена ниже,
void write(int byteval) throws IOException
Этот метод записывает в поток байтовое значение, указанное в качестве параметра byteval. Несмотря на то что этот параметр объявлен как int, учитываются только 8 младших битов его значения. Если в процессе записи возникнет ошибка, будет сгенерировано исключение IOException.
По завершении работы с файлом его нужно закрыть с помощью метода close(). Объявление этого метода выглядит следующим образом:
void close() throws IOException
При закрытии файла освобождаются связанные с ним системные ресурсы, чтобы использовать их для работы с другим файлом. Процедура закрытия файла также гарантирует, что данные, оставшиеся в буфере, будут записаны на диск.
В приведенном ниже примере программы осуществляется копирование текстового файла. Имена исходного и целевого файлов указываются в командной строке.
/* Копирование текстового файла. При вызове этой программы следует указать имя исходного и целевого файлов. Например, для копирования файла FIRST.TXT в файл SECOND.TXT в командной строке нужно указать следующее: java CopyFile FIRST.TXT SECOND.TXT / import java.io.; class CopyFile { public static void main(String args[]) { int i; FilelnputStream fin; FileOutputStream fout;
// Прежде всего следует убедиться, что оба файла были указаны,
if(args.length !=2 ) {
System.out.println("Usage: CopyFile From To");
return;
}
// открыть исходный файл
try {
fin = new FilelnputStream(args[0] ) ;
} catch(FileNotFoundException exc) {
System.out.println("Input File Not Found");
return;
}
// открыть целевой файл
try {
fout = new FileOutputStream(args[1]);
} catch(FileNotFoundException exc) {
System.out.println("Error Opening Output File");
// закрыть исходный файл
try {
fin.close();
} catch(IOException exc2) {
System.out.println("Error closing input file.");
}
return;
}
// копировать файл
try {
do {
// Чтение байтов из одного файла и запись их в другой файл.
i = fin.read();
if(i != -1) fout.write (i);
} while(i != -1);
} catch(IOException exc) {
System.out.println("File Error");
}
try {
fin.close() ;
} catch(IOException exc) {
System.out.println("Error closing input file.");
}
try {
fout.close();
} catch(IOException exc) {
System.out.println("Error closing output file.");
}
} }
## Автоматическое закрытие файлов
В примерах программ из предыдущего раздела метод с 1 о s е () вызывался явным образом для закрытия файла, когда он уже не был больше нужен. Подобным образом файлы закрывались с тех пор, как появилась первая версия Java. В итоге именно такой способ получил широкое распространение в существующих программах на Java. И до сих пор он остается вполне обоснованным и пригодным. Но в JDK 7 внедрено новое средство, предоставляющее другой, более рациональный способ управления ресурсами, в том числе и потоками файлового ввода-вывода, автоматизирующий процесс закрытия файлов. Этот способ основывается на новой разновидности оператора try, называемой оператором try с ресурсами, а иногда еще — автоматическим управлением ресурсами. Главное преимущество оператора try с ресурсами заключается в том, что он предотвращает ситуации, в которых файл (или другой ресурс) неумышленно остается неосвобожденным после того, как он уже больше не нужен. Как пояснялось ранее, если не позаботиться вовремя о закрытии файла в программе, это может привести к утечкам памяти и прочим осложнениям в работе программы.
Ниже приведена общая форма оператора try с ресурсами
try (описание_ресурса) { // использовать ресурс }
где описание_ресурса обозначает оператор, в котором объявляется и инициализируется конкретный ресурс, например файл. По существу, он содержит объявление переменной, в котором переменная инициализируется ссылкой на объект управляемого ресурса. По завершении блока try объявленный ресурс автоматически освобождается. Если этим ресурсом является файл, то он автоматически закрывается, что избавляет от необходимости вызывать метод close() явным образом. В блок оператора try с ресурсами могут также входить операторы catch и finally.
Оператор try с ресурсами можно применять только к тем ресурсам, в которых реализуется интерфейс AutoCloseable, определенный в пакете java. lang. Этот интерфейс внедрен в JDK 7, и в нем определен метод close(). Интерфейс AutoCloseable наследует от интерфейса Close able, определенного в пакете j ava. io. Оба интерфейса реализуются классами потоков, в том числе FilelnputStream и FileOutputStream. Следовательно, оператор try с ресурсами может применяться вместе с потоками, включая и потоки файлового ввода-вывода.
В качестве примера ниже приведена переделанная версия программы ShowFile, в которой оператор try с ресурсами применяется для автоматического закрытия файла.
/* В этой версии программы ShowFile оператор try с ресурсами применяется для автоматического закрытия файла, когда он уже больше не нужен. Примечание: для компиляции этого кода требуется JDK 7 или более поздняя версия данного комплекта. / import java.io.; class ShowFile { public static void main(String args[]) { int i; // Прежде всего следует убедиться, что оба файла были указаны, if(args.length != 1) { System.out.println("Usage: ShowFile filename"); return; }
// Ниже оператор try с ресурсами применяется сначала для открытия, а
// затем для автоматического закрытия файла после выхода из блока try.
try(FilelnputStream fin = new FilelnputStream(args[0])) {
// Блок оператора try с ресурсами,
do {
i = fin.read();
if (i != -1) System.out.print((char) i) ;
} while(i != -1);
} catch(IOException exc) {
System.out.println("I/O Error: " + exc);
}
} }
Особое внимание в данной программе обращает на себя следующая строка кода, в которой файл открывается в операторе try с ресурсами.
try(FilelnputStream fin = new FilelnputStream(args[0])) {
Как видите, в той части оператора try с ресурсами, где указывается конкретный ресурс, объявляется переменная fin типа FilelnputStream, которой затем присваивается ссылка на файл как объект, открываемый конструктором класса FilelnputStream. Следовательно, в данной версии программы переменная fin является локальной для блока try и создается при входе в этот блок. А при выходе из блока try файл, связанный с переменной fin, автоматически закрывается с помощью неявно вызываемого метода close(). Это означает, что метод close() не нужно вызывать явным образом, а следовательно, он избавляет от необходимости помнить, что файл нужно закрыть. Именно в этом и заключается главное преимущество автоматического управления ресурсами.
Следует иметь в виду, что ресурс, объявляемый в операторе try с ресурсами, неявно считается как final. Это означает, что ресурс нельзя присвоить после того, как он был создан. Кроме того, область действия ресурса ограничивается блоком оператора try с ресурсами.
С помощью одного оператора try с ресурсами можно управлять несколькими ресурсами. Для этого достаточно указать каждый из них через точку с запятой. В качестве примера ниже приведена переделанная версия рассмотренной ранее программы CopyFile. В этой версии оператор с ресурсами используется для управления переменными fin и fout, ссылающимися на два ресурса (в данном случае — оригинал и копию файла).
/* В этой версии программы CopyFile используется оператор try с ресурсами. В ней демонстрируется управление двумя ресурсами (в данном случае — файлами) с помощью единственного оператора try.
Примечание: для компиляции этого кода требуется JDK 7 или более поздняя версия данного комплекта. / import java.io.; class CopyFile { public static void main.(String args[] ) throws IOException { int i; // Прежде всего следует убедиться, что оба файла были указаны, if(args.length != 2) { System.out.println("Usage: CopyFile from to"); return; }
// открыть оба файла для управления с помощью оператора try
try (FilelnputStream fin = new FilelnputStream(args[0]);
FileOutputStream fout = new FileOutputStream(args[1]))
// Управление двумя ресурсами (в данном случае — файлами).
{
do {
i = fin.read();
if(i != -1) fout.write(i);
} whiled ! = -1) ;
} catch(IOException exc) {
System.out.println("I/O Error: " + exc);
}
} } Обратите внимание на то, каким образом входной и выходной файлы открываются в операторе try с ресурсами, как показано ниже.
try (FilelnputStream fin = new FilelnputStream(args[0]);
FileOutputStream fout = new FileOutputStream(args[1]))
{
По завершении этого блока try оба файла, на которые ссылаются переменные fin и fout, закрываются автоматически. Если сравнить эту версию программы с предыдущей, то можно заметить, что ее исходный код намного компактнее. Возможность писать более компактный код является еще одним, дополнительным преимуществом оператора try с ресурсами.
Следует также упомянуть о еще одной особенности оператора try с ресурсами. Вообще говоря, когда выполняется блок try, в нем может возникнуть одно исключение, приводящее к другому исключению при закрытии ресурса в блоке finally. И если это блок обычного оператора try, то исходное исключение теряется, прерываясь вторым исключением. А в блоке оператора try с ресурсами второе исключение подавляется. Но оно не теряется, а добавляется в список подавленных исключений, связанных с первым исключением. Этот список можно получить, вызвав метод get Suppressed(), определенный в классе Throwable.
В силу упомянутых выше преимуществ, присущих оператору try с ресурсами, можно ожидать, что он найдет широкое применение в программировании на Java. Поэтому именно он и будет использоваться в остальных примерах программ, представленных далее в этой главе. Но не менее важным остается и умение пользоваться рассмотренным ранее традиционным способом освобождения ресурсов с помощью вызываемого явным образом оператора close(). И на то имеется ряд веских оснований. Во-первых, уже существует немало написанных и повсеместно эксплуатируемых программ на Java, в которых применяется традиционный способ управления ресурсами. Поэтому все программирующие на Java должны как следует усвоить и уметь пользоваться этим традиционным способом для сопровождения устаревшего кода. Во-вторых, переход на JDK 7 может произойти не сразу, а следовательно, придется работать с предыдущей версией данного комплекта. В этом случае воспользоваться преимуществами оператора try с ресурсами не удастся и придется применять традиционный способ управления ресурсами. И наконец, в некоторых классах закрытие ресурса явным образом может оказаться более пригодным, чем его автоматическое освобождение. Но, несмотря на все сказанное выше, новый способ автоматического управления ресурсами считается более предпочтительным при переходе к JDK 7 или более поздней версии данного комплекта, поскольку он рациональнее и надежнее традиционного способа.
В приведенных до сих пор примерах программ читались и записывались байтовые значения, содержащие символы в коде ASCII. Но аналогичным образом можно также организовать чтение и запись любых типов данных. Допустим, требуется создать файл, содержащий значения типа int, double или short. Для чтения и записи простых типов данных в Java предусмотрены классы DatalnputStream и DataOutputStream.
Класс DataOutputStream реализует интерфейс DataOutput, в котором определены методы, позволяющие записывать в файл значения любых простых типов. Следует, однако, иметь в виду, что данные записываются во внутреннем двоичном формате, а не в виде последовательности символов. Методы, наиболее часто применяемые для записи простых типов данных в Java, приведены в табл. 10.5. Каждый из них генерирует исключение IOException при возникновении ошибки ввода-вывода.
Таблица 10.5. Наиболее часто употребляемые методы вывода данных, определенные в классе DataOutputStream
Метод вывода данных | Описание |
---|---|
void writeBoolean (boolean val) |
Записывает логическое значение, определяемое параметром val |
void writeByte (int,val) |
Записывает младший байт целочисленного значения, определяемого параметром val |
void writeChar (int,val) |
Записывает значение, определяемое параметром val, интерпретируя его как символ |
void writeDouble (double val) |
Записывает значение типа double, определяемое параметром val |
void writeFloat (float val) |
Записывает значение типа float, определяемое параметром val |
void writelnt(int val) |
Записывает значение типа int, определяемое параметром val |
void writeLong (long val) |
Записывает значение типа long, определяемое параметром val |
void writeShort (int val) |
Записывает целочисленное значение, определяемое параметром val, преобразуя его в тип short |
Ниже приведен конструктор класса DataOutputStream. Обратите внимание на то, что при вызове ему передается экземпляр класса OutputStream.
DataOutputStream(OutputStream OutputStream)
где OutputStream — это поток вывода, в который записываются данные. Для того чтобы организовать запись данных в файл, следует передать конструктору в качестве параметра OutputStream объект типа FileOutputStream.
Класс DatalnputStream реализует интерфейс Datalnput, в котором объявлены методы для чтения всех простых типов данных в Java (табл. 10.6). В каждом из этих методов может быть сгенерировано исключение IOException при возникновении ошибки ввода-вывода. В качестве своего основания класс DatalnputStream использует экземпляр класса InputStream, перекрывая его методами для чтения различных типов данных в Java. Однако в потоке типа DatalnputStream данные читаются в двоичном виде, а не в удобной для чтения форме. Ниже приведен конструктор класса DatalnputStream.
DatalnputStream(InputStream inputStream)
где inputStream — это поток, связанный с создаваемым экземпляром класса DatalnputStream. Для того чтобы организовать чтение данных из файла, следует передать конструктору в качестве параметра inputStream объект типа FilelnputStream.
Таблица 10.6. Наиболее часто употребляемые методы ввода данных, определенные в классе DatalnputStream
Метод ввода данных | Описание |
---|---|
boolean readBoolean() |
Читает значение типа boolean |
byte readByte() |
Читает значение типа byte |
char readChar() |
Читает значение типа char |
double readDouble() |
Читает значение типа double |
float readFloat() |
Читает значение типа float |
int readlnt() |
Читает значение типа int |
long readLong() |
Читает значение типа long |
short readShort() |
Читает значение типа short |
Ниже приведен пример программы, демонстрирующий применение классов DataOutputStream и DatalnputStream. В этой программе данные разных типов сначала записываются в файл, а затем читаются из файла.
// Запись и чтение двоичных данных.Для компиляции этого кода
// требуется JDK 7 или более поздняя версия данного комплекта.
import java.io.*;
class RWData {
public static void main(String args[])
{
int i = 10;
double d = 1023.56;
boolean b = true;
// записать ряд значений
try (DataOutputStream dataOut =
new DataOutputStream(new FileOutputStream("testdata")))
{
// Запись двоичных данных в файл testdata.
System.out.println("Writing " + i) ;
dataOut.writelnt(i);
System.out.println("Writing " + d) ;
dataOut.writeDouble(d);
System.out.println("Writing " + b);
dataOut.writeBoolean(b);
System.out.println("Writing " + 12.2 * 7.4);
dataOut.writeDouble(12.2 * 7.4);
}
catch(IOException exc) {
System.out.println("Write error.");
return;
}
System.out.println() ;
// а теперь прочитать записанные значения
try (DatalnputStream dataln =
new DatalnputStream(new FilelnputStream("testdata")))
{
// Чтение двоичных данных из файла testdata.
i = dataln.readlnt();
System.out.println("Reading " + i) ;
d = dataln.readDouble();
System.out.println("Reading " + d);
b = dataln.readBoolean() ;
System.out.println("Reading " + b);
d = dataln.readDouble();
System.out.println("Reading " + d) ;
}
catch(IOException exc) {
System.out.println("Read error.");
}
}
}
Выполнение этой программы дает следующий результат:
Writing 10
Writing 1023.56
Writing true
Writing 90.28
Reading 10
Reading 1023.56
Reading true
Reading 90.28
Пример для опробования 10.1. Утилита сравнения файлов
В этом проекте предстоит создать простую, но очень полезную утилиту для сравнения содержимого файлов. В ходе выполнения этой сервисной программы сначала открываются два сравниваемых файла, а затем данные читаются из них и сравниваются по соответствующему количеству байтов. Если на какой-то стадии операция сравнения дает отрицательный результат, это означает, что содержимое обоих файлов не одинаково. Если же конец обоих файлов достигается одновременно, это означает, что они содержат одинаковые данные.
Последовательность действий
- Создайте файл CompFiles.java.
- Введите в файл CompFiles.java приведенный ниже исходный код.
/*
Для того чтобы воспользоваться этой программой, укажите
имена сравниваемых файлов в командной строке, например:
java CompFile FIRST.TXT SECOND.TXT
Для компиляции этого кода требуется JDK 7
или более поздняя версия данного комплекта.
*/
import java.io.*;
class CompFiles {
public static void main(String args[])
{
int i=0, j=0;
// Прежде всего следует убедиться, что файлы были указаны,
if(args.length !=2 ) {
System.out.println("Usage: CompFiles fl f2");
return;
}
// сравнить файлы
try (FilelnputStream fl = new FilelnputStream(args[0]);
FilelnputStream f2 = new FilelnputStream(args[1]))
{
// проверить содержимое каждого файла
do {
i = f1.read();
j = f2.read();
if(i != j) break;
} while (i != -1 && j != -1) ;
if(i != j)
System.out.println("Files differ.");
else
System.out.println("Files are the same.");
} catch(IOException exc) {
System.out.println("I/O Error: " + exc);
}
}
}
- Для опробования программы скопируйте сначала файл CompFiles. java во временный файл temp, а затем введите в командной строке следующее:
java CompFiles CompFiles.java temp
- Программа сообщит, что файлы одинаковы. Далее сравните файл CompFiles.java с рассмотренным ранее файлом CopyFile. j ava, введя в командной строке следующее:
java CompFiles CompFiles.java CopyFile.java
- Эти файлы содержат разные данные, о чем и сообщит программа CompFiles.
- Попробуйте самостоятельно внедрить в программу CompFiles ряд дополнительных возможностей. В частности, введите в нее возможность выполнять сравнение без учета регистра символов. Программу CompFiles можно также доработать таким образом, чтобы она выводила место, где обнаружено первое отличие сравниваемых файлов.
До сих пор нам приходилось иметь дело с последовательными файлами, содержимое которых вводилось и выводилось побайтно, т.е. строго по порядку. Но в Java предоставляется также возможность обращаться к хранящимся в файле данным в произвольном порядке. Для этой цели предусмотрен класс RandomAccessFile, инкапсулирующий файл с произвольным доступом. Класс RandomAccessFile не является производным от класса InputStream или OutputStream. Вместо этого он реализует интерфейсы Datalnput и DataOutput, в которых объявлены основные методы ввода-вывода. В нем поддерживаются также запросы позиционирования, т.е. возможность задавать положение указателя файла произвольным образом. Ниже приведен конструктор класса RandomAccessFile, которым мы будем пользоваться далее.
RandomAccessFile(String имя_файла, String доступ)
throws FileNotFoundException
Здесь конкретный файл указывается с помощью параметра имя_файла, а параметр доступ определяет, какой именно тип доступа будет использоваться для обращения к файлу. Если параметр доступ принимает значение "г", то данные могут читаться из файла, но не записываться в него. Если же указан тип доступа "rw", то файл открывается как для чтения, так и для записи.
Метод seek(), общая форма объявления которого приведена ниже, служит для установки текущего положения указателя файла,
void seek(long newPos) throws IOException
Здесь параметр newPos определяет новое положение указателя файла в байтах относительно начала файла. Операция чтения или записи, следующая после вызова метода seek(), будет выполняться относительно нового положения указателя файла.
В классе RandomAccessFile определены методы read() и write(). Этот класс также реализует интерфейсы Datalnput и DataOuput, т.е. в нем доступны методы чтения и записи простых типов, например readlnt() и writeDouble().
Ниже приведен пример программы, демонстрирующий ввод-вывод с произвольным доступом. В этой программе шесть значений типа double сначала записываются в файл, а затем читаются из него, причем порядок чтения их отличается от порядка записи.
// Демонстрация произвольного доступа к файлам.
// Для компиляции этого кода требуется JDK 7
// или более поздняя версия данного комплекта.
import java.io.*;
class RandomAccessDemo {
public static void main(String args[])
{
double data[] = { 19.4, 10.1, 123.54, 33.0, 87.9, 74.25 };
double d;
// открыть и использовать файл с произвольным доступом
// Файл с произвольным доступом открывается для записи и чтения.
try (RandomAccessFile raf = new RandomAccessFile("random.dat", "rw"))
{
// записать значения в Файл
for(int i=0; i < data.length; i++) {
raf.writeDouble(data[i]);
}
//а теперь прочитать отдельные значения из файла
// Для установки указателя файла служит метод seek().
raf.seek(0); // найти первое значение типа double
d = raf.readDouble();
System.out.println("First value is " + d) ;
raf.seek(8); // найти второе значение типа double
d = raf.readDouble();
System.out.println("Second value is " + d) ;
raf.seek(8 * 3); // найти четвертое значение типа double
d = raf.readDouble();
System.out.println("Fourth value is " + d);
System.out.println();
// а теперь прочитать значения через одно
System.out.println("Here is every other value: ");
for(int i=0; i < data.length; i+=2) {
raf.seek(8 * i); // найти i-e значение типа double
d = raf.readDouble();
System.out.print(d + " ") ;
}
}
catch(IOException exc) {
System.out.println("I/O Error: " + exc) ;
}
}
}
Результат выполнения данной программы выглядит следующим образом:
First value is 19.4
Second value is 10.1
Fourth value is 33.0
Here is every other value:
19.4 123.54 87.9
Обратите внимание на расположение каждого числового значения. Ведь значение типа double занимает 8 байтов, и поэтому каждое последующее число начинается на 8-байтовой границе предыдущего числа. Иными словами, первое числовое значение начинается на позиции нулевого байта, второе — на позиции 8-го байта, третье — на позиции 16-го байта и т.д. Поэтому для чтения четвертого числового значения нужно установить указатель файла на позиции 24-го байта при вызове метода seek().
Как следует из предыдущих разделов этой главы, байтовые потоки в Java довольно эффективны и удобны в употреблении. Но что касается ввода-вывода символов, то байтовые потоки далеки от идеала. Поэтому для этих целей в Java определены классы символьных потоков. На вершине иерархии классов, поддерживающих символьные потоки, находятся абстрактные классы Reader и Writer. Методы класса Reader приведены в табл. 10.7, а методы класса Writer — в табл. 10.8. В большинстве этих методов может быть сгенерировано исключение IOException. Методы, определенные в указанных абстрактных классах Reader и Writer, доступны во всех их подклассах. Таким образом, они образуют минимальный набор функций ввода-вывода, необходимых для всех символьных потоков.
Таблица 10.7. Методы, определенные в классе Reader
Метод | Описание |
---|---|
abstract void close() |
Закрывает поток ввода данных. При последующей попытке чтения генерируется исключение IOException |
void mark (int numChars) |
Ставит отметку на текущей позиции в потоке. Отметка доступна до тех пор, пока на будет прочитано количество символов, определяемое параметром numChars |
boolean markSupported() |
Возвращает логическое значение true, если поток поддерживает методы mark() и reset() |
int read() |
Возвращает целочисленное представление очередного символа из потока ввода. Если достигнут конец потока, возвращается значение -1 |
int read(char buffer[]) |
Предпринимает попытку прочитать количество байтов, определяемое выражением buffer, length, в массив buffer и возвращает фактическое количество успешно прочитанных символов. Если достигнут конец потока, возвращается значение -1 |
abstract int read(char buffer[], int offset, int numChars) |
Предпринимает попытку прочитать количество символов, определяемое параметром numChars, в массив buffer, начиная с элемента buffer [ offset]. Если достигнут конец потока, возвращается значение -1 |
int read(CharBuffer buffer) |
Предпринимает попытку заполнить буфер, определяемый параметром buffer, символами, прочитанными из входного потока. Если достигнут конец потока, возвращается значение -1. CharBuffer — это класс, представляющий последовательность символов, например строку |
boolean ready() |
Возвращает логическое значение true, если следующий запрос на получение символа может быть выполнен немедленно. В противном случае возвращается логическое значение false |
void reset() |
Устанавливает указатель ввода на помеченной ранее позиции |
long skip(long numChars) |
Пропускает количество символов, определяемое параметром numChars, в потоке ввода. Возвращает фактическое количество пропущенных символов |
Таблица 10.8. Методы, определенные в классе Writer
Метод | Описание |
---|---|
Writer append(char ch) |
Записывает символ ch в конец текущего потока. Возвращает ссылку на поток |
Writer append(CharSequence chars) |
Записывает символы chars в конец текущего потока. Возвращает ссылку на поток. CharSequence — это интерфейс, в котором описаны только операции чтения последовательности символов |
Writer append(CharSequence chars, int begin, int end) |
Записывает символы chars в конец текущего потока, начинаяс позиции, определяемой параметром begin, и кончая позицией, определяемой параметром end. Возвращает ссылку на поток. CharSequence — это интерфейс, в котором описаны только операции чтения последовательности символов |
abstract void close() |
Закрывает поток вывода. При последующей попытке записи в поток генерируется исключение IOException |
abstract void flush() |
Выводит текущее содержимое буфера на устройство. В результате выполнения данной операции буфер очищается |
void write(int ch) |
Записывает в вызывающий поток вывода один символ. Параметр ch относится к типу int, что позволяет вызывать данный метод в выражениях, не приводя результат их вычисления к типу char |
void write(char buffer[]) |
Записывает в вызывающий поток вывода массив символов buffer |
abstract void write(char buffer[], int offset, int numChars) |
Записывает в вызывающий поток вывода количество символов, определяемое параметром numChars, из массива buffer, начиная с элемента buffer[ offset ] |
void write(String str) |
Записывает в вызывающий поток вывода символьную строку str |
void write(String str, int offset, int numChars) |
Записывает в вызывающий поток вывода часть numChars символов из строки str, начиная с позиции, обозначаемой параметром offset |
Если программа подлежит локализации, то при организации ввода с консоли символьным потокам следует отдать предпочтение перед байтовыми. А поскольку System.in — это байтовый поток, то для него придется построить оболочку в виде класса, производного от класса Reader. Наиболее подходящим для ввода с консоли является класс Buf feredReader, поддерживающий буферизованный поток ввода. Но объект типа Buf feredReader нельзя построить непосредственно из потока стандартного ввода System, in. Сначала нужно преобразовать байтовый поток в символьный. И для этой цели служит класс InputStreamReader, преобразующий байты в символы. Для того чтобы получить объект типа InputStreamReader, связанный с потоком стандартного ввода System, in, нужно воспользоваться следующим конструктором:
InputStreamReader(InputStream inputStream)
Поток ввода System.in является экземпляром класса InputStream, и поэтому его можно указать в качестве параметра inputStream данного конструктора.
Затем на основании объекта типа InputStreamReader можно создать объект типа BufferedReader, используя следующий конструктор:
BufferedReader(Reader inputReader)
где inputReader — это поток, который связывается с создаваемым экземпляром класса Buf feredReader. Объединяя обращения к указанным выше конструкторам в одну операцию, мы получаем приведенную ниже строку кода. В ней создается объект типа BufferedReader, связанный с клавиатурой.
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
После выполнения этого оператора присваивания переменная br будет содержать ссылку на символьный поток, связанный с консолью через поток ввода System.in.
Прочитать символы из потока ввода System, in можно с помощью метода read(), определенного в классе Buf f eredReader. Чтение символов мало чем отличается от чтения данных из байтовых потоков. Ниже приведены общие формы объявления трех вариантов метода read(), предусмотренных в классе Buf f eredReader.
int read() throws IOException
int read(char data[]) throws IOException
int read(char data[], int start, int max) throws IOException
В первом варианте метод read() читает один символ в уникоде. По достижении конца потока этот метод возвращает значение -1. Во втором варианте метод read() читает данные из потока ввода и помещает их в массив. Чтение оканчивается по достижении конца потока, по заполнении массива data символами или при возникновении ошибки. В этом случае метод возвращает число прочитанных символов, а если достигнут конец потока, — значение -1. В третьем варианте метод read() помещает прочитанные символы в массив data, начиная с элемента, определяемого индексом start. Максимальное число символов, которые могут быть записаны в массив, определяется параметром max. В данном случае метод возвращает число прочитанных символов или значение -1, если достигнут конец потока. При возникновении ошибки в каждом из перечисленных выше вариантов метода read() генерируется исключение IOException. При чтении данных из потока ввода System, in конец потока устанавливается нажатием клавиши < Enter>.
Ниже приведен пример программы, демонстрирующий применение метода read() для чтения символов с консоли. Символы читаются до тех пор, пока пользователь не введет точку. Следует иметь в виду, что исключения, которые могут быть сгенерированы при выполнении данной программы, обрабатываются за пределами метода main(). Как пояснялось выше, подобный подход характерен для обработки ошибок при чтении данных с консоли. По желанию вы можете употребить другой механизм обработки ошибок.
// Применение класса BufferedReader для чтения символов с консоли,
import java.io.*;
class ReadChars {
public static void main(String args[])
throws IOException
{
char c;
// Создание объекта типа BufferedReader, связанного
// с потоком стандартного ввода System.in.
BufferedReader br = new
BufferedReader(new
InputStreamReader'(System. in) ) ;
System.out.println("Enter characters, period to quit.");
// читать символы
do {
с = (char) br.read();
System.out.println(c) ;
} while(c != '.');
}
}
Результат выполнения данной программы выглядит следующим образом:
Enter characters, period to quit.
One Two.
O
n
e
T
w
о
Для ввода символьной строки с клавиатуры следует воспользоваться методом readLine() из класса Buf feredReader. Ниже приведена общая форма объявления этого метода.
String readLine() throws IOException
Этот метод возвращает объект типа String, содержащий прочитанные символы. При попытке прочитать символьную строку по окончании потока метод возвращает пустое знчение null.
Ниже приведен пример программы, демонстрирующий применение класса BufferedReader и метода readLine(). В этой программе текстовые строки читаются и отображаются до тех пор, пока не будет введено слово "stop".
// Чтение символьных строк с консоли средствами класса BufferedReader.
import java.io.*;
class ReadLines {
public static void main(String args[])
throws IOException
{
// создать объект типа BufferedReader, связанный с потоком System.in
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
String str;
System.out.println("Enter lines of text.");
System.out.println("Enter 'stop' to quit.");
do {
// использовать метод readLine() из класса BufferedReader
// для чтения текстовой строки
str = br.readLine();
System.out.println(str) ;
} while(!str.equals("stop")) ;
}
}
Несмотря на то что поток стандартного вывода System, out вполне может использоваться для вывода на консоль, такой подход скорее пригоден для целей отладки или при создании очень простых программ, подобных тем, которые приводятся в качестве примеров в этой книге. Для реальных прикладных программ на Java вывод на консоль обычно организуется через поток PrintWriter, относящийся к одному из классов, представляющих символьные потоки. Как упоминалось ранее, применение символьных потоков упрощает локализацию прикладных программ.
В классе PrintWriter определен ряд конструкторов. Далее будет использоваться следующий конструктор:
PrintWriter(OutputStream OutputStream, boolean flushOnNewline)
где в качестве первого параметра OutputStream конструктору передается объект типа OutputStream, а второй параметр f lushOnNewline указывает, должен ли производиться вывод данных из буфера в поток вывода при каждом вызове метода println(). Если параметр f lushOnNewline принимает логическое значение true, данные выводятся из буфера автоматически.
В классе PrintWriter поддерживаются методы print() и println() для всех типов, включая Object. Следовательно, методы print() и println() можно использовать точно так же, как и вместе с потоком вывода System, out. Если значение аргумента не относится к простому типу, то методы из класса PrintWriter вызывают метод toString() для объекта, указываемого в качестве параметра, а затем выводят результат.
Для вывода данных на консоль через поток типа PrintWriter следует указать System.out B качестве потока вывода и обеспечить вывод данных из буфера после каждого вызова метода println(). Например, при выполнении следующей строки кода создается объект типа PrintWriter, связанный с консолью:
PrintWriter pw = new PrintWriter(System.out, true);
Ниже приведен пример прикладной программы, демонстрирующий применение класса PrintWriter для организации вывода на консоль.
// Применение класса PrintWriter.
import java.io.*;
public class PrintWriterDemo {
public static void main(String args[]) {
// Создание объекта типа PrintWriter, связанного
// с потоком стандартного вывода System.out.
PrintWriter pw = new PrintWriter(System.out, true);
int i = 10;
double d = 123.65;
pw.println("Using a PrintWriter.");
pw.println(i);
pw.println(d);
pw.println(i + " + " + d + " is " + (i+d));
}
}
Выполнение этой программы дает следующий результат:
Using a PrintWriter.
10
123.65
10 + 123.65 is 133.65
Несмотря на все удобство символьных потоков, не следует забывать, что для изучения языка Java или отладки программ можно вполне пользоваться и потоком вывода System, out. Но если в программе применяется поток PrintWriter, то ее проще локализировать. Для кратких примеров программ, представленных в этой книге, применение потока PrintWriter не имеет существенных преимуществ перед потоком System, out, поэтому в и последующих примерах для вывода на консоль будет использоваться поток System.out.
На практике чаще всего приходится обращаться с файлами, имеющими байтовую организацию, тем не менее, для этой цели можно пользоваться символьными потоками. Преимущество символьных потоков заключается в том, что они оперируют непосредственно символами в уникоде. Так, если требуется сохранить текст в уникоде, для этой цели лучше всего воспользоваться символьными потоками. Как правило, для файлового ввода-вывода символов служат классы FileReader и FileWriter.
Класс FileWriter представляет поток, через который можно осуществлять запись данных в файл. Ниже приведены общие формы объявления двух наиболее часто употребляемых конструкторов данного класса.
FileWriter(String имя_файла) throws IOException
FileWriter(String имя_файла, boolean append) throws IOException
Здесь имя файла обозначает полный путь к файлу. Если параметр append принимает логическое значение true, данные записываются в конец файла, а иначе они перезаписывают прежние данные на том же месте в файле. При возникновении ошибки в каждом из указанных выше конструкторов генерируется исключение IOException. Класс FileWriter является производным от классов OutputStreamWriter и Writer. Следовательно, в нем доступны методы, объявленные в его суперклассах.
Ниже приведен краткий пример программы, демонстрирующий ввод текстовых строк с клавиатуры и последующий их вывод в файл test. txt. Набираемый текст читается до тех пор, пока пользователь не введет слово "stop". Для вывода текстовых строк в файл используется класс FileWriter.
// Простой пример утилиты ввода с клавиатуры и вывода данных
// на диск, демонстрирующий применение класса FileWriter.
// Для компиляции этого кода требуется JDK 7
// или более поздняя версия данного комплекта.
import java.io.*;
class KtoD {
public static void main(String args[])
{
String str;
BufferedReader br =
new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Enter text ('stop' to quit).");
// Создание потока вывода типа FileWriter.
try (FileWriter fw = new FileWriter("test.txt"))
{
do {
System.out.print(": ");
str = br.readLine();
if(str.compareTo("stop") == 0) break;
str = str + "\r\n"; // add newline
// Запись текстовых строк в файл,
fw.write(str);
} while(str.compareTo("stop") != 0) ;
} catch(IOException exc) {
System.out.println("I/O Error: " + exc);
}
}
}
В классе FileReader создается объект типа Reader, который можно использовать для чтения содержимого файла. Чаще всего употребляется такой конструктор этого класса:
FileReader(String имя_файла) throws FileNotFoundException
где имя файла обозначает полный путь к файлу. Если указанный файл не существует, генерируется исключение FileNotFoundException. Класс FileReader является производным от классов InputStreamReader и Reader. Следовательно, в нем доступны методы, объявленные в его суперклассах.
Приведенный ниже пример демонстрирует простую утилиту для отображения на экране содержимого текстового файла test. txt. Она является своего рода дополнением к утилите, рассмотренной в предыдущем разделе.
// Простая утилита ввода с дйска и вывода на экран,
// демонстрирующая применение класса FileReader.
// Для компиляции этого кода требуется JDK 7
// или более поздняя версия данного комплекта.
import java.io.*;
class DtoS {
public static void main(String args[]) {
String s;
// Создание в классе BufferedReader оболочки с целью заключить
// в нее класс FileReader и организовать чтение данных из файла.
try (BufferedReader br = new BufferedReader(new FileReader("test.txt")))
{
while((s = br.readLine()) != null) {
System.out.println(s) ;
}
} catch(IOException exc) {
System.out.println("I/O Error: " + exc) ;
}
}
}
Обратите внимание на то, что для потока типа FileReader создана оболочка в классе BufferedReader. Благодаря этому появляется возможность обращаться к методу readLine(). Кроме того, закрытие потока типа Buf feredReader, на который в данном примере ссылается переменная br, автоматически приводит к закрытию файла.
Прежде чем завершить обсуждение средств ввода-вывода, необходимо рассмотреть еще один способ, помогающий читать числовые строки. Как известно, метод println() предоставляет удобные средства для вывода на консоль различных типов данных, в том числе целых чисел и чисел с плавающей точкой. Он автоматически преобразует числовые значения в удобную для чтения форму. Но в Java отсутствует метод, который читал бы числовые строки и преобразовывал бы их во внутреннюю двоичную форму. Например, не существует варианта метода read(), который читал бы числовую строку "100" и автоматически преобразовывал ее в целое число, пригодное для хранения в переменной типа int. Но для этой цели в Java имеются другие средства. И проще всего подобное преобразование осуществляется с помощью так называемых оболочек типов.
Оболочки типов в Java представляют собой классы, которые инкапсулируют простые типы. Оболочки типов необходимы, поскольку простые типы не являются объектами, что ограничивает их применение. Так, простой тип нельзя передать методу по ссылке. Для того чтобы исключить ненужные ограничения, в Java были предусмотрены классы, соответствующие каждому из простых типов.
Оболочками типов являются классы Double, Float, Long, Integer, Short, Byte, Character и Boolean. Эти классы предоставляют обширный ряд методов, позволяющих полностью интегрировать простые типы в иерархию объектов Java. Кроме того, в классах-оболочках числовых типов содержатся методы, предназначенные для преобразования числовых строк в соответствующие двоичные эквиваленты. Эти методы приведены ниже. Каждый из них возвращает двоичное значение, соответствующее числовой строке.
Оболочка типа | Метод преобразования |
---|---|
Double | static double parseDouble(String str) throws NumberFormatException |
Float | static float parseFloat(String str) throws NumberFormatException |
Long | static long parseLong(String str) throws NumberFormatException |
Integer | static int parselnt(String str) throws NumberFormatException |
Short | static short parseShort(String str) throws NumberFormatException |
Byte | static byte parseByte(String str) throws NumberFormatException |
Оболочки целочисленных типов также предоставляют дополнительный метод синтаксического анализа, позволяющий задавать основание системы счисления.
Методы синтаксического анализа позволяют без труда преобразовать во внутренний формат числовые значения, введенные в виде символьных строк с клавиатуры или из текстового файла. Ниже приведен пример программы, демонстрирующий применение для этих целей методов parselnt() и parseDouble(). В этой программе находится среднее арифметическое ряда чисел, введенных пользователем с клавиатуры. Сначала пользователю прелагается указать количество числовых значений для обработки, а затем программа вводит числа с клавиатуры, используя метод readLine(), а с помощью метода parselnt() преобразует символьную строку в целочисленное значение. Далее осуществляется ввод числовых значений и последующее их преобразование в тип double с помощью метода parseDouble().
/* Эта программа находит среднее арифметическое для
ряда чисел, введенных пользователем с клавиатуры. */
import java.io.*;
class AvgNums {
public static void main(String args[])
throws IOException
{
// создать объект типа BufferedReader,
// использующий поток ввода System.in
BufferedReader br = new
BufferedReader(new InputStreamReader(System.in));
String str;
int n;
double sum = 0.0;
double avg, t;
System.out.print("How many numbers will you enter: ");
str = br.readLine();
try {
// Преобразование символьной строки
// в числовое значение типа int.
n = Integer.parselnt(str);
}
catch(NumberFormatException exc) {
System.out.println("Invalid format");
n = 0;
}
System.out.println("Enter " + n + " values.");
for(int i=0; i < n ; i++) {
System.out.print(" : ");
str = br.readLine();
try {
// Преобразование символьной строки
// в числовое значение типа double,
t = Double.parseDouble(str) ;
} catch(NumberFormatException exc) {
System.out.println("Invalid format");
t = 0.0;
}
sum += t;
}
avg = sum / n;
System.out.println("Average is " + avg);
}
}
Выполнение этой программы может дать, например, следующий результат:
How many numbers will you enter: 5
Enter 5 values.
: 1.1
: 2.2
: 3.3
: 4.4
: 5.5
Average is 3.3
Пример для опробования 10.2. Создание справочной системы, находящейся на диске
В примере для опробования 4.1 был создан класс Help, позволяющий отображать сведения об операторах Java. Справочная информация хранилась в самом классе, а пользователь выбирал требуемые сведения из меню. И хотя такая справочная система выполняет свои функции, подход к ее разработке был выбран далеко не самый лучший. Так, если требуется добавить или изменить какие-нибудь сведения в подобной справочной системе, придется внести изменения в исходный код программы, которая ее реализует. Кроме того, выбирать пункт меню по его номеру не очень удобно, а если количество пунктов велико, то такой способ оказывается вообще непригодным. В этом проекте предстоит устранить недостатки, имеющиеся в справочной системе, разместив справочную информацию на диске.
В новом варианте справочная информация должна храниться в файле. Это будет обычный текстовый файл, который можно изменять, не затрагивая исходный код программы. Для того чтобы получить справку по конкретному вопросу, следует ввести название темы. Система будет искать соответствующий раздел в файле. Если поиск завершится успешно, справочная информация будет выведена на экран.
Последовательность действий
- Создайте файл, в котором будет храниться справочная информация и который будет использоваться в справочной системе. Это должен быть обычный текстовый файл, организованный следующим образом:
#название_темы_1
Информация по теме
#название_темы_2
Информация по теме
#название_темы_N
Информация по теме
- Название каждой темы располагается в отдельной строке и предваряется символом #. Наличие специального символа в строке (в данном случае — #) позволяет программе быстро найти начало раздела. Под названием темы может располагаться любая справочная информация. После окончания одного раздела и перед началом другого должна быть введена пустая строка. Кроме того, в конце строк не должно быть лишних пробелов.
- Ниже приведен пример простого файла со справочной информацией, который можно использовать вместе с новой версией справочной системы. В нем хранятся сведения об операторах Java.
#if
if(condition) statement;
else statement;
#switch
switch(expression) {
case constant:
statement sequence
break;
// ...
}
#for
for(init; condition; iteration) statement;
#while
while(condition) statement;
#do
do {
statement;
} while (condition);
#break
break; or break label;
#continue
continue; or continue label;
- Присвойте этому файлу имя helpfile.txt.
- Создайте файл FileHelp.java.
- Начните создание новой версии класса Help со следующих строк кода:
class Help {
String helpfile; // Имя файла со справочной информацией
Help(String fname) {
helpfile = fname;
}
- Имя файла со справочной информацией передается конструктору класса Help и запоминается в переменной экземпляра helpfile. А поскольку каждый экземпляр класса Help содержит отдельную копию переменной helpf ile, то каждый из них может взаимодействовать с отдельным файлом. Это дает возможность создавать отельные наборы справочных файлов на разные темы.
- Добавьте в класс Help метод helpon(), код которого приведен ниже. Этот метод извлекает справочную информацию по заданной теме.
// отобразить справочную информацию по заданной теме
boolean helpon(String what) {
int ch;
String topic, info;
// открыть справочный файл
try (BufferedReader helpRdr =
new BufferedReader(new FileReader(helpfile)))
{
do {
// читать символы до тех пор, пока не встретится знак #
ch = helpRdr.read();
// а теперь проверить, совпадают ли темы
if(ch == '#') {
topic = helpRdr.readLine();
if(what.compareTo(topic) == 0) { // found topic
do {
info = helpRdr.readLine();
if(info != null) System.out.println(info);
} while((info != null) &&
(info.compareTo("") != 0));
return true;
}
}
} while(ch != -1);
}
catch(IOException exc) {
System.out.println("Error accessing help file.");
return false;
}
return false; // тема не найдена
}
- Прежде всего обратите внимание на то, что в методе helpon() обрабатываются все исключения, связанные с вводом-выводом, поэтому в заголовке метода не указано ключевое слово throws. Благодаря такому подходу упрощается разработка методов, в которых используется метод helpon(). В вызывающем методе достаточно обратиться к методу helpon(), не заключая его вызов в блок try/catch.
- Для открытия файла со справочной информацией служит класс FileReader, оболочкой которого является класс Buf feredReader. В справочном файле содержится текст, и поэтому справочную систему удобнее локализовать через символьные потоки ввода-вывода.
- Метод helpon (\ действует следующим образом. Символьная строка, содержащая название темы, передается этому методу в качестве параметра. Метод открывает сначала файл со справочной информацией. Затем в файле производится поиск, т.е. проверяется совпадение содержимого переменной what и названия темы. Напомним, что в файле заголовок темы предваряется символом #, поэтому метод сначала ищет данный символ. Если символ найден, производится сравнение следующего за ним названия темы с содержимым переменной what. Если сравниваемые строки совпадают, то отображается справочная информация по данной теме. И если заголовок темы найден, то метод helpon() возвращает логическое значение true, иначе — логическое значение false.
- В классе Help содержится также метод getSelectionO, который предлагает задать тему и возвращает строку, введенную пользователем.
// получить тему
String getSelectionO {
String topic = "";
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
System.out.print("Enter topic: ") ;
try {
topic = br.readLine();
}
catch(IOException exc) {
System.out.println("Error reading console.");
}
return topic;
}
- В теле этого метода сначала создается объект типа Buf feredReader, который связывается с потоком вывода System, in. Затем в нем запрашивается название темы, которое принимается и далее возвращается вызывающей части программы.
- Ниже приведен весь исходный код программы, реализующей справочную систему, находящуюся на диске.
/*
Пример для опробования 10.2.
Справочная система, находящаяся на диске.
Для компиляции этой программы требуется JDK 7
или более поздняя версия данного комплекта.
*/
import java.io.*;
/* В классе Help открывается файл со справочной информацией,
производится поиск названия темы, а затем отображается
справочная информация по этой теме.
Обратите внимание на то, что в этом классе поддерживаются
все исключения, освобождая от этой обязанности вызывающий код. */
class Help {
String helpfile; // Имя файла со справочной информацией
Help(String fname) {
helpfile = fname;
}
// отобразить справочную информацию по заданной теме
boolean helpon(String what) {
int ch;
String topic, info;
// открыть справочный файл
try (BufferedReader helpRdr =
new BufferedReader(new FileReader(helpfile)))
{
do {
// читать символы до тех пор, пока не встретится знак #
ch = helpRdr.read();
// а теперь проверить, совпадают ли темы
if(ch =='#') {
topic = helpRdr.readLine();
if(what.compareTo(topic) == 0) { // тема найдена
do {
info = helpRdr.readLine();
if(info != null) System.out.println(info);
} while((info != null) &&
(info.compareTo("") != 0));
return true;
}
}
} while(ch != -1);
}
catch(IOException exc) {
System.out.println("Error accessing help file.");
return false;
}
return false; // тема не найдена
}
// получить тему
String getSelection() {
String topic = "";
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
System.out.print("Enter topic: ");
try {
topic = br.readLine();
}
catch(IOException exc) {
System.out.println("Error reading console.");
}
return topic;
}
}
// продемонстрировать справочную систему, находящуюся на диске
class FileHelp {
public static void main(String args[]) {
Help hlpobj = new Help("helpfile.txt");
String topic;
System.out.println("Try the help system. " +
"Enter ’stop' to end.");
do {
topic = hlpobj.getSelection();
if(!hlpobj.helpon(topic))
System.out.println("Topic not found.\n");
} while(topic.compareTo("stop") != 0);
}
}
по материалу главы 10
- Для чего в Java определены как байтовые, так и символьные потоки?
- Как известно, ввод-вывод данных на консоль осуществляется в текстовом виде. Почему же в Java для этой цели используются байтовые потоки?
- Как открыть файл для чтения байтов?
- Как открыть файл для чтения символов?
- Как открыть файл для ввода-вывода с произвольным доступом?
- Как преобразовать числовую строку "123.23" в двоичный эквивалент?
- Напишите программу, которая будет копировать текстовые файлы. Видоизмените ее таким образом, чтобы все пробелы заменялись дефисами. Используйте при написании программы классы, представляющие байтовые потоки, а также традиционный способ закрытия файла явным вызовом метода close().
- Перепишите программу, созданную в ответ на предыдущий вопрос, таким образом, чтобы в ней использовались классы, представляющие символьные потоки. На этот раз воспользуйтесь оператором try с ресурсами для автоматического закрытия файла.
- К какому типу относится поток System. in?
- Что возвращает метод read() из класса InputStream по достижении конца потока?
- Поток какого типа используется для чтения двоичных данных?
- Классы Reader и Writer находятся на вершине иерархии классов _______ .
- Оператор try без ресурсов служит для ______ .
- Если для закрытия файла используется традиционный способ, то это лучше всего делать в блоке finally. Верно или неверно?