W3docs

Введение в Java I/O

Обзор Java I/O: байтовые и символьные потоки, буферизованный I/O, java.io и java.nio.file.

Часть 12 завершилась набором понятий, которые вы сможете применить напрямую в этой части: лямбды, Consumer<T> и Supplier<T> как шаблоны для «дай мне строку» и «сделай что-нибудь с этой строкой», try-with-resources для всего, что требует детерминированного закрытия, и конвейер Stream для построчной обработки данных. API ввода-вывода Java были спроектированы именно под эти шаблоны — задолго до того, как появился термин «функциональный интерфейс», у базовых объектов уже был ровно один метод каждый, а фасад, появившийся после Java 8, довёл всё до конца.

В этой части рассматриваются четыре перекрывающихся набора инструментов:

  1. Потоки java.io — оригинальный API Java 1.0: InputStream/OutputStream для байтов, Reader/Writer для символов и декораторы Buffered*, Data*, Print*, которые оборачивают их.
  2. java.io.File — устаревший класс «эта строка является путём». До сих пор повсеместно встречается в старом коде; для нового кода заменён java.nio.file.Path.
  3. java.nio.file — современный API (Java 7+): Path, Files и статические вспомогательные методы (Files.readString, Files.writeString, Files.lines, Files.walk), которые сводят большинство файловых операций к одной строке.
  4. Сериализация — преобразование объектных графов в байты и обратно с помощью ObjectOutputStream / ObjectInputStream.

Первые шесть глав рассматривают высокоуровневые файловые операции (открытие, создание, чтение, запись, удаление) с использованием как java.io, так и java.nio.file, чтобы вы могли увидеть одну и ту же задачу двумя способами. Средние главы подробно изучают сами классы потоков — байтовые и символьные, буферизацию, данные, вывод. Последние главы посвящены сериализации и API path/walk.

Разделение байт/символ

Каждый API ввода-вывода в java.io имеет одну из двух форм:

InputStream  /  OutputStream     — byte-oriented   (raw bytes: int read() returns 0..255 or -1)
Reader       /  Writer            — character-oriented (decoded text: int read() returns a char or -1)

Это разделение не косметическое. Байты — это то, что хранят диски и сокеты; символы — это то, что читают люди. .png — это байты; .txt — тоже байты на диске, но обычно вы хотите декодировать их в символы с помощью кодировки. Смешение двух форм без указания кодировки — это самая распространённая причина ошибок «странных символов» в устаревшем Java-коде.

Классы-мосты — InputStreamReader и OutputStreamWriter — выполняют преобразование между ними и принимают аргумент Charset. Используйте StandardCharsets.UTF_8, если у вас нет задокументированной причины использовать что-то другое; формы без аргументов используют кодировку платформы по умолчанию, которая различается в разных операционных системах и является хрестоматийным источником ошибок «работает на моём Mac, сломано на Linux-сервере».

Паттерн декоратор

java.io построен на паттерне декоратор: небольшой набор исходных потоков (FileInputStream, FileOutputStream, FileReader, FileWriter) оборачивается в многоуровневую функциональность (буферизация, построчный текст, примитивные типы, форматированный вывод). Вы компонуете то, что вам нужно, в месте вызова:

// Read a UTF-8 text file line by line:
try (BufferedReader in = new BufferedReader(
        new InputStreamReader(new FileInputStream("a.txt"), StandardCharsets.UTF_8))) {
  String line;
  while ((line = in.readLine()) != null) {
    System.out.println(line);
  }
}

Три уровня, снизу вверх: FileInputStream читает сырые байты; InputStreamReader декодирует их как символы UTF-8; BufferedReader добавляет буфер в памяти и метод readLine(). Каждый уровень — это отдельный класс с одной задачей. Java 11 свела именно этот паттерн к одной строке — Files.newBufferedReader(path) — но декорирование по-прежнему происходит внутри.

try-with-resources — это правило

Каждый поток, читатель, писатель и канал в java.io и java.nio реализует AutoCloseable. Закрытие важно: незакрытый FileOutputStream может потерять хвостовой буфер; незакрытый сокет утечёт файловый дескриптор; незакрытый читатель в Windows удерживает блокировку, которую ОС не освободит. Конструкция try-with-resources (Java 7+) гарантирует вызов close() на каждом успешном и исключительном пути:

try (BufferedReader in = Files.newBufferedReader(path)) {
  return in.readLine();
}                                  // close() runs here, even if readLine() throws

В одном try можно объявить несколько ресурсов; они закрываются в обратном порядке. Старый код try/finally, который вручную вызывает close(), почти всегда неверен — внутреннее исключение поглощает исключение закрытия, или само закрытие забывается на пути ошибки. Используйте try-with-resources для всего, что открывает дескриптор.

java.io против java.nio.file

