Файловые каналы
Содержание
1 Введение
2 Чтение данных из файла
3 Пример отображения файлов
4 Копирование содержимого с использованием файловых каналов
5 Копирование с помощью метода transferTo
Введение
Каналы являются новой возможностью библиотеки Java, частью нового пакета ввода/вывода (java.nio). В документации (http://java.sun.com/j2se/1.4/docs/api/java/nio/channels/Channel.html) каналы определяются следующим образом:
Канал представляет собой открытое соединение с такими элементами, как аппаратное устройство, файл, сетевой сокет или программный компонент, и способное выполнять одну или более отдельных операций ввода/вывода, например чтение или запись.
В пакете java.nio имеется много важных функциональных возможностей, включая буферы, файловые каналы, другие типы каналов, такие как каналы сокетов, и расширяемые интерфейсы. В данной статье рассматриваются только файловые каналы. Важным преимуществом использования файловых каналов являются дополнительные возможности ввода/вывода в ваших приложениях. В этой статье представлено несколько примеров использования этих возможностей на практике.
Предположим, что у вас имеются некоторые данные в бинарных файлах. Эти данные представляют собой несколько 16-ти битных чисел, записываемых следующей программой на языке С:
/* cd1.c */ #include <stdio.h> #include <assert.h> short data[] = {1234, 2345, 3456, 4567, 5678}; #define SIZESHORT 2 int main() { int i, j; /* открыть файл данных для записи */ FILE* fp = fopen("data", "wb"); assert(fp); /* записать каждое значение short, младший байт первым */ for (i = 0; i < sizeof(data) / SIZESHORT; i++) { short item = data[i]; for (j = 0; j < SIZESHORT; j++) { char c = (char)(item & 0xff); fputc(c, fp); item >>= 8; } } fclose(fp); return 0; }
Эта программа записывает пять 16-ти битных значений в файл. Каждое значение записывается в виде двух байт, младший байт пишется первым (такой порядок следования байтов называется "little-endian" - прямым порядком). Откомпилируйте, скомпонуйте и выполните программу для создания файла данных.
Чтение данных из файла
Как можно прочитать эти данные в Java-программе? Вот один из способов:
import java.io.*;
public class ChannelDemo0 {
public static void main(String args[]) throws IOException {
FileInputStream fis = new FileInputStream("data");
DataInputStream dis = new DataInputStream(fis);
short s = dis.readShort();
System.out.println(s);
dis.close();
}
}
В этой программе используется класс DataInputStream и метод readShort этого класса. Однако если вы выполните программу ChannelDemo0, результат ее работы будет таким:
-11772
К сожалению, он не соответствует первоначальному значению (1234), записанному в файл данных. Проблема в том, что программа на языке С записывает значения типа short в порядке младший байт/старший байт, а метод readShort использует порядок - старший байт/младший байт.
Как решить эту проблему? Вот другой подход, в котором значения читаются из файла данных, и вычисляется их сумма:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo1 {
// просуммировать значения типа short из файла данных
static short sumFileContents(String fn) throws IOException {
// открыть поток ввода и создать канал
FileInputStream fis = new FileInputStream(fn);
FileChannel fc = fis.getChannel();
// отобразить файл в байтовый буфер
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc
.size());
// установить прямой порядок байтов
mbb.order(ByteOrder.LITTLE_ENDIAN);
// создать ссылку на байтовый буфер как на буфер значений short
ShortBuffer sb = mbb.asShortBuffer();
// просуммировать результаты
short sum = 0;
while (sb.hasRemaining()) {
sum += sb.get();
}
// закончить
fc.close();
fis.close();
return sum;
}
public static void main(String args[]) throws IOException {
short sum = sumFileContents("data");
System.out.println(sum);
}
}
Метод sumFileContents сначала создает FileInputStream, а затем файловый канал, основанный на этом потоке. Аналогичный подход используется для потоков вывода (FileOutputStream) или для файлов, которые открыты и по чтению и по записи (RandomAccessFile).
После создания канала метод отображает файл данных в MappedByteBuffer. Это значит, что содержимое буфера является содержимым файла, то есть, при чтении буфера байты выбираются из файла, а при записи в буфер байты записываются в файл.
Затем порядок байтов в байтовом буфере указывается как прямой (little-endian), в отличие от обратного порядка (big-endian) по умолчанию. Далее в методе создается "буфер просмотра" значений short на байтовом буфере. Буфер просмотра предоставляет доступ к отображенному байтовому буферу как к последовательности значений типа short (16-бит). Значения short состоят из двух байт, младший байт считается первым, для того чтобы соответствовать записанным программой на языке С значениям. Буфер просмотра основан на байтовом буфере. То есть, метод фактически создает представление файла данных как последовательности значений short в формате little-endian. Последнее действие метода - суммирование значений.
После выполнения программы ChannelDemo1 вы получите следующий результат:
17280
Пример отображения файлов
Давайте продолжим и рассмотрим еще один пример отображения файлов. Предположим, что вы хотите поменять порядок байтов в файле. Как это можно сделать? Один из простых способов приведен ниже.
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo2 {
public static void main(String args[]) throws IOException {
// проверить аргументы командной строки
if (args.length != 1) {
System.err.println("missing file argument");
System.exit(1);
}
// создать канал
RandomAccessFile raf = new RandomAccessFile(args[0], "rw");
FileChannel fc = raf.getChannel();
// отобразить файл в буфер
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc
.size());
// реверсировать байты в файле
int len = (int) fc.size();
for (int i = 0, j = len - 1; i < j; i++, j--) {
byte b = mbb.get(i);
mbb.put(i, mbb.get(j));
mbb.put(j, b);
}
// завершить
fc.close();
raf.close();
}
}
Программа открывает канал, основанный на объекте RandomAccessFile, и отображает файл для чтения и записи. Затем выполняется цикл, который меняет байты, начиная с обоих концов буфера и заканчивая на его середине. Поскольку MappedByteBuffer отображен на дисковый файл, изменения в буфере отражаются в файле. Если вы выполните команды:
javac ChannelDemo2.java
java ChannelDemo2 ChannelDemo2.java
и посмотрите на текст в ChannelDemo2.java, вы обнаружите, что все байты в ChannelDemo2.java расположены в обратном порядке. Другими словами, первой строкой в файле должна быть:
}
а последней:
;*.oin.avaj tropmi
Для восстановления файла ChannelDemo2.java в оригинальный вид вы должны выполнить команду:
java ChannelDemo2 ChannelDemo2.java
Копирование содержимого с использованием файловых каналов
Отображение файла может быть очень полезным, но оно не обязательно освободит вас от выполнения дополнительной работы. Например, предположим, что вы отображаете файл для получения удобного доступа к нескольким последним байтам файла. В этой ситуации вы должны выполнить определенное количество дополнительной работы, например, поиск конца файла и чтение блока файла в оперативную память. Эту работу обойти нельзя. Но отображение, безусловно, удобно и, иногда, оно может работать быстрее, чем альтернативные подходы. Например, отображение может помочь вам избежать копий буфера, или накладных расходов на системные вызовы.
Рассмотрим несколько способов копирования одного файла в другой с использованием возможностей файловых каналов. Первый подход является еще одним примером отображенных файлов:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo3 {
public static void main(String args[]) throws IOException {
// проверить аргументы командной строки
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
// создать канал
FileInputStream fis = new FileInputStream(args[0]);
FileOutputStream fos = new FileOutputStream(args[1]);
FileChannel fcin = fis.getChannel();
FileChannel fcout = fos.getChannel();
// отобразить входной файл
MappedByteBuffer mbb = fcin.map(FileChannel.MapMode.READ_ONLY, 0, fcin
.size());
// выполнить копирование файла
fcout.write(mbb);
// завершить
fcin.close();
fcout.close();
fis.close();
fos.close();
}
}
В этом примере входной файловый канал отображается в буфер, а затем этот буфер записывается в выходной канал. Поскольку буфер представляет целый файл, запись в буфер эквивалентна копированию файла. Поэтому, если вы выполните команды:
javac ChannelDemo3.java
java ChannelDemo3 ChannelDemo3.java ChannelCopy3.java
файл ChannelDemo3 скопируется в ChannelCopy3.java.
Копирование с помощью метода transferTo
Другой способ копирования файла выглядит следующим образом:
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
public class ChannelDemo4 {
public static void main(String args[]) throws IOException {
// проверить аргументы командной строки
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
// создать каналы
FileInputStream fis = new FileInputStream(args[0]);
FileOutputStream fos = new FileOutputStream(args[1]);
FileChannel fcin = fis.getChannel();
FileChannel fcout = fos.getChannel();
// выполнить копирование файла
fcin.transferTo(0, fcin.size(), fcout);
// завершить
fcin.close();
fcout.close();
fis.close();
fos.close();
}
}
Метод transferTo передает байты из канала-источника (fcin) в указанный канал-назначение (fcout). Передача обычно производится без явных чтений или записи в канал на уровне пользователя. В документации (http://java.sun.com/j2se/1.4/docs/api/java/nio/channels/FileChannel.html#transferTo(long, long, java.nio.channels.WritableByteChannel)) по поводу метода transferTo говорится:
Этот метод потенциально намного более эффективен, чем простой цикл, который читает из этого канала и записывает в канал-назначение. Многие операционные системы могут передавать байты непосредственно из кэша файловой системы в канал-назначение без фактического их копирования.
Другими словами, метод transferTo должен полагаться на специальные возможности операционной системы, поддерживающей очень быструю передачу файлов.
Как выглядит "обычное" копирование файла с использованием каналов? Вот пример:
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class ChannelDemo5 {
public static void main(String args[]) throws IOException {
// проверить аргументы командной строки
if (args.length != 2) {
System.err.println("missing filenames");
System.exit(1);
}
// создать каналы
FileInputStream fis = new FileInputStream(args[0]);
FileOutputStream fos = new FileOutputStream(args[1]);
FileChannel fcin = fis.getChannel();
FileChannel fcout = fos.getChannel();
// разместить буфер
ByteBuffer buf = ByteBuffer.allocateDirect(8192);
// выполнить копирование
long size = fcin.size();
long n = 0;
while (n < size) {
buf.clear();
if (fcin.read(buf) < 0) {
break;
}
buf.flip();
n += fcout.write(buf);
}
// завершить
fcin.close();
fcout.close();
fis.close();
fos.close();
}
}
Эта программа копирует один файл в другой, один буфер за раз. Перед чтением каждой части входного файла в буфер программа "очищает" буфер. Это подготавливает буфер к чтению, позиция устанавливается в 0, а граница - в размер буфера. Затем, после каждого чтения, программа "перебрасывает" буфер. Это подготавливает буфер для записи, граница устанавливается в текущую позицию, а текущая позиция - в 0.
Вы увидели некоторые из новых возможностей, предлагаемых файловыми каналами. Существует и другие возможности, такие как блокировка, о которых тоже важно знать. Кроме новых возможностей файловые каналы предлагают значительный выигрыш в производительности операций ввода/вывода. Дополнительная информация по файловым каналам находится в книге "New I/O API" (http://java.sun.com/j2se/1.4/docs/guide/nio/index.html).