File
Основной класс для работы с файлами и директориями.
1 | public File(String) |
Получение сепараторов, присущих ОС:
1 | System.getProperty("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 | try(var in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("my_object.dat")))) { |
Таким образом чтение будет осуществляться до тех пор, пока из входящего потока будут приходить сериализованные данные. Если количество объектов внутри входящего потока известно, можно ограничиться соответствующим количеством вызовов readObject()
.
Процесс десериализации
Десериализации (и сериализации) подвергаются все поля, как родительских классов, так и целевого. При этом для родительских тоже действует условие, что они должны быть Serializable
- все родители, которые не промаркированы Serializable
, не будут сериализованы:
1 | class Ape { |
Поле 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 | FileSystem remote = FileSystem.getFileSystem(new URI("http://...")); |
- Из File и обратно:
1 | File file = new File("..."); |
Опции для 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 | var path = Path.of("/"); |
subpath()
- Метод возвращает вырезанную часть пути. Аргумент begin
- начальный индекс (включительно); end
- конечный индекс (исключительно):
1 | Path path = Path.of("/this/is", "file", "in", "the", "dir"); |
При выходе за количество имен, будет выброшено исключение IllegalArgumentException
.
getFileName()
- получает имя последнего элемента пути
getParent()
- получает путь до родительского элемента, относительно текущего. Может включать относительные пути типа “..”, “.”. Внимательно, они считаются отдельными элементами пути!
getRoot()
- возвращает корневую директорию. Доступно только(!) при указании абсолютного пути.
resolve()
- принимает String
или Path
, который добавляется в конец к своему пути и возвращает новый Path
. Если был передан корневой для ОС элемент (т.е. абсолютный путь), то вернется именно этот абсолютный путь.
relativize()
- получает относительный путь одного Path
относительно другого. Для упрощения восприятия следует думать что текущей директорией является конечный элемент в Path
:
1 | var path1 = Path.of("file1"); // abs: /example/files/file1 |
Так же, можно искать максимально похожий ближайший путь. Особенно хорошо и удобно работает с относительными путями). НО! Относительные и абсолютные пути связывать таким методом нельзя! Для Windows так же нельзя сравнивать пути между разными томами (C:, D: и т.д.).
normalize()
- нормализует строки пути, убирая из нее лишние специальные ссылки на родительские и текущие директории (“..” и “.”, соответственно). Следует помнить что фактического обращения к файловой системе еще не выполняется, а следовательно и получить имя родительской директории невозможно (если она является ссылкой):
1 | Path.of("./abc/../123").normalize(); // 123 |
toRealPath()
- попытка приведения Path
к фактическому, который существует в файловой системе. Если по построенному пути (относительному или абсолютному) не будет найден объект действительно существующий, будет исключение. В случае если на пути будет встречен symlink, она будет подставлена вместо исходного элемента в пути:
1 | // myLink.lnk = symlink:dir-abs |
Переход по ссылкам может быть отмечен специальной опцией.
Операции с файлами и директориями
Ниже будет рассмотрены методы вспомогательного класса 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
довольно незначителен.