Файлы: чтение и запись
Практически любая программа так или иначе работает с файлами: читает конфигурацию, сохраняет результаты, обрабатывает данные, пишет логи. В этом уроке мы разберём современный API для работы с файлами — java.nio.file (Path и Files) — и классические потоки ввода-вывода (BufferedReader/BufferedWriter).
📂 Путь к файлу: Path
До Java 7 путь к файлу представлялся просто строкой (String) или классом java.io.File. File был ограничен: его методы могли не работать, он плохо поддерживал разные ОС, не давал полезной информации. Начиная с Java 7, появился новый API — java.nio.file.Path и java.nio.file.Files.
Path — это программное представление пути к файлу или директории. Он не обязан указывать на реально существующий файл — это просто адрес. Путь может быть абсолютным (начинается с корня диска) или относительным (относительно текущей рабочей директории).
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathExample {
public static void main(String[] args) {
// Создание Path через Paths.get()
Path absolute = Paths.get("/Users/nefor/projects/data.txt");
Path relative = Paths.get("data", "config", "settings.properties");
Path fromString = Path.of("documents/report.pdf"); // Java 11+
System.out.println("Абсолютный: " + absolute);
System.out.println("Относительный: " + relative);
// Компоненты пути
System.out.println("Корень: " + absolute.getRoot()); // /
System.out.println("Родитель: " + absolute.getParent()); // /Users/nefor/projects
System.out.println("Имя файла: " + absolute.getFileName()); // data.txt
System.out.println("Расширение? (нет прямого метода)");
// Разбор на элементы
for (int i = 0; i < absolute.getNameCount(); i++) {
System.out.println(" [" + i + "]: " + absolute.getName(i));
}
// Нормализация — убирает . и ..
Path messy = Paths.get("/home/./user/../user/docs/./file.txt");
Path clean = messy.normalize();
System.out.println("Грязный: " + messy);
System.out.println("Чистый: " + clean);
// Конвертация
Path absolutePath = relative.toAbsolutePath();
System.out.println("Абсолютный из относительного: " + absolutePath);
// toFile() — для совместимости с File
java.io.File file = absolute.toFile();
System.out.println("File: " + file);
}
}
💡 Аналогия: Path — это как URL или адрес в файловой системе. Вы можете написать на бумажке адрес дома (Path), но это не значит, что дом существует. Чтобы проверить, есть ли дом по адресу, нужно пойти по адресу — этим занимается класс Files.
🛠️ Класс Files — основные операции
java.nio.file.Files — это утилитный класс со статическими методами для всех операций с файлами: создание, удаление, копирование, перемещение, чтение, запись. Он заменяет большинство методов java.io.File.
import java.nio.file.*;
import java.io.IOException;
public class FilesBasicOperations {
public static void main(String[] args) {
Path dir = Paths.get("test_dir");
Path file = dir.resolve("example.txt"); // test_dir/example.txt
try {
// Создание директории
if (!Files.exists(dir)) {
Files.createDirectory(dir);
System.out.println("Директория создана: " + dir);
}
// Создание файла
if (!Files.exists(file)) {
Files.createFile(file);
System.out.println("Файл создан: " + file);
}
// Проверка атрибутов
System.out.println("Существует: " + Files.exists(file));
System.out.println("Это файл: " + Files.isRegularFile(file));
System.out.println("Это директория: " + Files.isDirectory(file));
System.out.println("Читаемый: " + Files.isReadable(file));
System.out.println("Пишемый: " + Files.isWritable(file));
System.out.println("Исполняемый: " + Files.isExecutable(file));
System.out.println("Размер: " + Files.size(file) + " байт");
// Время последнего изменения
System.out.println("Последнее изменение: " +
Files.getLastModifiedTime(file));
// Копирование файла
Path copy = dir.resolve("copy.txt");
Files.copy(file, copy, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Скопирован в: " + copy);
// Перемещение (переименование)
Path renamed = dir.resolve("renamed.txt");
Files.move(copy, renamed, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Перемещён в: " + renamed);
// Удаление
Files.delete(renamed);
System.out.println("Удалён: " + renamed);
// Удаление с проверкой существования
boolean deleted = Files.deleteIfExists(Paths.get("не_существует.txt"));
System.out.println("Файл удалён? " + deleted); // false
// Очистка
Files.deleteIfExists(file);
Files.deleteIfExists(dir);
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
⚠️ IOException — ваш новый спутник: Почти все операции с файлами могут выбросить IOException. Файл может быть занят, диск может быть полон, путь может быть неверным, разрешения могут отсутствовать. Всегда обрабатывайте IOException (через try-catch или throws). Никогда не игнорируйте исключения!
📖 Чтение файлов: три подхода
В Java есть несколько способов прочитать файл. Выбор зависит от размера файла и нужного формата.
📌 Способ 1: Files.readString() — весь файл в строку (Java 11+)
Самый простой способ для небольших файлов (до нескольких мегабайт). Прочитает весь файл в строку и закроет все ресурсы автоматически.
import java.nio.file.*;
import java.io.IOException;
public class ReadStringExample {
public static void main(String[] args) {
try {
// Чтение всего файла в строку (Java 11+)
String content = Files.readString(Path.of("example.txt"));
System.out.println("Содержимое файла:");
System.out.println(content);
// Чтение с указанием кодировки
String utf8Content = Files.readString(
Path.of("example.txt"),
java.nio.charset.StandardCharsets.UTF_8
);
} catch (IOException e) {
System.err.println("Ошибка чтения: " + e.getMessage());
}
}
}
📌 Способ 2: Files.readAllLines() — список строк
Читает файл и возвращает List<String>, где каждый элемент — одна строка файла. Удобно для построчной обработки.
import java.nio.file.*;
import java.util.List;
import java.io.IOException;
public class ReadAllLinesExample {
public static void main(String[] args) {
try {
// Чтение всех строк в список
List<String> lines = Files.readAllLines(
Path.of("data.csv"),
java.nio.charset.StandardCharsets.UTF_8
);
System.out.println("Всего строк: " + lines.size());
// Обработка строк
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
System.out.printf("Строка %d: %s%n", i + 1, line);
}
// Пропуск заголовка
List<String> data = lines.subList(1, lines.size());
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
📌 Способ 3: BufferedReader — для больших файлов
Если файл большой (сотни мегабайт или гигабайты), читать его целиком в память нельзя — не хватит памяти. Используйте BufferedReader с построчным чтением. BufferedReader читает данные из файла порциями (буфером), что эффективнее, чем читать по одному символу.
import java.io.*;
import java.nio.file.*;
public class BufferedReaderExample {
public static void main(String[] args) {
Path path = Path.of("large_file.log");
// try-with-resources — автоматически закрывает файл
try (BufferedReader reader = Files.newBufferedReader(
path, java.nio.charset.StandardCharsets.UTF_8)) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
// Обрабатываем каждую строку по отдельности
if (line.contains("ERROR")) {
System.out.printf("Строка %d: %s%n", lineNumber, line);
}
}
System.out.println("Всего обработано строк: " + lineNumber);
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
🔑 try-with-resources: Это конструкция, которая автоматически закрывает ресурсы, реализующие AutoCloseable (BufferedReader, BufferedWriter, FileInputStream и т.д.). Закрытие происходит в любом случае — и при успехе, и при исключении. Раньше нужно было писать finally с close(), теперь это автоматизировано. Всегда используйте try-with-resources!
📌 Способ 4: Files.lines() — Stream строк (Java 8+)
Современный способ — получить Stream<String> строк. Позволяет использовать Stream API для обработки. Важно: Stream нужно закрывать (через try-with-resources или самостоятельно).
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class StreamLinesExample {
public static void main(String[] args) {
Path path = Path.of("server.log");
// try-with-resources с Stream (Stream реализует AutoCloseable)
try (Stream<String> lines = Files.lines(path)) {
// Считаем ошибки
long errorCount = lines
.filter(line -> line.contains("ERROR"))
.count();
System.out.println("Найдено ошибок: " + errorCount);
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
// Другой пример: собираем уникальные IP-адреса
try (Stream<String> lines = Files.lines(path)) {
lines
.filter(line -> line.contains("ERROR"))
.map(line -> line.split(" ")[0]) // IP в начале строки
.distinct()
.forEach(System.out::println);
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
✏️ Запись в файлы
Для записи в файлы есть симметричные методы.
📌 Files.writeString() — простая запись (Java 11+)
import java.nio.file.*;
import java.io.IOException;
public class WriteStringExample {
public static void main(String[] args) {
try {
// Запись строки в файл (создаст или перезапишет файл)
Files.writeString(Path.of("hello.txt"), "Привет, мир!");
// Запись с указанием кодировки и опций
Files.writeString(
Path.of("log.txt"),
"Новая запись в лог\n",
StandardOpenOption.CREATE,
StandardOpenOption.APPEND // Добавить в конец, а не перезаписать
);
System.out.println("Файл записан успешно");
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
💡 StandardOpenOption: CREATE — создать файл, если не существует. APPEND — добавлять в конец (вместо перезаписи). TRUNCATE_EXISTING — очистить файл перед записью. WRITE — открыть для записи. По умолчанию: CREATE, TRUNCATE_EXISTING, WRITE (создать или перезаписать).
📌 Files.write() — запись списка строк
import java.nio.file.*;
import java.util.List;
import java.io.IOException;
public class WriteListExample {
public static void main(String[] args) {
List<String> lines = List.of(
"Имя,Возраст,Город",
"Иван,30,Москва",
"Мария,25,Санкт-Петербург",
"Алексей,35,Казань"
);
try {
// Запись списка строк (каждая строка — отдельная линия в файле)
Files.write(
Path.of("users.csv"),
lines,
java.nio.charset.StandardCharsets.UTF_8
);
System.out.println("CSV файл создан");
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
📌 BufferedWriter — для больших объёмов
import java.io.*;
import java.nio.file.*;
public class BufferedWriterExample {
public static void main(String[] args) {
Path path = Path.of("report.txt");
try (BufferedWriter writer = Files.newBufferedWriter(
path, java.nio.charset.StandardCharsets.UTF_8)) {
writer.write("=== Отчёт ===\n");
writer.newLine();
for (int i = 1; i <= 1000; i++) {
writer.write("Строка " + i);
writer.newLine();
}
System.out.println("Отчёт записан: " + path.toAbsolutePath());
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
📁 Работа с директориями и обход файлов
📌 Создание и удаление директорий
import java.nio.file.*;
import java.io.IOException;
public class DirectoryOperations {
public static void main(String[] args) {
try {
// Создание одной директории
Files.createDirectory(Path.of("new_folder"));
// Создание нескольких вложенных директорий сразу
Files.createDirectories(Path.of("a/b/c/d/e"));
// Временная директория
Path tmp = Files.createTempDirectory("myapp_");
System.out.println("Временная: " + tmp);
// Создание временного файла
Path tmpFile = Files.createTempFile("data", ".txt");
System.out.println("Временный файл: " + tmpFile);
// Листинг файлов
Path dir = Path.of(".");
try (var stream = Files.list(dir)) {
System.out.println("Файлы в текущей директории:");
stream.forEach(System.out::println);
}
// Обход рекурсивно (Files.walk)
try (var walk = Files.walk(Path.of("src"), 3)) { // глубина 3
walk.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// Удаление — только пустые директории
Files.deleteIfExists(Path.of("new_folder"));
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
📌 Files.walkFileTree — мощный обход
Files.walkFileTree позволяет выполнять действия при входе в директорию, при выходе из неё и при обнаружении каждого файла. Используется для копирования дерева, поиска, удаления.
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.io.IOException;
public class FileTreeWalkExample {
public static void main(String[] args) {
Path startDir = Paths.get("src");
try {
Files.walkFileTree(startDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Файл: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Вход в: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Не удалось прочитать: " + file);
return FileVisitResult.CONTINUE; // продолжаем обход
}
});
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
🔌 Классические I/O потоки
До появления java.nio.file (Java 7) и Files.readString (Java 11) основным способом работы с файлами были потоки ввода-вывода: FileInputStream, FileOutputStream, FileReader, FileWriter и их буферизованные версии. Вы будете встречать их в старом коде.
import java.io.*;
public class ClassicIOExample {
public static void main(String[] args) {
// Чтение через FileReader + BufferedReader
try (BufferedReader reader = new BufferedReader(
new FileReader("input.txt", java.nio.charset.StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
// Запись через FileWriter + BufferedWriter
try (BufferedWriter writer = new BufferedWriter(
new FileWriter("output.txt", java.nio.charset.StandardCharsets.UTF_8))) {
writer.write("Привет из классического IO!");
writer.newLine();
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
// Чтение байт через InputStream
try (InputStream is = new FileInputStream("image.jpg")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
// Обработка буфера байт
System.out.println("Прочитано байт: " + bytesRead);
}
} catch (IOException e) {
System.err.println("Ошибка: " + e.getMessage());
}
}
}
💡 Новые методы vs классические: Для текстовых файлов используйте Files.readString() и Files.writeString() — это проще. Для бинарных файлов — Files.readAllBytes() и Files.write(). BufferedReader/BufferedWriter нужны для потоковой обработки больших файлов. Классические FileInputStream/FileOutputStream — для низкоуровневой работы с байтами.
🏋️ Практические примеры
📌 Пример 1: Простой логгер в файл
import java.io.*;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FileLogger {
private static final DateTimeFormatter DT_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void log(String level, String message) {
try {
String timestamp = LocalDateTime.now().format(DT_FMT);
String logLine = String.format("[%s] %s: %s%n", timestamp, level, message);
Files.writeString(
Path.of("app.log"),
logLine,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND
);
} catch (IOException e) {
System.err.println("Не удалось записать лог: " + e.getMessage());
}
}
public static void main(String[] args) {
log("INFO", "Приложение запущено");
log("DEBUG", "Загружено 15 конфигураций");
log("WARN", "Низкий запас памяти: 256 MB");
log("ERROR", "Ошибка подключения к БД");
}
}
📌 Пример 2: Копирование файла с прогрессом
import java.io.*;
import java.nio.file.*;
public class FileCopyProgress {
public static void copyWithProgress(Path source, Path target) throws IOException {
long totalBytes = Files.size(source);
long copiedBytes = 0;
try (InputStream in = new BufferedInputStream(
Files.newInputStream(source));
OutputStream out = new BufferedOutputStream(
Files.newOutputStream(target))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
copiedBytes += bytesRead;
int percent = (int) (copiedBytes * 100 / totalBytes);
System.out.printf("\rПрогресс: %d%%", percent);
}
System.out.println("\nКопирование завершено!");
}
}
public static void main(String[] args) throws IOException {
copyWithProgress(
Path.of("big_file.zip"),
Path.of("big_file_copy.zip")
);
}
}
📌 Пример 3: Парсер CSV
import java.nio.file.*;
import java.util.*;
import java.io.IOException;
public class CsvParser {
public static List<Map<String, String>> parseCsv(Path file) throws IOException {
List<String> lines = Files.readAllLines(file);
if (lines.isEmpty()) return List.of();
// Заголовок
String[] headers = lines.get(0).split(",");
List<Map<String, String>> result = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
String[] values = lines.get(i).split(",");
Map<String, String> row = new HashMap<>();
for (int j = 0; j < headers.length && j < values.length; j++) {
row.put(headers[j].trim(), values[j].trim());
}
result.add(row);
}
return result;
}
public static void main(String[] args) throws IOException {
Path csvFile = Path.of("users.csv");
Files.writeString(csvFile, "name,age,city\nИван,30,Москва\nМария,25,СПб\n");
List<Map<String, String>> data = parseCsv(csvFile);
for (Map<String, String> row : data) {
System.out.println(row.get("name") + " из " + row.get("city"));
}
Files.deleteIfExists(csvFile);
}
}
✅ Итоги урока
- Path — представление пути к файлу/директории (не обязательно существующего). Методы: getFileName, getParent, getRoot, normalize, toAbsolutePath, resolve
- Files — утилитный класс для всех файловых операций. Основные: exists, createFile, createDirectory, delete, copy, move, size, isRegularFile, isDirectory, getLastModifiedTime
- Files.readString() (Java 11+) — весь файл в строку. Для небольших файлов
- Files.readAllLines() — список строк. Удобно для CSV и построчной обработки
- BufferedReader — для больших файлов. Построчное чтение без загрузки всего файла в память
- Files.lines() — Stream строк. Для современной функциональной обработки
- Files.writeString() и Files.write() — запись строки или списка строк в файл
- try-with-resources — автоматическое закрытие файлов. Всегда используйте с ресурсами
- StandardOpenOption: CREATE, APPEND, TRUNCATE_EXISTING, WRITE
- Всегда обрабатывайте IOException — работа с файлами непредсказуема
В следующем уроке разберём форматирование и интернационализацию — как правильно выводить числа, даты и сообщения в разных локалях. 🚀
Тест: Файлы: чтение и запись (java.nio.file.Path и Files)
10 вопросов