Потоки ввода/вывода
Потоки в Java
В Java выделяется файловый ввод/вывод, вывод на печать, сетевой поток.
Три потока определены в классе system статическими полями in, out и err. Их можно использовать без всяких дополнительных определений. Они называются соответственно стандартным вводом (stdin), стандартным выводом (stdout) и стандартным выводом сообщений (stderr). Эти стандартные потоки могут быть соединены с разными конкретными устройствами ввода и вывода.
Потоки out и err — это экземпляры класса PrintStream, организующего выходной поток байтов. Эти экземпляры выводят информацию на консоль методами print (), println () и write.
Поток err предназначен для вывода системных сообщений программы: трассировки, сообщений об ошибках или, просто, о выполнении каких-то этапов программы. Такие сведения обычно заносятся в специальные файлы (log-файлы).
Поток in — это экземпляр класса InputStream. Он предназначен для ввода с консоли методами read (). Класс InputStream абстрактный, поэтому реально используется какой-то из его подклассов.
В Java имеется возможность переназначения потока, с консоли в файл.
В Java предусмотрена возможность создания потоков, направляющих символы или байты не на внешнее устройство, а в массив или из массива. Это позволяет обрабатывать данные в строках или массивах как в файлах.
Кроме того, Java можно создать канал (pipe) обмена информацией между подпроцессами.
Еще один вид потока — поток байтов, составляющих объект Java. Его можно направить в файл или передать по сети, а потом восстановить в оперативной памяти. Эта операция называется сериализацией (serialization) объектов.
Методы организации потоков собраны в классы пакета java.io.
Кроме классов, организующих поток, в пакет java.io входят классы с методами преобразования потока, например, можно преобразовать поток байтов, образующих целые числа, в поток этих чисел.
|
Еще одна возможность, предоставляемая классами пакета java.io, — слить несколько потоков в один поток.
Иерархия классов
Во главе иерархии четыре класса, построенных от класса object:
• Reader — абстрактный класс, в котором собраны самые общие методы символьного ввода;
• Writer — абстрактный класс, в котором собраны самые общие методы символьного вывода;
• InputStream — абстрактный класс с общими методами байтового ввода;
• OutputStream — абстрактный класс с общими методами байтового вывода.
Классы входных потоков Reader и InputStream определяют по три метода ввода:
• read () — возвращает один символ или байт, взятый из входного потока, в виде целого значения типа int; если поток уже закончился, возвращает -1;
• read (char [] buf) — заполняет заранее определенный массив buf символами из входного потока; в классе InputStream массив типа byte [] и заполняется он байтами; метод возвращает фактическое число взятых из потока элементов или -1, если поток уже закончился;
• read (char [] buf, int offset, int len) — заполняет часть символьного или байтового массива buf, начиная с индекса offset, число взятых из потока элементов равно len; метод возвращает фактическое число взятых из потока элементов или -1.
Эти методы выбрасывают IOException, если произошла ошибка ввода/вывода.
Четвертый метод skip (long n) пропускает в потоке начиная с текущей позиции на n символов или байт вперед. Эти элементы потока не вводятся методами read (). Метод возвращает реальное число пропущенных элементов, которое может отличаться от n, например, если поток закончился.
|
Текущий элемент потока можно пометить методом mark (int n), а затем вернуться к помеченному элементу методом reset (), но не более чем через n элементов. Не все подклассы реализуют эти методы, поэтому перед расстановкой пометок следует обратиться к логическому методу marksupported (), который возвращает true, если реализованы методы расстановки и возврата к пометкам.
Классы выходных потоков writer и O utputStream определяют по три почти одинаковых метода вывода:
• write (char [] buf) — выводит массив в выходной поток, в классе Outputstream массив имеет тип byte[];
• write (char [] buf, int offset, int len) — выводит len элементов массива buf, начиная с элемента с индексом offset;
• write (int elem) в классе Writer - выводит 16, а в классе OutputStream 8 младших битов аргумента elem в выходной поток,
В классе Writer есть еще два метода:
• Write (string s) — выводит строку s в выходной поток;
• Write (String s, int offset, int len) — выводит len символов строки s, начиная с символа с номером offset.
Многие подклассы классов Writer и OutputStream осуществляют буферизованный вывод. При этом элементы сначала накапливаются в буфере, в оперативной памяти, и выводятся в выходной поток только после того, как буфер заполнится. Это удобно для выравнивания скоростей вывода из программы и вывода потока, но часто надо вывести информацию в поток еще до заполнения буфера. Для этого предусмотрен метод flush (). Данный метод сразу же выводит все содержимое буфера в поток.
Наконец, по окончании работы с потоком его необходимо закрыть методом closed ().
Рисунок 1.1 - Иерархия символьных потоков
|
Рисунок 1.2. - Классы байтовых потоков
Потоковые классы
Все классы пакета java.io можно разделить на две группы: классы, создающие поток, и классы, управляющие потоком.
Классы, создающие потоки, в свою очередь, можно разделить на пять групп:
• классы, создающие потоки, связанные с файлами:
FileReader FileInputStream
FileWriterFile OutputStream
RandomAccessFile
• классы, создающие потоки, связанные с массивами:
CharArrayReader ByteArraylnputStream
CharArrayWriter ByteArrayOutputStream
• классы, создающие каналы обмена информацией между подпроцессами:
PipedReader PipedInputStream
PipedWriter PipedOutputStream
• классы, создающие символьные потоки, связанные со строкой:
StringReader
StringWriter
• классы, создающие байтовые потоки из объектов Java:
ObjectlnputStream
ObjectOutputStream
Слева перечислены классы символьных потоков, справа — классы байтовых потоков.
Классы, управляющие потоком, получают в своих конструкторах имеющийся поток и создают новый поток. Можно представлять их себе как переходник (преобразование данных из одного представления в другое).
Четыре класса созданы специально для преобразования потоков:
FilterReader FilterlnputStream
FilterWriter FilterOutputStream
Сами по себе эти классы бесполезны — они выполняют тождественное преобразование. Их следует расширять, переопределяя методы ввода/вывода. Но для байтовых фильтров есть полезные расширения, которым соответствуют некоторые символьные классы.
Четыре класса выполняют буферизованный ввод/вывод:
BufferedReader BufferedlnputStream
BufferedWriter BufferedOutputStream
Два класса преобразуют поток байтов, образующих восемь простых типов Java, в эти самые типы:
DataInputStream DataOutputStr eam
Два класса содержат методы, позволяющие вернуть несколько символов или байтов во входной поток:
PushbackReader PushbackInputStream
Два класса связаны с выводом на строчные устройства — экран дисплея, принтер:
PrintWriter PrintStream
Два класса связывают байтовый и символьный потоки:
• InputStreamReader — преобразует входной байтовый поток в символьный поток;
• OutputStreamWriter — преобразует выходной символьный поток в байтовый поток.
Класс streamTokenizer позволяет разобрать входной символьный поток на отдельные элементы (tokens).
Из управляющих классов выделяется класс SequenceInputStream, сливающий несколько потоков, заданных в конструкторе, в один поток, и класс LineNumberReader, который считывает выходной символьный поток построчно. Строки в потоке разделяются символами '\n' и/или '\r'.
Консольный ввод/вывод
Консоль является байтовым устройством, и символы Unicode перед выводом на консоль должны быть преобразованы в байты. Для символов Latin1 с кодами '\u0000' — '\u00FF' при этом просто откидывается нулевой старший байт и выводятся байты '0х00' —'0xFF'. Для кодов кириллицы, которые лежат в диапазоне '\u0400 1 —'\u04FF 1 кодировки Unicode, и других национальных алфавитов производится преобразование по кодовой таблице, соответствующей установленной на компьютере.
Трудности с отображением кириллицы возникают, если вывод на консоль производится в кодировке, отличной от установленной. Обычно в операционной системе Windows устанавливается кодовая страница СР1251, а вывод на консоль происходит в кодировке СР866.
В этом случае надо заменить PrintStream на PrintWriter и добавить преобразование символов Unicode в поток байт system. out, выводимых на консоль, в виде объекта класса OutputStreamWriter. В конструкторе этого объекта следует указать нужную кодировку (например: СР866).
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(System.out, "Cp866"), true);
Класс PrintStream буферизует выходной поток. Второй аргумент true его конструктора вызывает принудительный сброс содержимого буфера в выходной поток после каждого выполнения метода println ().
Методы класса PrintWriter по умолчанию не очищают буфер, а метод print () не очищает его в любом случае. Для очистки буфера используйте метод flush ().
Следует заметить, что конструктор класса PrintWriter, в котором задан байтовый поток, всегда неявно создает объект класса OutputStreamWriter с локальной кодировкой для преобразования байтового потока в символьный поток.
Ввод с консоли производится методами read () класса InputStream с помощью статического поля in класса system. С консоли идет поток байтов, полученных из scan -кодов клавиатуры. Эти байты должны быть преобразованы в символы Unicode такими же кодовыми таблицами, как и при выводе на консоль.
Для правильного ввода кириллицы удобнее всего определить экземпляр класса BufferedReader, используя объект класса InputStreamReader:
BufferedReader br = new BufferedReader(
new InputstreamReader(System.an, "Cp866"));
Класс BufferedReader переопределяет три метода read () своего суперкласса Reader. Кроме того, он содержит метод readLine ().
Метод readLine () возвращает строку типа string, содержащую символы входного потока, начиная с текущего, и заканчивая символом '\n' и/или '\r'. Эти символы-разделители не входят в возвращаемую строку. Если во входном потоке нет символов, то возвращается null.
Пример консольного ввода/вывода:
import java.io.*;
public class RrintWr {
public static void main(String[] args){
try {
BufferedReader br = new BufferedReader(
new InputStreamReader(System. in, "Cp866"));
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(System. out, "Cp866"), true);
String s = "Это строка с русским текстом";
System. out. println("System.out puts: " + s);
pw.println("PrintWriter puts: " + s);
int c = 0;
pw.println("Посимвольный ввод:");
while ((c = br.read())!= -1)
pw.println((char)c);
pw.println("Построчный ввод:");
do {
s = br.readLine();
pw.println(s);
} while (!s.equals("q"));
}
catch (Exception e){
System. out. println(e);
}
}
}
Первая строка выводится потоком system.out.Кириллица выводится неправильно. Следующая строка предварительно преобразована в поток байтов, записанных в кодировке СР866.
Затем, после текста "Посимвольный ввод:" с консоли вводятся символы "Россия" и нажимается клавиша <Enter>. Каждый вводимый символ отображается на экране — операционная система работает в режиме так называемого "эха". Фактический ввод с консоли начинается только после нажатия клавиши <Enter>, потому что клавиатурный ввод буферизуется операционной системой. Символы сразу после ввода отображаются по одному на строке. После буквы "я" выводятся неотображаемые символы '\n' и '\r', которые попали во входной поток при нажатии клавиши <Enter>.
Потом нажата комбинация клавиш <Ctrl>+<Z>. Она отображается на консоль как "^Z" и означает окончание клавиатурного ввода, завершая цикл ввода символов. Коды этих клавиш уже не попадают во входной поток.
Далее, после текста "Построчный ввод:" с клавиатуры набирается строка "Это строка" и, вслед за нажатием клавиши <Enter>, заносится в строку s. Затем строка s выводится обратно на консоль.
Для окончания работы необходимо ввести q и нажать клавишу <Enter>.
Файловый ввод/вывод
Поскольку файлы в большинстве современных операционных систем понимаются как последовательность байтов, для файлового ввода/вывода создаются байтовые потоки с помощью классов FileInputStream и FileOutputStream. Это особенно удобно для бинарных файлов, хранящих байт-коды, архивы, изображения, звук.
Но очень много файлов содержат тексты, составленные из символов. Несмотря на то, что символы могут храниться в кодировке Unicode, эти тексты чаще всего записаны в байтовых кодировках. Поэтому и для текстовых файлов можно использовать байтовые потоки. В таком случае со стороны программы придется организовать преобразование байтов в символы и обратно.
Чтобы облегчить это преобразование, в пакет java.io введены классы FileReader и FileWriter. Они организуют преобразование потока: со стороны программы потоки символьные, со стороны файла — байтовые.
Несмотря на различие потоков, использование классов файлового ввода/вывода очень похоже.
В конструкторах всех четырех файловых потоков задается имя файла в виде строки типа string или ссылка на объект класса File. Конструкторы не только создают объект, но и отыскивают файл и открывают его. Например:
FileInputStream fis = new FileInputStream("PrWr.Java");
FileReader fr = new FileReader("D:\\jdkl.3\\src\\PrWr.Java");
Если файлы отсутствуют формируется исключение FileNotFoundException (конструктор класса FileWriter выбрасывает более общее исключение IOException).
После открытия выходного потока типа FileWriter или FileQutputStream содержимое файла, если он был не пуст, стирается. Для того чтобы можно было делать запись в конец файла, и в том и в другом классе предусмотрен конструктор с двумя аргументами. Если второй аргумент равен true, то происходит дозапись в конец файла, если false, то файл заполняется новой информацией. Например:
FileWriter fw = new FileWriter("ch_8.txt", true);
FileOutputStream fos = new FileOutputStream("D:\\samples\\newfile.txt");
Сразу после выполнения конструктора можно читать файл:
fis.read(); fr.read();
или записывать в него:
fos.write((char)с); fw.write((char)с);
По окончании работы с файлом поток следует закрыть методом close ().
Преобразование потоков в классах FileReader и FileWriter выполняется по кодовой таблице установленной на компьютере. Для правильного ввода кирилицы надо применять FileReader, a нe FileInputStream. Если файл содержит текст в кодировке, отличной от локальной кодировки, то можно использовать промежуточный класс. Например:
InputStreamReader isr = new InputStreamReader(fis, "KOI8_R"));
Байтовый поток fis определен выше.
Получение свойств файла
В конструкторах классов файлового ввода/вывода указывалось имя файла в виде строки. При этом оставалось неизвестным, существует ли файл, разрешен ли к, нему доступ, какова длина файла.
Получить такие сведения можно от предварительно созданного экземпляра класса File, содержащего сведения о файле. В конструкторе этого класса File(String filename) указывается путь к файлу или каталогу, записанный по правилам операционной системы. В UNIX имена каталогов разделяются наклонной чертой /, в MS Windows — обратной наклонной чертой \, в Apple Macintosh — двоеточием:. Этот символ содержится в системном свойстве file.separator. Путь к файлу предваряется префиксом. В UNIX это наклонная черта, в MS Windows — буква раздела диска, двоеточие и обратная наклонная черта. Если префикса нет, то путь считается относительным и к нему прибавляется путь к текущему каталогу, который хранится в системном свойстве user.dir.
Конструктор не проверяет, существует ли файл с таким именем, поэтому после создания объекта следует это проверить логическим методом exists ().
Класс File содержит около сорока методов, позволяющих узнать различные свойства файла или каталога.
Прежде всего, логическими методами isFile(), isDirectory() можно выяснить, является ли путь, указанный в конструкторе, путем к файлу или каталогу.
Для каталога можно получить его содержимое — список имен файлов и подкаталогов— методом list(), возвращающим массив строк string[]. Можно получить такой же список в виде массива объектов класса File[] методом listFiles(). Можно выбрать из списка только некоторые файлы, реализовав интерфейс FileNameFilter и обратившись к методу list(FileNameFilter filter).
Если каталог с указанным в конструкторе путем не существует, его можно создать логическим методом mkdir(). Этот метод возвращает true, если каталог удалось создать. Логический метод mkdirs() создает еще и все несуществующие каталоги, указанные в пути.
Пустой каталог удаляется методом delete ().
Для файла можно получить его длину в байтах методом length (), время последней модификации в секундах с 1 января 1970 г. методом lastModified(). Если файл не существует, эти методы возвращают нуль.
Логические методы canRead (), canWrite () показывают права доступа к файлу.
Файл можно переименовать логическим методом renameTo(File newMame) или удалить логическим методом delete(). Эти методы возвращают true, если операция прошла удачно.
Если файл с указанным в конструкторе путем не существует, его можно создать логическим методом createNewFile(), возвращающим true, если файл не существовал, и его удалось создать, и false, если файл уже существовал.
Статическими методами
createTempFile(String prefix, String suffix, File tmpDir)
createTempFile(String prefix, String suffix)
можно создать временный файл с именем prefix и расширением suffix в каталоге tmpDir или каталоге, указанном в системном свойстве java.io.tmpdir. Имя prefix должно содержать не менее трех символов. Если suffix = null, то файл получит суффикс.tmp.
Перечисленные методы возвращают ссылку типа File на созданный файл. Если обратиться к методу deieteOnExit (), то по завершении работы JVM временный файл будет уничтожен.
Несколько методов getxxxo возвращают имя файла, имя каталога и другие сведения о пути к файлу. Эти методы полезны в тех случаях, когда ссылка на объект класса File возвращается другими методами и нужны сведения о файле. Наконец, метод toURL () возвращает путь к файлу в форме URL.
import java.io.*;
class FileTest{
public static void main(String[] args) throws IOException{
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(System.out, "Cp866"), true);
File f = new File("FileTest.Java");
pw.println();
pw.println("Файл \"" + f.getName() + "\"" +(f.exists()?"":"не ") +
"существует");
pw.println("Вы " + (f.canRead()?"":"не ") + "можете читать файл");
pw.println("Вы " + (f.canWrite()?"":"нe ") +"можете записывать в файл");
pw.println("Длина файла " + f.length() + " b");
pw.println();
File d = new File(" D:\\jdkl.3\\MyProgs ");
pw.println("Содержимое каталога:");
if (d.exists() && d.isDirectory()) {
String[] s = d.list();
for (int i = 0; i < s.length; i++)
pw.println(s[i]);
}
}
}
Буферизованный ввод/вывод
Операции ввода/вывода по сравнению с операциями в оперативной памяти выполняются очень медленно. Для компенсации в оперативной памяти выделяется некоторая промежуточная область — буфер, в которой постепенно накапливается информация. Когда буфер заполнен, его содержимое быстро переносится процессором, буфер очищается и снова заполняется информацией.
Классы файлового ввода/вывода не обеспечивают буферизацию. Для этой цели есть четыре специальных класса BufferedXxx, перечисленных выше. Они присоединяются к потокам ввода/вывода как промежуточное звено, например:
BufferedReader br = new BufferedReader(isr);
BufferedWriter bw = new BufferedWriter(fw);
Потоки isr и fw определены выше.
Пример программы, которая считывает файл, в кодировке СР866, и записывает его содержимое в файл в кодировке KOI8_R. При чтении и записи применяется буферизация. Имя исходного файла задается в командной строке параметром args[0], имя копии — параметром args[l].
class DOStoUNIX{
public static void main(String[] args) throws IOException{
if (args.length!= 2){
System. err. println("Usage: DOStoUNIX Cp866file KOI8_Rfile");
System. exit (0);
}
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream(args[0]), "Cp866"));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(args[1]), "KOI8_R"));
int c = 0;
while ((c = br.read())!= -1)
bw.write((char)c);
br.close(); bw.close();
System. out. println("The job's finished.");
}
}