Java Basics: IO

File

Основной класс для работы с файлами и директориями.

1
2
3
public File(String)
public File(File, String)
public File(String, String)

Получение сепараторов, присущих ОС:

1
2
System.getProperty("file.separator");
java.io.File.separator;

I/O Streams

Приняты следующие правила именования:

  • *InputStream/*OutputStream - классы для работы с бинарными данными (массив байт);
  • *Reader/*Writer - классы для работы с потоками символов.

Low-level VS High-level

Low-level классами считаются те, которые работают с источником данных напрямую (например, FileInputStream/ByteArrayOutputStream).
High-level классы - это обертки, которые могут как-то влиять на Low-level стрим (например, BufferedReader). Могут добавлять новые методы, а так же изменять уже существующие.

Правила именования

  • InputStream/OutputStream работают с байтовым потоком данных;
  • Reader/Writer работают с потоком символов или строк;
  • Большинство классов имеют зеркальные in/out реализации;
  • Low-level подключаются к источнику данных;
  • High-level используют другой поток в качестве источника;
  • Buffered* работают с группами байт или символов, могут значительно увеличить производительность и рекомендованы к использованию.

Основные используемые классы

  • FileInputStream/FileOutputStream - работает с файлами. Low-level. Может выкинуть FileNotFoundException. В атрибутах может принимать флаг append для записи в конец файла (при значении false полностью переписывает файл);
  • FileReader/FileWriter - работает с файлами, но как с потоком символов и строк. Low-level;
  • BuffedInputStream/BufferedOutputStream - работает с потоком байт и использует буферизацию - сохраняет данные в буфер при чтении/записи из другого потока, не задерживая его чтения/записи. High-level. Может значительно повысить производительность I/O операции. Рекомендуемый объем буфера - 2n;
  • BufferedReader/BufferedWriter - работает с символами и строками, использует буферизацию. High-level. Дополняет Buffered*Stream несколькими методами: readLine() - чтение строки до следующего возврата каретки; newLine() - в output добавляет символ возврата каретки;
  • ObjectInputStream/ObjectOutputStream - сериализует и десериализует POJO объекты. High-level;
  • PrintStream - класс, который расширяет OutputStream, позволяя записывать в него примитивы и POJO. Допускает использование форматирование записи (аналог String.format). High-level;
  • PrintWriter - класс аналогичный PrintStream, но работает с потоком как с потоком символов и строк. Используется как обертка для Writer. High-level.

Основные операции

  • read()/write() - чтение/запись в поток допускает использование int, т.к. в метод write можно передать -1, что будет означать окончание записи. На текущий момент (в современных JVM) выкидывается исключение вместо -1. Так же имеется реализация с byte[] (+offset length). При этом, offset и length будет наложен именно на результирующий массив, а не на поток (данные поступающие в него останутся неизменны). Для Reader и Writer своя реализация с char[];
  • close() - закрывает поток. В случае если используется High-level реализация, то закрывать достаточно его, используемый Low-level закроется автоматически:
1
try(var io = new BufferedOutputStream(new FileOutputStream("out.dat"))) {...}
  • markSupported() - только для input. Указывает, допускает ли реализация потока маркировку позиции;
  • mark(int) - устанавливает маркер, к которому можно будет вернуться при вызове метода reset(). Т.е. установив маркер, позиция в потоке запомнится и после вызова reset() чтение продолжится именно с этой позиции. Если количество прочитанных данных больше значения, переданного в атрибуте метода, может быть выдано исключение (зависит от реализации потока);
  • reset() - сбрасывает маркер и возвращается к чтению потока в ту позицию, которая была помечена маркером. Фактически, при mark() -> reset() происходит буферизация данных потока в локальный кэш и чтение возобновляется после прочтения всех данных из буфера. Но если буферизация не поддерживается, то при попытке вызова метода может быть выкинуто исключение;
  • skip(int) - пропуск указанного количества байт при чтении потока. Результатом метода будет фактическое количество пропущенных байт. Метод синхронный, т.е. активный JVM поток (Thread) будет ожидать завершения метода (пропуска указанного количества или завершения потока);
  • flush() - только для Output. Сбрасывает данные из локального кэша ОС в поток данных и ожидает подтверждения выполнения действия, в противном случае будет ошибка.

Сериализация POJO

Для сериализации объекта, его класс должен реализовывать маркировочный интерфейс Serializable.
В случае если поле класса будет иметь модификатор transient, он будет проигнорирован при (де)сериализации (при чтении объекта, таким полям будет присвоено значение по умолчанию в соответствии с правилами языка). Поле serialVersionUID является необязательным, но помогает JVM понять, можно ли десериализовывать тот или иной объект - в случае различия версий, будет выдано исключение InvalidClassException (при этом существуют API, которые избегают его и “подстраиваются” под версию данных).

Требования к реализации:

  • Класс объекта должен быть промаркирован Serializable;
  • Поля должны быть сериализуемыми, или помеченными transient, или иметь null как значение при сериализации.

Сериализация объектов выполняется с помощью ObjectInputStream и ObjectOutputStream, которые являются High-level. Реализуют методы readObject и writeObject.
Чтение будет специфичное и отличаться от стандарта:

1
2
3
4
5
6
7
8
9
10
11
try(var in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("my_object.dat")))) {
while(true) {
var obj = in.readObject();
if(obj instanceof MyClass) {
MyClass myObj = (MyClass) obj;
...
}
}
} catch(EOFException eofe) {
// all is correct. Object is received!
}

Таким образом чтение будет осуществляться до тех пор, пока из входящего потока будут приходить сериализованные данные. Если количество объектов внутри входящего потока известно, можно ограничиться соответствующим количеством вызовов readObject().

Процесс десериализации

Десериализации (и сериализации) подвергаются все поля, как родительских классов, так и целевого. При этом для родительских тоже действует условие, что они должны быть Serializable - все родители, которые не промаркированы Serializable, не будут сериализованы:

1
2
3
4
5
6
7
8
9
class Ape {
private int height = 75;
}

class Chimpanzee extends Ape implements Serializable {
private String name = "A";
private transient int age = 10;
...
}

Поле height сериализовано не будет и будет принимать стандартное значение (присваивается instance-init или constructor и т.д.). Поле name и поле age изначально будут иметь значения по умолчанию - null и 0, соответственно. Затем для одного из них будет десериализовано (прочитано) значение и присвоено. Т.к. age помечен как transient, он не будет (де)сериализован, а значит, его значение, так и останется равным 0.

1
Chimpanzee obj = new Chimpanzee("B", 15);

При десериализации, объект obj будет иметь значения: name = "B", age = 0, height = 75.

Важно! При десериализации, конструктором объекта будет выбран ближайший no-arg конструктор того родителя, у которого нет(!) маркировки Serializable.

Для всех прочих полей (фактически, сериализуемых) будут присвоены значения по умолчанию (String = null, int = 0, Long = null) и только после этого начнется десериализация. Т.е. поля, которые имеют значения, их получат.

NIO.2 (Non-blocking I/O)

В отличие от I/O API, поддерживает символические ссылки, предусмотренные файловой системой ОС.

Path. Получение

  • Path.of. Поддерживает как полный путь, так и varargs. Начиная с Java 11+;
  • Paths.get. Метод аналогичный Path.of;
  • Path.of(URI), Paths.get(URI). Может выкидывать исключение IllegalArgumentException, если URI не абсолютный (зависит от ОС);
  • FileSystems.getPath(). Допускает получение через объект FileSystems:
1
Path path = FileSystems.getDefault().getPath("...");

Так же, можно получить ресурсы с удаленной файловой системы:

1
2
FileSystem remote = FileSystem.getFileSystem(new URI("http://..."));
Path path = remote.getPath("...");
  • Из File и обратно:
1
2
3
File file = new File("...");
Path path = file.toPath();
file = path.toFile();

Опции для NIO.2

  • LinkOption implements CopyOption, OpenOption;
    • NOFOLLOW_LINKS - не следовать по ссылке на файл, если она есть;
  • StandardCopyOption implements CopyOption;
    • ATOMIC_MOVE - при перемещении файла выполнять операцию как атомарную на уровне ОС;
    • COPY_ATTRIBUTES - копировать атрибуты старого файла в новый (например, дату создания, дату изменения и т.д.);
    • REPLACE_EXISTING - заменить файл если он уже существует. Без указания данной опции в случае существования файла будет выброшено исключение;
  • StandardOpenOption implements OpenOption;
    • APPEND - дописывать данные в конец существующего файла;
    • CREATE - создать файл если не существует;
    • CREATE_NEW - создать файл если не существует; если файл существует, будет выброшено исключение;
    • READ - только чтение;
    • TRUNCATE_EXISTING - если файл существует, удалить содержимое и писать в пустой файл;
    • WRITE - допустить чтение и запись в файл;
  • FileVisitOption;
    • FOLLOW_LINKS - переходить по ссылкам на другие ресурсы, если они указаны.

Объект Path - неизменяемый (immutable), а значит, любая попытка его изменить создает новый объект.

Корень файловой системы не рассматривается как часть списка имен:

1
2
3
var path = Path.of("/");
var cnt = path.getNameCount(); // 0
var last = path.getLastName(); // IllegalArgumentException

subpath() - Метод возвращает вырезанную часть пути. Аргумент begin - начальный индекс (включительно); end - конечный индекс (исключительно):

1
2
3
4
Path path = Path.of("/this/is", "file", "in", "the", "dir");
Path subPath1 = path.subpath(0, path.getNameCount() -1); // /this/is/file/in/the
Path subPath2 = path.subpath(0, 2); // /this/is
Path subPath3 = path.subpath(2, 4); // /file/in

При выходе за количество имен, будет выброшено исключение IllegalArgumentException.

getFileName() - получает имя последнего элемента пути

getParent() - получает путь до родительского элемента, относительно текущего. Может включать относительные пути типа “..”, “.”. Внимательно, они считаются отдельными элементами пути!

getRoot() - возвращает корневую директорию. Доступно только(!) при указании абсолютного пути.

resolve() - принимает String или Path, который добавляется в конец к своему пути и возвращает новый Path. Если был передан корневой для ОС элемент (т.е. абсолютный путь), то вернется именно этот абсолютный путь.

relativize() - получает относительный путь одного Path относительно другого. Для упрощения восприятия следует думать что текущей директорией является конечный элемент в Path:

1
2
3
4
var path1 = Path.of("file1"); // abs: /example/files/file1
var path2 = Path.of("dir/another"); // abs: /example/files/dir/another
var rel1 = path1.relativize(path2); // ../dir/another
var rel2 = path2.relativize(path1); // ../../file1

Так же, можно искать максимально похожий ближайший путь. Особенно хорошо и удобно работает с относительными путями). НО! Относительные и абсолютные пути связывать таким методом нельзя! Для Windows так же нельзя сравнивать пути между разными томами (C:, D: и т.д.).

normalize() - нормализует строки пути, убирая из нее лишние специальные ссылки на родительские и текущие директории (“..” и “.”, соответственно). Следует помнить что фактического обращения к файловой системе еще не выполняется, а следовательно и получить имя родительской директории невозможно (если она является ссылкой):

1
2
Path.of("./abc/../123").normalize(); // 123
Path.of("../../parent/./123").normalize(); // ../../parent/123

toRealPath() - попытка приведения Path к фактическому, который существует в файловой системе. Если по построенному пути (относительному или абсолютному) не будет найден объект действительно существующий, будет исключение. В случае если на пути будет встречен symlink, она будет подставлена вместо исходного элемента в пути:

1
2
// myLink.lnk = symlink:dir-abs
Paths.get(“myLink.lnk/myFile”); // dir-abs/myFile

Переход по ссылкам может быть отмечен специальной опцией.

Операции с файлами и директориями

Ниже будет рассмотрены методы вспомогательного класса Files. Большинство кидают IOException если объект не найден.

exists(Path, LinkOption) - проверяет наличие файла или директории по указанному пути. Не выкидывает IOException.

isSameFile(Path, Path) - проверяет, являются ли файлы или директории ссылкой на один и тот же объект в файловой системе. При проверке выполняются все соответствующие переходы по symlink.

createDirectory(Path, FileAttribute<?>...), createDirectories(Path, FileAttribute<?>...) - создает целевую директорию. В случае если директория существует, будет выброшено исключение IOException. Метод createDirectories создает (если они отсутствуют) все директории, необходимые для создания целевого объекта. createDirectory в случае невозможности достижения пути до целевого объекта выкинет исключение IOException.

copy(Path, Path, CopyOptions...) - метод выполняет копирование объекта файловой системы из одного места в другое. При этом, файл действительно копируется, а директория, де-факто, просто создается с тем же именем. Если исходный объект копирования не существует, будет выброшено исключение IOException; если объект по целевому пути уже существует, будет выброшено исключение IOException.
При передаче StandardCopyOption.REPLACE_EXISTING будет выполнена замена целевого объекта без IOException.

copy(InputStream, Path) - копирует содержимое из InputStream в файл. Если в Path будет передана директория, копирование будет выполнено, но в новый файл (там же где и директория). Но если директория передана в Path и она не пустая, то будет DirectoryNotEmptyException.

copy(Path, OutputStream) - копирует содержимое файла в OutputStream. Если будет передана директория или не существующий файл, будет выброшено IOException.

move(Path, Path, CopyOptions ...) - выполняет перемещение одного объекта файловой системы в новое место. На уровне файловой системы выполняется именно перемещение, а не копирование с последующим удалением исходного объекта.
Если целевой объект уже существует или исходного объекта не существует - будет выброшено IOException.

При Files.move можно передать StandardCopyOption, который указывает файловой системе, что операция перемещения нужно выполнять атомарно - либо все, либо ничего, без возможности доступа сторонних программ и потоков к ресурсу.
Если ОС не поддерживает опцию ATOMIC_MOVE, будет выброшено IOException (а точнее, AtomicMoveNotSupportedException). Если ATOMIC_MOVE передать в метод copy(), будет выброшено IOException.

delete(Path), deleteIfExists(Path) - удаляет объект файловой системы. Если выполняется попытка удаления директории и она не пустая, будет выброшено исключение IOException. Если удаляется symlink, то будет удалена именно ссылка, а не объект, на который ссылка указывает.

newBufferedReader(Path), newBufferedWriter(Path) - методы позволяют читать и записывать в файл, соответственно. Могут принимать Charset (по умолчанию, выбор Charset зависит от ОС, но как правило, это UTF-8). Закрывать потоки придется самостоятельно. Вызов методов можно обернуть в конструкцию try-with-resources-catch-finally.

readAllLines(Path) - возвращает список (List<String>) строк. Может вызвать OutOfMemoryException. Читает весь файл, может принимать Charset.

isDirectory(Path, LinkOptions...) - проверяет, является ли запрашиваемый объект директорией. Может принимать NOT_FOLLOW_LINKS.

isSymbolicLink(Path) - проверяет, является ли объект ссылкой.

isRegularFile(Path, LinkOptions ...) - проверяет, является ли объект файлом.

isHidden(Path) - проверяет, является ли объект скрытым.

isReadable(Path) - проверяет, доступен ли объект для чтения.

isWritable(Path) - проверяет, доступен ли объект для записи.

isExacutable(Path) - проверяет, является ли объект исполняемым.

size(Path) - получает размер файла. При попытке применения метода на директорию, будет выведено некое значение, характеризующее стандарт для ОС (например, 4096).

getLastModifiedTime() - возвращает FileTime, обозначающий дату последнего изменения в UTC. Может вернуть и дату для директории (получает дату из атрибутов объекта, так что доверять данному методу на 100% нельзя).

readAttributes(Path, Class<A>) - вернет атрибуты, соответствующие переданному классу A. Доступные классы:

  • BasicFileAttributes - базовые атрибуты;
  • DosFileAttributes - атрибуты специфичные для DOS/Windows;
  • PosixFileAttributes - атрибуты специфичные для UNIX, Linux, Mac и т.д.
    Для DOS/Windows, в основном, признаки системности, скрытности. Для Posix - в основном, признаки прав. При использовании, оба возвращают включенные Basic атрибуты.

list(Path) - получает Stream<File> с перечнем файлов и директорий, входящих в переданную директорию. Кидает NoDirectoryException при указании в атрибуте не директории.
Данные Stream необходимо закрывать, т.к. он использует ресурсы операционной системы. Поэтому рекомендуется вставлять его в try-with-resources. В нашем случае, любая терминальная операция класса Stream не закрывает ресурсы с точки зрения ОС (а Stream считается ресурсом).

walk(Path, int, FileVisitOptions ...) - позволяет выполнить обход по содержимому директории и возвращает Stream, который содержит ссылки на содержащиеся Path. Допускает передачу максимальной глубины погружения относительно исходной директории (по умолчанию, имеет значение Integer.MAX). Считывание производится согласно иерархии по уровням (т.е. не по веткам, а последовательно от уровня к уровню). По умолчанию, данный метод не учитывает переход по ссылкам (как некоторые другие методы), но переход можно принудительно включить используя FOLLOW_LINKS - тогда метод будет “проваливаться” по ссылкам. При этом, возникает возможность цикличности (если ссылка указывает на какой-то объект в рамках корневой директории) - если JVM это заметит, будет выброшено исключение FileSystemLoopException.

find(Path, int, BiPredicate<Path, BasicAttributes>, FileVisitOptions ...) - метод выполняет схожую с walk() операцию, но в Stream попадают только те объекты, которые пройдут проверку BiPredicate (фактически, исполняет роль фильтра).

lines(Path) - позволяет считывать построчно файл, но не загружает его в память целиком (как это делает readAllLines()), а читает их последовательно в Stream<String>. Требует закрывать Stream, т.е. лучше оборачивать в try-with-resources. Шанс что выпадет OutOfMemoryException довольно незначителен.

 Comments
Comment plugin failed to load
Loading comment plugin