$ sudo teach IT
МОДУЛЬ 11 · УРОК 3

Файлы: чтение и запись

Практически любая программа так или иначе работает с файлами: читает конфигурацию, сохраняет результаты, обрабатывает данные, пишет логи. В этом уроке мы разберём современный API для работы с файлами — java.nio.file (Path и Files) — и классические потоки ввода-вывода (BufferedReader/BufferedWriter).

⏱ ~40 минут 🎓 Средний уровень ☕ Java

📂 Путь к файлу: 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 вопросов

Чтение файла

Premium