Работа с потоками ввода-вывода и файловой системой
1 Теоретическая часть
1.1 Файловые потоки ввода-вывода
1.1.1 Общие концепции
Классы, осуществляющие файловый ввод и вывод, а также другие действия с потоками, расположенные в пакете java.io
. Классы этого пакета предлагают ряд методов для создания таких потоков, чтения, записи и т.д. Существует два подмножества классов – соответственно для работы с текстовыми и бинарными (двоичными) файлами.
Непосредственную работу с текстовыми файлами осуществляют объекты классов FileReader
и FileWriter
. Потоки, предназначенные для работы с текстовой информацией, называются потокам символов.
Важный элемент работы с файловыми потоками – это буферизация. Буферизация предусматривает создание в оперативной памяти специальной области (буфера), в которую данные загружаются из файла для дальнейшего поэлементного чтения либо поэлементно записываются данные с последующим переписыванием на диск. Объекты класса BufferedReader
осуществляют такое буферизированное чтение. Для буферизированного вывода применяют объекты класса BufferedWriter
.
Непосредственный форматированный вывод осуществляется методами print()
и println()
объекта класса PrintWriter
. Вся работа с потоками, кроме стандартного потока System.out
, должна предусматривать перехват исключений, связанных с вводом-выводом. Это IOException
и его потомки – FileNotFoundException
, ObjectStreamException
и другие.
Очень важно закрыть все файлы, взаимодействие с которыми имело место. При закрытии файлов осуществляется переписывание данных, оставшихся в буфере, освобождение буфера и других ресурсов, связанных с файлом. Закрыть поток можно с помощью метода close()
. Например, для потока in
:
in.close();
Если программа, которая требует файлового ввода, загружается в среде IntelliJ IDEA, необходимые для чтения файлы следует разместить в папке проекта (не в папке пакета). Именно в папке проекта можно найти результирующие файлы, которые появляются после завершения выполнения программы, включающей файловый вывод.
В программе можно одновременно открыть несколько потоков ввода и несколько потоков вывода.
1.1.2 Работа с потоками символов
В следующем примере из файла с именем data.txt осуществляется чтение одного целого и одного вещественного значения, их сумма записывается в файл results.txt.
package ua.inf.iwanoff.files; import java.io.*; import java.util.StringTokenizer; public class FileTest { void readWrite() { try { FileReader fr = new FileReader("data.txt"); BufferedReader br = new BufferedReader(fr); String s = br.readLine(); int x; double y; try { StringTokenizer st = new StringTokenizer(s); x = Integer.parseInt(st.nextToken()); y = Double.parseDouble(st.nextToken()); } finally { br.close(); } double z = x + y; FileWriter fw = new FileWriter("results.txt"); PrintWriter pw = new PrintWriter(fw); pw.println(z); pw.close(); } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
Для открытия файла создается объект класса FileReader
, в конструкторе которого указывается строка – имя файла. Ссылка на созданный объект передается в конструктор класса BufferedReader
. Чтение из файла осуществляется с помощью метода readLine()
, который возвращает ссылку на строку символов, или null
, если достигнут конец файла.
Переменная s
типа String
ссылается на строку, содержащую два числа. Для выделения из этой строки отдельных лексем используют объект класса StringTokenizer
, в конструктор которого передается строка. Ссылки на отдельные части строки постепенно получают с помощью метода nextToken()
. Эти ссылки могут быть использованы непосредственно, либо используются для преобразования данных в числовые значения (статические методы parseDouble()
и parseInt()
классов Double
и Integer
соответственно).
Для чтения из файла можно использовать уже знакомый класс Scanner
. Фактическим параметром конструктора может быть файловый поток. Предыдущий пример можно реализовать с помощью класса Scanner
. Можно также сократить код путем исключения ненужных переменных. Кроме того, целесообразно воспользоваться конструкцией try () { }
Java 7, которая для классов, реализующих интерфейс AutoCloseable
, обеспечивает гарантированное закрытие потоков:
package ua.inf.iwanoff.files; import java.io.*; import java.util.Scanner; public class FileTest { void readWrite() { try (Scanner scanner = new Scanner(new FileReader("data.txt"))) { try (PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { new FileTest().readWrite(); } }
Преимуществом такого подхода является возможность произвольного расположения входных данных (не обязательно в одной строке). Как видно из приведенного примера, несколько блоков try ()
могут использовать один блок catch ()
. Альтернативой является размещение нескольких утверждений внутри скобок:
try (Scanner scanner = new Scanner(new FileReader("data.txt")); PrintWriter pw = new PrintWriter("results.txt")) { pw.println(scanner.nextInt() + scanner.nextDouble()); } catch (IOException ex) { ex.printStackTrace(); }
При работе с классом Scanner
можно определить дополнительные параметры, например, установить символ-разделитель (или последовательность символов). При этом можно использовать регулярные выражения. Например, можно перед чтением данных добавить следующую строку:
scanner.useDelimiter(",");
Теперь объект-сканер будет воспринимать запятые как разделители (вместо пробелов).
1.1.3 Работа с бинарными потоками (потоками байтов)
Для работы с нетекстовыми (бинарными) файлами используют потоки, имена которых вместо "Writer
" содержат "Stream
", например InputStream
, FileInputStream
, OutputStream
, FileOutputStream
т.п. Такие потоки называются потоками байтов. В следующем примере осуществляется копирования двоичного файла FileCopy.class в папку проекта с новым именем:
package ua.inf.iwanoff.files; import java.io.*; public class FileCopy { public static void copy(String inFile, String outFile) { byte[] buffer = new byte[1024]; // Буфер байтов try (InputStream input = new FileInputStream(inFile); OutputStream output = new FileOutputStream(outFile)) { int bytesRead; while ((bytesRead = input.read(buffer)) >= 0) { output.write(buffer, 0, bytesRead); } } catch (IOException ex) { ex.printStackTrace(); } } public static void main(String[] args) { copy("out/production/FileCopy/ua/inf/iwanoff/files/FileCopy.class", "FileCopy.copy"); } }
Как видно из приведенного примера, Java позволяет использовать обычную черту (/
) вместо обратной. Это – более универсальный подход, приемлемый для различных операционных систем. Кроме того, обратную черту необходимо было бы записать дважды (\\
).
Для работы с бинарными файлами существуют дополнительные возможности – использование потоков данных и потоков объектов. Так называемые потоки данных (data streams) поддерживают бинарный ввод/вывод значений примитивных типов данных (boolean
, char
, byte
, short
, int
, long
, float
и double
), а также значений типа String
. Все потоки данных реализуют интерфейсы – DataInput
или DataOutput
. Для большинства задач достаточно стандартных реализации этих интерфейсов – DataInputStream
и DataOutputStream
. Данные в файле хранятся в таком виде, в котором они представлены в оперативной памяти. Для записи строк используют метод writeUTF()
. В следующем примере осуществляется запись данных:
package ua.inf.iwanoff.files; import java.io.*; public class DataStreamDemo { public static void main(String[] args) { double x = 4.5; String s = "all"; int[] a = { 1, 2, 3 }; try (DataOutputStream out = new DataOutputStream(new FileOutputStream("data.dat"))) { out.writeDouble(x); out.writeUTF(s); for (int k : a) { out.writeInt(k); } } catch (IOException e) { e.printStackTrace(); } } }
Теперь данные можно прочитать в другой программе:
package ua.inf.iwanoff.files; import java.io.*; import java.util.*; public class DataReadDemo { public static void main(String[] args) { try (DataInputStream in = new DataInputStream(new FileInputStream("data.dat"))) { double x = in.readDouble(); String s = in.readUTF(); List<Integer> list = new ArrayList<>(); try { while (true) { int k = in.readInt(); list.add(k); } } catch (Exception e) { } System.out.println(x); System.out.println(s); System.out.println(list); } catch (Exception e) { e.printStackTrace(); } } }
Примечание. В приведенной выше программе выход из цикла осуществляется при возбуждении исключения. Такой подход не является рекомендуемым, поскольку механизм исключений снижает эффективность работы программы. В нашем случае целесообразно было бы отдельно сохранять длину массива перед его элементами, а затем использовать эту длину для организации цикла for
при чтении.
Для чтения и записи данных может быть также использован класс java.io.RandomAccessFile
. Объект этого класса позволяет свободно перемещаться по файлу в прямом и обратном направлении. Основным преимуществом класса RandomAccessFile
является возможность читать и записывать данные в произвольное место файла.
Для того чтобы создать объект класса RandomAccessFile
, необходимо вызвать его конструктор с двумя параметрами: именем файла для ввода/вывода и режимом доступа к файлу. В качестве режима можно использовать строки "r"
(для чтения), "rw"
(для чтения и записи), "rws"
(с синхронизацией файла) или "rwd"
(с синхронизацией файла и метаданных). Так может выглядеть открытие файла данных:
RandomAccessFile file1 = new RandomAccessFile("file1.dat", "r"); // для чтения RandomAccessFile file2 = new RandomAccessFile("file2.dat", "rw"); // для чтения и записи
После того как файл открыт, можно использовать методы readDouble()
, readInt()
, readUTF()
и т. д. для чтения или writeDouble()
, writeInt()
, writeUTF()
и т. д. для вывода.
В основе управления файлом лежит текущий указатель на текущую позицию, где происходит чтение или запись данных. В момент создания объекта класса RandomAccessFile
указатель устанавливается в начало файла и имеет значение 0. Вызовы методов read...()
и write...()
смещают позицию текущего указателя на количество прочитанных или записанных байтов. Для произвольного сдвига указателя на некоторое количество байтов можно использовать метод skipBytes()
, или же установить указатель в определенное место файла вызовом метода seek()
. Для того чтобы узнать текущую позицию, в которой находится указатель, нужно вызвать метод getFilePointer()
. Например, в одной программе мы записываем данные в новый файл:
RandomAccessFile fileOut = new RandomAccessFile("new.dat", "rw"); int a = 1, b = 2; fileOut.writeInt(a); fileOut.writeInt(b); fileOut.close();
В другой программе мы читаем второе целое число:
RandomAccessFile fileIn = new RandomAccessFile("new.dat", "rw"); fileIn.skipBytes(4); // перемещаем файловый указатель ко второму числу int c = fileIn.readInt(); System.out.println(c); fileIn.close();
Узнать длину файла в байтах можно с помощью функции length()
.
1.2 Двоичная сериализация
Для записи и чтения объектов используют потоки ObjectInputStream
и ObjectOutputStream
. Наиболее естественным является использование этих потоков для сериализации и десериализации. Механизм сериализации (serialization, размещение в последовательном порядке) предусматривает запись объектов в поток битов для хранения в файле или для передачи через компьютерные сети. Десериализация предполагает чтение потока битов, создание хранимых объектов и воспроизведение их состояния на момент сохранения. Для того, чтобы объекты определенного класса можно было сериализовать, класс должен реализовывать интерфейс java.io.Serializable
. Этот интерфейс не определяет методов, его наличие лишь указывает, что объекты этого класса можно сериализовать. Однако гарантированная сериализация и десериализация требует наличия в таких классах специального статического поля serialVersionUID
, которое обеспечивает уникальность класса.
В среде IntelliJ IDEA статическое поле serialVersionUID
, можно сгенерировать автоматически, предварительно включив в установках File | Settings | Editor | Inspections | Java | Serialization issues опцию Serializable class without 'serialVersionUID'. После этого, находясь в редакторе, добавив реализацию интерфейса и выбрав соответствующий класс, можно воспользоваться контекстной подсказкой Add 'serialVersionUID' field.
Классы ObjectOutputStream
и ObjectInputStream
позволяют осуществлять сериализацию и десериализацию. Они реализуют интерфейсы ObjectOutput
и ObjectInput
соответственно. Механизмы сериализации и десериализации рассмотрим на следующем примере. Предположим, описан класс Point
:
package ua.inf.iwanoff.files; import java.io.Serializable; public class Point implements Serializable { private static final long serialVersionUID = -3861862668546826739L; private double x, y; public void setX(double x) { this.x = x; } public void setY(double y) { this.y = y; } public double getX() { return x; } public double getY() { return y; } }
Также создан класс Line
:
package ua.inf.iwanoff.files; import java.io.Serializable; public class Line implements Serializable { private static final long serialVersionUID = -4909779210010719389L; private Point first = new Point(), second = new Point(); public void setFirst(Point first) { this.first = first; } public Point getFirst() { return first; } public Point getSecond() { return second; } public void setSecond(Point second) { this.second = second; } }
В приведенной ниже программе (в том же пакете) осуществляется создание объектов с последующей сериализацией:
package ua.inf.iwanoff.files; import java.io.*; public class SerializationTest { public static void main(String[] args) { Line line = new Line(); line.getFirst().setX(1); line.getFirst().setY(2); line.getSecond().setX(3); line.getSecond().setY(4); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("temp.dat"))) { out.writeObject(line); } catch (IOException e) { e.printStackTrace(); } } }
В другой программе можно осуществить десериализацию:
package ua.inf.iwanoff.files; import java.io.*; public class DeserializationTest { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("temp.dat"))) { Line line = (Line) in.readObject(); System.out.println(line.getFirst().getX() + " " + line.getFirst().getY() + " " + line.getSecond().getX() + " " + line.getSecond().getY()); } catch (IOException e) { e.printStackTrace(); } } }
Можно сериализовать объекты, содержащие массивы других объектов:
package ua.inf.iwanoff.files; import java.io.*; class Pair implements Serializable { private static final long serialVersionUID = 6802552080830378203L; double x, y; public Pair(double x, double y) { this.x = x; this.y = y; } } class ArrayOfPairs implements Serializable { private static final long serialVersionUID = 5308689750632711432L; Pair[] pairs; public ArrayOfPairs(Pair[] pairs) { this.pairs = pairs; } } public class ArraySerialization { public static void main(String[] args) { Pair[] points = { new Pair(1, 2), new Pair(3, 4), new Pair(5, 6) }; ArrayOfPairs arrayOfPoints = new ArrayOfPairs(points); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("temp.dat"))) { out.writeObject(arrayOfPoints); } catch (IOException e) { e.printStackTrace(); } } }
Теперь можно осуществить десериализацию:
package ua.inf.iwanoff.files; import java.io.*; public class ArrayDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("temp.dat"))) { ArrayOfPairs arrayOfPairs = (ArrayOfPairs) in.readObject(); for (Pair p : arrayOfPairs.pairs) { System.out.println(p.x + " " + p.y); } } catch (Exception e) { e.printStackTrace(); } } }
Некоторые поля класса, значения которых не влияют на состояние объекта, можно описать с модификатором transient
. например:
class SomeClass implements Serializable { transient int someUnnecessaryField; }
Такие поля не будут сохраняться в потоке при сериализации и не будут воспроизведены при десериализации.
Сериализовать можно также объекты обобщенных классов. При десериализации необходимо дополнительно обрабатывать исключение ClassNotFoundException
. Кроме того, необходимо подавлять предупреждение "unchecked
".
1.3 Работа с архивами
Пакет java.util.zip
предоставляет возможности работы со стандартными файлами ZIP и GZIP форматов.
Для записи в архив применяют класс ZipOutputStream
. С помощью функции setMethod()
этого класса можно определить метод архивации – ZipOutputStream.DEFLATED
(с компрессией) или ZipOutputStream.STORED
(без компрессии). Метод setLevel()
определяет уровень компрессии (вд 0 до 9, по умолчанию Deflater.DEFAULT_COMPRESSION
, обычно максимальная компрессия). Метод setComment()
позволяет добавить комментарий к архиву.
Для каждого файла, который нужно поместить в zip-файл, создается объект ZipEntry
. Предполагаемое имя файла передается конструктору ZipEntry
. В нем можно отдельно установить аналогичные параметры. Далее с помощью метода putNextEntry()
класса ZipOutputStream
"раскрывается" соответствующая точка входа в архив. С помощью средств работы с файловыми потоками осуществляется запись данных в архив, затем следует закрыть объект ZipEntry
посредством вызова closeEntry()
.
В следующем примере создается архив Source.zip, к которому прилагается содержимое исходного файла ZipCreator.java:
package ua.inf.iwanoff.files; import java.io.*; import java.util.zip.*; public class ZipCreator { public static void main(String[] args) { try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Source.zip"))) { ZipEntry zipEntry = new ZipEntry("src/ua/inf/iwanoff/files/ZipCreator.java"); zOut.putNextEntry(zipEntry); try (FileInputStream in = new FileInputStream("src/ua/inf/iwanoff/files/ZipCreator.java")) { byte[] bytes = new byte[1024]; int length; while ((length = in.read(bytes)) >= 0) { zOut.write(bytes, 0, length); } } zOut.closeEntry(); } catch (IOException e) { e.printStackTrace(); } } }
Вновь созданный архив содержит относительный путь к файлу. Если это не нужно, при создании объекта ZipEntry
следует указать только имя без пути:
ZipEntry zipEntry = new ZipEntry("ZipCreator.java");
Для того, чтобы прочитать данные из архива, необходимо воспользоваться классом ZipInputStream
. В каждом таком архиве всегда нужно просматривать отдельные записи (entries). Метод getNextEntry()
возвращает следующую ссылку на объект типа ZipEntry
. Метод read()
класса ZipInputStream
возвращает -1 вконце текущей записи (а не только в конце Zip-файла). Далее вызывается метод closeEntry()
для получения возможности перехода к считыванию следующей записи. В следующем примере осуществляется чтение записи ZipCreator.java из ранее созданного архива и вывод его содержимого в консольное окно:
package ua.inf.iwanoff.files; import java.io.*; import java.util.zip.*; public class ZipExtractor { public static void main(String[] args) { try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Source.zip"))) { ZipEntry entry; byte[] buffer = new byte[1024]; while ((entry = zIn.getNextEntry()) != null) { int bytesRead; System.out.println("------------" + entry.getName() + "------------"); while ((bytesRead = zIn.read(buffer)) >= 0) { System.out.write(buffer, 0, bytesRead); } zIn.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
Аналогично осуществляется работа с архивами формата GZIP. Соответствующие потоки чтения и записи – GZIPInputStream
і GZIPOutputStream
.
1.4 Работа с файловой системой
1.4.1 Общие концепции
Java предоставляет возможность работы не только с содержанием файлов, а также с файловой системой в целом. Файловая система – это способ организации данных, используемый операционной системой для хранения информации в виде файлов на носителях информации. Также этим понятием обозначают совокупность файлов и каталогов (папок), которые размещаются на логическом или физическом устройстве.
К типичным функциям взаимодействия с файловой системой относятся:
- проверка существования файла или каталога
- получение списка файлов и подкаталогов заданного каталога
- создание файлов и ссылок на файлы
- копирование файлов
- переименование и перемещение файлов
- управление атрибутами файлов
- удаление файлов
- обход дерева подкаталогов
- отслеживание изменений файлов
Для работы с файловой системой Java предоставляет два подхода:
- использование класса
java.io.File
; - использование средств пакета
java.nio.file
.
1.4.2 Использование класса File
В версиях Java до 6 включительно для работы с файловой системой предоставлены средства, реализованные классом java.io.File
. Для создания объекта этого класса в качестве параметра конструктора следует определить полный или относительный путь к файлу. Например:
File dir = new File("C:\\Users"); File currentDir = new File("."); // Папка проекта (текущая)
Класс File
содержит методы для получения списка файлов определенной папки (list()
, listFiles()
), получения и модификации атрибутов файлов (setLastModified()
, setReadOnly()
, isHidden()
, isDirectory()
и т.д.), создания нового файла (createNewFile()
, createTempFile()
), создания папок (mkdir()
), удаления файлов и папок (delete()
) и многие другие. Работу некоторых из этих методов можно продемонстрировать на следующем примере:
package ua.inf.iwanoff.files; import java.io.*; import java.util.*; public class FileTest { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Введите имя папки, которую вы хотите создать:"); String dirName = scanner.next(); File dir = new File(dirName); // Создаем новую папку: if (!dir.mkdir()) { System.out.println("Нельзя создать папку!"); return; } // Создаем новый файл внутри новой папки: File file = new File(dir + "\\temp.txt"); file.createNewFile(); // Показываем список файлов папки: System.out.println(Arrays.asList(dir.list())); file.delete(); // Удаляем файл dir.delete(); // Удаляем папку } }
Функция list()
без параметров позволяет получить массив строк – всех файлов и подкаталогов папки, определенной при создании объекта типа File
. Выводятся относительные имена файлов (без пути). В следующем примере мы получаем список файлов и подкаталогов папки, имя которой вводится с клавиатуры:
package ua.inf.iwanoff.files; import java.io.File; import java.io.FilenameFilter; import java.util.Scanner; public class ListOfFiles { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Введите имя папки:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Неправильное имя папки!"); return; } String[] list = dir.list(); for(String name : list) { System.out.println(name); } } }
В отличие от list()
, функция listFiles()
возвращает массив объектов типа File
. Это дает дополнительные возможности – получение имен файлов с полным путем, проверка значений атрибутов файлов, отдельная работа с папками и т.д. Эти дополнительные возможности покажем на следующем примере:
File[] list = dir.listFiles(); // Выводятся данные о файлах в форме по умолчанию: for(File file : list) { System.out.println(file); } // Выводится полный путь: for(File file : list) { System.out.println(file.getCanonicalPath()); } // Выводятся только подкаталоги: for(File file : list) { if (file.isDirectory()) { System.out.println(file.getCanonicalPath()); } }
Функция getCanonicalPath()
, используемая в данном примере, позволяет получить полный путь к файлу. В отличие от getAbsolutePath()
, исключаются элементы относительного пути типа "\..\.\
".
Для определения маски-фильтра необходимо создать объект класса, который реализует интерфейс FilenameFilter
. В следующем примере мы получаем список файлов и подкаталогов, имена которых начинаются с буквы s
:
String[] list = dir.list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return name.toLowerCase().charAt(0) == 's'; } }); for(String name : list) { System.out.println(name); }
Аналогичный параметр типа FilenameFilter
применим к функции listFiles()
.
1.4.3 Работа с пакетом java.nio
Пакет java.nio
, появившийся в JDK 1.4, первоначально включал альтернативные средства ввода-вывода. По сравнению с традиционными потоками ввода-вывода, java.nio
обеспечивает более высокую эффективность операций ввода-вывода. Это достигается за счет того, что традиционные средства ввода-вывода работают с данными в потоках, в то время как java.nio
работает с данными в блоках. Центральными объектами в java.nio
являются "Канал" (Channel
) и "Буфер" (Buffer
). Каналы аналогичны потокам в пакете java.io
. Буфер – это контейнерный объект. Все данные, которые передаются в канал, должны быть сначала помещены в буфер. Любые данные, которые считываются из канала, считываются в буфер. Средства java.nio
эффективны при работе с двоичными файлами.
Версия Java 7 предоставляет альтернативный подход к работе с файловой системой – набор классов, описанных в пакете java.nio.files
. Пакет java.nio.files
предоставляет класс Path
, обеспечивающий представление пути в файловой системе. Отдельные составляющие этого пути можно представить некоторой коллекцией имен промежуточных подкаталогов и имени самого файла (подкаталога). Получить объект класса Path
можно с помощью метода get()
класса Paths
. Методу get()
передается строка – путь:
Path path = Paths.get("c:/Users/Public");
Теперь можно получить информацию про путь:
System.out.println(path.toString()); // c:\Users\Public System.out.println(path.getFileName()); // Public System.out.println(path.getName(0)); // Users System.out.println(path.getNameCount()); // 2 System.out.println(path.subpath(0, 2)); // Users\Public System.out.println(path.getParent()); // c:\Users System.out.println(path.getRoot()); // c:\
После того, как объект класса Path
создан, можно использовать в качестве аргумента статических функций класса java.nio.files.Files
. Для проверки наличия (отсутствия) файла используют соответственно функции exists()
и notExists()
:
Path dir = Paths.get("c:/Windows"); System.out.println(Files.exists(dir)); // скорее всего, true System.out.println(Files.notExists(dir)); // скорее всего, false
Наличие двух отдельных функций связано с возможностью получения неопределенного результата (запрещен доступ к файлу).
Чтобы убедиться, что программа может получить необходимый доступ к файлу, можно использовать методы isReadable(Path)
, isWritable(Path)
и isExecutable(Path)
. Допустим, создан объект file типа Path
и задан путь к файлу. Следующий фрагмент кода проверяет, существует ли конкретный файл, и можно ли загрузить его на выполнение:
boolean isRegularExecutableFile = Files.isRegularFile(file) & Files.isReadable(file) & Files.isExecutable(file);
Для получения метаданных (данных о файлах и каталогах) класс Files
предоставляет ряд статических методов:
Методы | Объяснение |
---|---|
size(Path) |
Возвращает размер указанного файла в байтах |
isDirectory(Path, LinkOption...) |
Возвращает true , если указанный Path определяет файл, который является каталогом |
isRegularFile(Path, LinkOption...) |
Возвращает true , если указанный
Path указывает на обычный файл |
isHidden(Path) |
Возвращает true , если указанный
Path указывает на скрытый файл |
getLastModifiedTime(Path, LinkOption...) setLastModifiedTime(Path, FileTime) |
Возвращает или устанавливает время последнего изменения указанного файла |
getOwner(Path, LinkOption...) setOwner(Path, UserPrincipal) |
Возвращает или устанавливает владельца файла |
getAttribute(Path, String, LinkOption...) setAttribute(Path, String, Object, LinkOption...) |
Возвращает или устанавливает значение атрибута файла |
Для ОС Windows различных версий строка атрибута должна начинаться с префикса "dos:
". Например, так можно установить необходимые атрибуты некоторому файлу:
Path file = ... Files.setAttribute(file, "dos:archive", false); Files.setAttribute(file, "dos:hidden", true); Files.setAttribute(file, "dos:readonly", true); Files.setAttribute(file, "dos:system", true);
Чтение необходимых атрибутов может также осуществляться методом readAttributes()
. Его второй параметр – метаданные о возвращаемом типе, которые могут быть получены через значение поля class
(метаданные типов будут рассмотрены позже). Наиболее подходящий тип результата – класс java.nio.file.attribute.BasicFileAttributes
. Например, так можно получить некоторые данные о файле:
package ua.inf.iwanoff.files; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class Attributes { public static void main(String[] args) throws Exception { System.out.println("Введите имя файла или каталога:"); Path path = Paths.get(new Scanner(System.in).nextLine()); BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); System.out.println("Время создания: " + attr.creationTime()); System.out.println("Время последнего доступа: " + attr.lastAccessTime()); System.out.println("Время последнего изменения: " + attr.lastModifiedTime()); System.out.println("Каталог: " + attr.isDirectory()); System.out.println("Обычный файл: " + attr.isRegularFile()); System.out.println("Размер: " + attr.size()); } }
Класс DosFileAttributes
, производный от BasicFileAttributes
, предоставляет также функции isReadOnly()
, isHidden()
, isArchive()
и isSystem()
.
В отличие от ранее созданных средств для работы с файловой системой, класс java.nio.files.Files
предоставляет функцию copy()
для копирования файлов. Например:
Files.copy(Paths.get("c:/autoexec.bat"), Paths.get("c:/Users/autoexec.bat")); Files.copy(Paths.get("c:/autoexec.bat"), Paths.get("c:/Users/autoexec.bat"), StandardCopyOption.REPLACE_EXISTING);
Существуют также опции StandardCopyOption.ATOMIC_MOVE
и StandardCopyOption.COPY_ATTRIBUTES
. Опции можно перечислять через запятую.
Для перемещения файлов используют функцию move()
(с аналогичными атрибутами или без них). Переименование выполняется той же функцией:
Files.move(Paths.get("c:/Users/autoexec.bat"), Paths.get("d:/autoexec.bat"));// перемещение Files.move(Paths.get("d:/autoexec.bat"), Paths.get("d:/unnecessary.bat"));// переименование
Создание новых каталогов осуществляется с помощью функции createDirectory()
класса Files
. Параметр функции имеет тип Path
.
Path dir = Paths.get("c:/NewDir"); Files.createDirectory(dir);
Для создания каталога нескольких уровней в глубину, когда один или несколько родительских каталогов, возможно, еще не существует, можно использовать метод createDirectories()
:
Path dir = Paths.get("c:/NewDir/1/2"); Files.createDirectories(dir);
Для получения списка файлов подкаталога можно воспользоваться классом DirectoryStream
.
package ua.inf.iwanoff.files; import java.io.IOException; import java.nio.file.*; public class FileListDemo { public static void main(String[] args) { Path dir = Paths.get("c:/Windows"); try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path p : ds) { System.out.println(p.getFileName()); } } catch (IOException e) { e.printStackTrace(); } } }
Удаление файлов и папок осуществляется с помощью функций delete()
и deleteIfExists()
:
Files.delete(Paths.get("d:/unnecessary.bat")); Files.deleteIfExists(Paths.get("d:/unnecessary.bat"));
Для обхода дерева каталогов пакет java.nio.files
предоставляет средства, не требующие реализации рекурсивных алгоритмов. Существует метод walkFileTree()
класса Files
, обеспечивающий обход дерева подкаталогов. В качестве параметров необходимо указать начальный каталог (объект типа Path
), , а также объект, реализующий обобщенный интерфейс FileVisitor
.
Примечание: существует другой вариант метода, позволяющий задавать также опции обхода каталогов и ограничение на глубину обхода подкаталогов.
Для реализации интерфейса FileVisitor
нужно определить методы preVisitDirectory()
, postVisitDirectory()
, visitFile()
и visitFileFailed()
. Результат этих функций – перечисление типа FileVisitResult
. Возможные значения этого перечисления – CONTINUE
(продолжать поиск), TERMINATE
(продолжать поиск), SKIP_SUBTREE
(пропустить поддерево) и SKIP_SIBLINGS
(пропустить элементы того же уровня).
Чтобы каждый раз не реализовывать все методы интерфейса FileVisitor
, можно воспользоваться обобщенным классом SimpleFileVisitor
. Этот клас предоставляет реализацию функций интерфейса по умолчанию. В этом случае необходимо только перекрыть нужные функции. В следующем примере осуществляется поиск всех файлов заданного каталога и его подкаталогов:
package ua.inf.iwanoff.files; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class FindAllFiles { private static class Finder extends SimpleFileVisitor<Path> { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("----------------" + dir + "----------------"); return FileVisitResult.CONTINUE; } } public static void main(String[] args) { String dirName = new Scanner(System.in).nextLine(); try { Files.walkFileTree(Paths.get(dirName), new Finder()); // Текущий каталог } catch (IOException e) { e.printStackTrace(); } } }
Для поиска файлов можно пользоваться масками (так называемые "glob"-маски), активно применяемых во всех операционных системах. Примеры таких масок – "a*.*
" (имена файлов начинаются на букву a
), "*.txt
" (файлы с расширением *.txt
) и т. д. Допустим, строка pattern
содержит такую маску. Далее создается объект PathMatcher
:
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
В следующем примере в заданном каталоге осуществляется поиск файлов по указанной маске:
package ua.inf.iwanoff.files; import java.io.IOException; import java.nio.file.*; import java.util.Scanner; public class FindMatched { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String dirName = scanner.nextLine(); String pattern = scanner.nextLine(); Path dir = Paths.get(dirName); PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) { for (Path file : ds) { if (matcher.matches(file.getFileName())) { System.out.println(file.getFileName()); } } } catch (IOException e) { e.printStackTrace(); } } }
Маски могут сочетаться с обходом дерева каталогов.
Одна из задач файловой системы – отслеживание состояния указанного каталога. Например, программа должна обновлять данные о файлах и подкаталогах некоторого каталога, если другие процессы или потоки управления обусловили появление, изменение, удаление файлов и папок и т. д. Пакет java.nio.files
предоставляет средства для регистрации таких каталогов и отслеживания их состояния. Для отслеживания изменений можно реализовать интерфейс WatchService
. Подходящую реализацию можно получить с помощью функции FileSystems.getDefault().newWatchService()
. Класс StandardWatchEventKinds
предоставляет необходимые константы для возможных событий.
Сначала необходимо зарегистрировать необходимый каталог, а потом в бесконечном цикле читать информацию о событиях связанных с его изменениями. Интерфейс WatchEvent
предоставляет описание возможного события. Например, можно предложить следующую программу:
package ua.inf.iwanoff.files; import java.nio.file.*; import java.util.Scanner; import static java.nio.file.StandardWatchEventKinds.*; public class WatchDir { @SuppressWarnings("unchecked") public static void main(String[] args) throws Exception { System.out.println("Введите имя каталога:"); Path dir = Paths.get(new Scanner(System.in).nextLine()); // Создаем объект WatchService WatchService watcher = FileSystems.getDefault().newWatchService(); // Регистрируем отслеживаемые события: WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); while (true) { // бесконечный цикл key = watcher.take(); // ожидаем следующий набор событий for (WatchEvent<?> event: key.pollEvents()) { WatchEvent<Path> ev = (WatchEvent<Path>)event; System.out.printf("%s: %s\n", ev.kind().name(), dir.resolve(ev.context())); } key.reset(); // сбрасываем состояние набора событий } } }
Библиотека java.nio.files
поддерживает работу как с символьными ссылками (symlinks, soft links), так и с жесткими ссылками (hard links). Метод createSymbolicLink(новая_ссылка, существующий_объект)
класса Files
создает символьную ссылку, метод createLink(новая_ссылка, существующий_файл)
создает жесткую ссылку. Метод isSymbolicLink()
возвращает true
, если переданный ему объект – символьная ссылка. Метод readSymbolicLink()
позволяет найти объект, на который ссылается символьная ссылка.
2 Примеры программ
2.1 Построчное копирование текстовых файлов
Предположим, необходимо создать программу, которая осуществляет построчное копирование текстовых файлов. Имена файлов задаются аргументами командной строки. Текст программы будет иметь следующий вид:
package ua.inf.iwanoff.files; import java.io.*; public class TextFileCopy { public static void main(String[] args) { if (args.length < 2) { System.out.println("Нужны аргументы!"); return; } try (BufferedReader in = new BufferedReader(new FileReader(args[0])); PrintWriter out = new PrintWriter(new FileWriter(args[1]))) { String line; while ((line = in.readLine()) != null) { out.println(line); } } catch (IOException e) { e.printStackTrace(); } } }
2.2 Сериализация и десериализация данных
Предположим, необходимо создать классы Страна (Country
) и Континент (Continent
), создать объект типа Continent
, осуществить его сериализацию и десериализацию. Класс Country
будет таким:
package ua.inf.iwanoff.files; import java.io.Serializable; public class Country implements Serializable { private static final long serialVersionUID = -6755942443306500892L; private String name; private double area; private int population; public Country(String name, double area, int population) { this.name = name; this.area = area; this.population = population; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getArea() { return area; } public void setArea(double area) { this.area = area; } public int getPopulation() { return population; } public void setPopulation(int population) { this.population = population; } }
Класс Continent
может быть таким:
package ua.inf.iwanoff.files; import java.io.Serializable; public class Continent implements Serializable { private static final long serialVersionUID = 8433147861334322335L; private String name; private Country[] countries; public Continent(String name, Country... countries) { this.name = name; this.countries = countries; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Country[] getCountries() { return countries; } public void setCountries(Country[] countries) { this.countries = countries; } }
Приведенная ниже программа осуществляет создание и сериализацию объекта Continent:
package ua.inf.iwanoff.files; import java.io.*; public class DataSerialization { public static void main(String[] args) { Continent c = new Continent("Европа", new Country("Украина", 603700, 46314736), new Country("Франция", 547030, 61875822), new Country("Германия", 357022, 82310000) ); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Countries.dat"))) { out.writeObject(c); } catch (IOException e) { e.printStackTrace(); }; } }
Так можно осуществить десериализацию:
package ua.inf.iwanoff.files; import java.io.*; public class DataDeserialization { public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("Countries.dat"))) { Continent continent = (Continent) in.readObject(); for (Country c : continent.getCountries()) { System.out.println(c.getName() + " " + c.getArea() + " " + c.getPopulation()); } } catch (IOException e) { e.printStackTrace(); }; } }
2.3 Сериализация и десериализация объектов обобщенных классов
Допустим, мы создали обобщенный класс Triple
(тройка).
package ua.inf.iwanoff.files; import java.io.*; class Triple<T> implements Serializable { private static final long serialVersionUID = 7512336951571111736L; T x, y, z; Triple(T x, T y, T z) { this.x = x; this.y = y; this.z = z; } }
Можно создать объекты с различными параметрами обобщения и осуществить сериализацию:
package ua.inf.iwanoff.files; import java.io.*; public class GenericsSerialization { public static void main(String[] args) { Triple<Integer> t1 = new Triple<>(1, 2, 3); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Integers.dat"))) { out.writeObject(t1); } catch (IOException e) { e.printStackTrace(); }; Triple<String> t2 = new Triple<>("A", "B", "C"); try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Strings.dat"))) { out.writeObject(t2); } catch (IOException e) { e.printStackTrace(); }; } }
Можно осуществить десериализацию:
package ua.inf.iwanoff.files; import java.io.*; public class GenericsDeserialization { @SuppressWarnings("unchecked") public static void main(String[] args) throws ClassNotFoundException { try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("Integers.dat"))) { Triple<Integer> t1 = (Triple<Integer>) in.readObject(); System.out.printf("%d %d %d%n", t1.x, t1.y, t1.z); } catch (IOException e) { e.printStackTrace(); }; try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("Strings.dat"))) { Triple<String> t2 = (Triple<String>) in.readObject(); System.out.printf("%s %s %s%n", t2.x, t2.y, t2.z); } catch (IOException e) { e.printStackTrace(); }; } }
Как видно из примера, для сериализации и десериализации можно использовать классы с пакетной видимостью.
2.4 Работа с архивом
Данные об объектах из примера 2.2 можно сохранить в архиве. Приведенная ниже программа осуществляет создание объекта Continent
и сохранение данных в архиве. Каждой стране соответствует своя точка входа ZipEntry
:
package ua.inf.iwanoff.files; import java.io.*; import java.util.zip.*; public class StoreToZip { public static void main(String[] args) { Continent continent = new Continent("Европа", new Country("Украина", 603700, 46314736), new Country("Франция", 547030, 61875822), new Country("Германия", 357022, 82310000) ); try (ZipOutputStream zOut = new ZipOutputStream(new FileOutputStream("Continent.zip")); DataOutputStream out = new DataOutputStream(zOut)) { for (Country country : continent.getCountries()) { ZipEntry zipEntry = new ZipEntry(country.getName()); zOut.putNextEntry(zipEntry); out.writeDouble(country.getArea()); out.writeInt(country.getPopulation()); zOut.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
Так можно осуществить чтение из архива:
package ua.inf.iwanoff.files; import java.io.*; import java.util.zip.*; public class ReadFromZip { public static void main(String[] args) { try (ZipInputStream zIn = new ZipInputStream(new FileInputStream("Continent.zip")); DataInputStream in = new DataInputStream(zIn)) { ZipEntry entry; while ((entry = zIn.getNextEntry()) != null) { System.out.println("Страна: " + entry.getName()); System.out.println("Территория: " + in.readDouble()); System.out.println("Население: " + in.readInt()); System.out.println(); zIn.closeEntry(); } } catch (IOException e) { e.printStackTrace(); } } }
2.5 Обход дерева файлов с использованием средств класса File
Предположим, необходимо создать программу, которая осуществляет поиск файлов с длиной не меньше заданной во всех подкаталогах, начиная с некоторого каталога. Можно использовать класс java.io.File
. Программа будет иметь следующий вид:
package ua.inf.iwanoff.files; import java.io.File; import java.io.IOException; import java.util.Scanner; public class FindWithIO { public static void showList(File dir, int len) throws IOException { for (File f : dir.listFiles()) { if (!f.isDirectory()) { if (f.length() > len) { System.out.println(f.getCanonicalPath() + " " + f.length()); } } } for (File f : dir.listFiles()) { if (f.isDirectory()) { showList(f, len); } } } public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); System.out.print("Введите имя папки:"); String dirName = scanner.next(); File dir = new File(dirName); if (!dir.isDirectory()) { System.out.println("Неправильное имя папки!"); return; } System.out.print("Введите минимальную длину файла:"); int len = scanner.nextInt(); showList(dir, len); } }
2.6 Обход дерева файлов с использованием средств пакета java.nio.file
Предыдущую задачу можно решить с использованием средств пакета java.nio.file
. Программа будет иметь следующий вид:
package ua.inf.iwanoff.files; import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Scanner; public class FindWithNIO { private static class Finder extends SimpleFileVisitor<Path> { private int len; Finder(int len) { this.len = len; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (Files.size(file) >= len) { System.out.println(file + " " + Files.size(file)); } return FileVisitResult.CONTINUE; } } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Введите имя папки:"); String dirName = scanner.nextLine(); System.out.print("Введите минимальную длину файла:"); int len = scanner.nextInt(); try { Files.walkFileTree(Paths.get(dirName), new Finder(len)); // Текущий каталог } catch (IOException e) { e.printStackTrace(); } } }
3 Задания на самостоятельную работу
3.1 Работа с текстовыми файлами
Разработать программу, которая осуществляет копирование из одного текстового файла в другой. Осуществить копирование только тех строк, длина которых не превосходит введенного значения.
3.2 Текстовые файлы с числовыми данными
Разработать программу, которая осуществляет чтение из текстового файла действительных значений (до конца файла), находит произведение модулей ненулевых элементов и выводит в другой текстовый файл.
3.3 Работа с несколькими файлами*
Разработать программу, которая осуществляет чтение из текстового файла целых чисел и сохранение в двух файлах данных (DataOutputStream
) соответственно четных и нечетных чисел. В другой программе прочитать данные из двух файлов данных (DataInputStream
) и записать их в новый текстовый файл в порядке убывания. Использовать PriorityQueue
для обеспечения упорядоченности чисел.
3.4 Реализация сериализации и десериализации*
Описать классы Студент и Академическая группа (с массивом студентов в качестве поля). Создать объекты, осуществить их сериализацию и десериализацию.
3.5 Сериализация и десериализация объектов обобщенных классов*
Описать классы "Учебное заведение" и "Массив" (обобщенный класс). Создать массив объектов "Учебное заведение", осуществить сериализацию и десериализацию.
3.6 Работа с ZIP-архивом*
Описать классы Студент и Академическая группа (с массивом студентов в качестве поля). Создать объекты Студент и Академическая группа, осуществить запись данных о студентах академической группы в архив (ZIP). В другой программе осуществить чтение из архива.
3.7 Архивация нескольких файлов*
Реализовать две программы, одна из которых осуществляет архивацию нескольких указанных файлов, а другая – извлечение их из архива.
3.8 Реализация алгоритма Хаффмана (задача повышенной трудности)
Разработать программу, которая осуществляет архивацию текстового файла с использованием алгоритма Хаффмана, а также извлечение данных из архива.
3.9 Работа с классом File*
Создать новый файл в корневой папке проекта. Вывести список файлов и папок корневой папки проекта. Удалить только что созданный файл. Использовать класс java.io.File
.
3.10 Получение информации об атрибутах файлов*
Разработать программу, которая для заданного файла (каталога) выдает информацию об его атрибутах. Использовать класс DosFileAttributes
.
3.11 Копирование файлов*
До начала выполнения программы создать каталог с несколькими файлами. В программе создать новый каталог и скопировать туда файлы из ранее созданного. Удалить ранее созданный каталог. Использовать класс java.io.File
.
3.12 Перемещение файлов
Решить предыдущую задачу с использованием средств перемещения (переименования) файлов.
3.13 Обход дерева каталогов*
Ввести имя подкаталога и осуществить поиск всех скрытых файлов во всех подкаталогах, начиная с некоторого каталога. Реализовать два подхода – с использованием класса java.io.File
и с использованием средств пакета java.nio.file
.
3.14 Отслеживание состояния каталога*
Реализовать программу отслеживания появления, изменения и удаления в заданном каталоге файлов с расширением .txt
. Использовать средства пакета java.nio.file
.
4 Контрольные вопросы
- В чем отличие текстовых и двоичных файлов?
- Чем отличаются потоки байтов от потоков символов по области применения?
- В чем смысл явного закрытия файлов?
- Можно ли одновременно открыть несколько потоков ввода/вывода?
- Каким образом можно обеспечить автоматическое закрытие потоков?
- Какие классы обеспечивают работу с текстовыми файлами и бинарными файлами?
- В чем преимущества использования класса
RandomAccessFile
? - Каково использование файлов данных
DataOutputStream
иDataInputStream
? Какие у них преимущества и недостатки? - В чем назначение сериализации?
- В чем есть преимущества и недостатки сериализации?
- Какие функции следует определить для реализации интерфейса
java.io.Serializable
? - Для чего используют модификатор
transient
? - Как в Java осуществляется работа с архивами?
- Можно ли создать архив с несколькими файлами внутри?
- Как определить понятие "файловая система"?
- Какие можно назвать типовые функции для работы с файловой системой?
- Какие средства предоставляет Java для работы с файловой системой?
- Как получить атрибуты файла с помощью средств класса
java.io.File
? - Чем отличаются функции
list()
иlistFiles()
? - Как осуществить копирование файлов?
- Как осуществить переименование и перемещение файлов?
- Как осуществить управление атрибутами файлов?
- Как осуществить удаление файлов?
- Как осуществить поиск файлов?
- Как осуществить обход дерева каталогов?
- Как осуществить отслеживание изменений каталогов и файлов?