java.io.File (1996) моделировал путь как String и предлагал несколько операций (exists, isFile, delete, listFiles). Класс до сих пор повсеместно встречается в устаревшем коде, и многие API по-прежнему возвращают или принимают File. Но у него есть ограничения, которые JDK больше не замалчивает:

  • Нет способа узнать, почему операция завершилась неудачей — file.delete() возвращает false для «файла не существует», «доступ запрещён» и «файл открыт». Вы не можете различить эти случаи.
  • Нет поддержки символических ссылок, атрибутов файлов, разрешений или атомарных операций.
  • Нет способа обойти дерево каталогов без написания рекурсии вручную.

java.nio.file (Java 7) заменяет его. Path — новый тип «это путь», а Files — класс static-утилит примерно с 80 методами для всего, что вы захотите с ним сделать:

Path p = Path.of("data", "users.txt");                       // platform-independent path
String text = Files.readString(p, StandardCharsets.UTF_8);   // whole file, one call
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
Files.writeString(p, "hello\n", StandardCharsets.UTF_8);
try (Stream<String> s = Files.lines(p, StandardCharsets.UTF_8)) {
  s.filter(l -> !l.isBlank()).forEach(System.out::println);
}

Два момента, на которые стоит обратить внимание. Первый: Files.lines(path) возвращает Stream<String> — конвейер потоков, который вы изучили в части 12, читает файлы напрямую. Второй: поток владеет открытым файловым дескриптором, поэтому обёртка try-with-resources обязательна — без неё файл остаётся открытым до следующего GC.

На протяжении всей части 13 мы будем показывать оба API рядом. Новый код должен в первую очередь использовать java.nio.file; устаревшие главы существуют потому, что вы встретите старые шаблоны в каждой кодовой базе, созданной до Java 11.

Что нас ждёт в этой части

  • Следующая глава, Класс File в Java, рассматривает устаревший API java.io.File — его методы запроса, листинг и ограничения, которые мотивировали появление java.nio.file.
  • Четыре следующие главы (Создание файлов, Чтение файлов, Запись файлов, Удаление файлов) охватывают высокоуровневые операции «сделать одно действие с файлом» с использованием обоих API.
  • Главы о байтовых, символьных и буферизованных потоках подробно рассматривают стек декораторов java.io.
  • Сериализация, а затем Path, Files и API обхода каталогов завершают часть.

Практический пример: одна задача, четыре способа

Программа ниже записывает один короткий текстовый файл четырьмя способами — через современный Files.writeString, классический FileWriter + try-with-resources, декорированный BufferedWriter и PrintWriter для форматированного вывода. Затем она читает файл двумя способами — через Files.readString (весь файл одним вызовом) и через Files.lines как Stream<String>, отфильтрованный Predicate<String>. Пример использует системный временный файл, чтобы работать в любой среде.

java— editable, runs on the server

Что можно извлечь из выполнения:

  • Один и тот же файл был записан четырьмя разными способами. Files.writeString — это кратчайший путь для «положить эту строку в этот файл»; FileWriter — классический исходный писатель; BufferedWriter добавляет буфер в памяти (это дёшево, когда вы записываете много мелких фрагментов); PrintWriter добавляет printf. Каждый раз перезаписывал предыдущее содержимое, потому что режим открытия по умолчанию — «усечь, затем записать» — для добавления в файл нужно передать StandardOpenOption.APPEND (рассматривается в главе о записи).
  • Каждый писатель работал внутри try-with-resources. Пропуск этого для буферизованного писателя — это ошибка, при которой последние несколько символов никогда не попадают на диск: именно close() сбрасывает хвостовой буфер.
  • Files.readString вернул весь файл в виде одного String — подходит для маленьких файлов, неверный выбор для 4-гигабайтного лога. Files.lines вернул Stream<String>, через который можно пропустить filter, map и count, не удерживая весь файл в памяти. Обязательный try-with-resources для потока обусловлен тем, что поток владеет открытым файловым дескриптором.
  • Строка Predicate<String> nameLine = l -> l.startsWith("name") — это тот же словарь из части 12: значение Predicate, передаваемое в Stream.filter. Files.lines — это место, где API потоков встречается с API ввода-вывода.
  • Files.deleteIfExists — это удаление без исключений: возвращает true, если файл был удалён, false, если его не было. Устаревший File.delete() возвращает boolean и для «удалено», и для «не удалось удалить» — Files различает их, бросая исключение.

Что дальше

Прежде чем современный API java.nio.file займёт остаток части, следующая глава рассматривает класс, который вы встретите первым в любой старой кодовой базе: Класс File в Java. Это устаревший тип пути и метаданных — ограниченный, но повсеместный — и понимание того, чего он не может делать, даёт основания для использования Path и Files.

Практика

Практика
Почему `java.io` разделяет `InputStream`/`OutputStream` и `Reader`/`Writer`?
Почему `java.io` разделяет `InputStream`/`OutputStream` и `Reader`/`Writer`?
Was this page helpful?