JavaRush /Курсы /Kotlin SELF /File и

File и Path: проверки, директории, сборка пути из частей

Kotlin SELF
42 уровень , 1 лекция
Открыта

1. Зачем нужны File и Path, если путь — строка

Когда вы впервые видите работу с файлами, возникает соблазн: «Ну путь же можно хранить в String, значит, дальше просто readText("data/input.txt") и погнали». На практике строка — это слишком «тупая» сущность: в ней легко ошибиться, её неудобно диагностировать, и она не умеет отвечать на вопросы вроде «а это файл или папка?». Поэтому в Kotlin/JVM мы обычно используем объекты File и Path, которые оборачивают путь и дают полезные операции. Kotlin отлично дружит с Java-библиотеками (это часть идеи «interoperable with Java»), поэтому java.io.File — нормальный, привычный инструмент.

Сразу важный психологический момент: File("data/input.txt") не создаёт файл. Это просто объект, который хранит путь и умеет по этому пути делать проверки/операции.

Мини-демонстрация «объект пути»:

import java.io.File

fun main() {
    val f = File("data/input.txt")

    println("path = ${f.path}")                 // path = data/input.txt
    println("absolutePath = ${f.absolutePath}") // absolutePath = .../data/input.txt
}

Обратите внимание: мы пока ничего не читаем и не пишем. Мы просто делаем то, что очень помогает при отладке: смотрим, куда именно программа «целится».

Небольшая схема, как обычно выглядит путь в голове и в реальности:

flowchart TD
    A["Вы пишете: File('data/input.txt')"] --> B["ОС/Java считает путь относительным"]
    B --> C["Относительный путь считается от рабочей директории"]
    C --> D["Получается абсолютный путь"]
    D --> E["Уже по нему делаем проверки exists/isFile/isDirectory"]

2. File: три вопроса перед I/O

Когда вы начинаете работать с файлами по-настоящему, главная проблема не в том, «как прочитать текст», а в том, что вы читаете не то, не оттуда, или пытаетесь читать папку как файл. Поэтому дисциплина на старте такая: перед любым чтением/записью мы задаём три вопроса объекту File.

Первый вопрос — exists(): «по этому пути вообще что-то есть?». Второй вопрос — isFile: «это обычный файл?». Третий — isDirectory: «это папка?». Эти проверки не делают код «длинным ради длины»; они делают его предсказуемым: вместо загадочного падения «где-то внутри чтения» вы получаете понятную диагностику.

Вот небольшой пример, который можно запускать в любой момент, чтобы понять, что реально находится по пути:

import java.io.File

fun main() {
    val f = File("data")

    println("path=${f.path}")               // path=data
    println("exists=${f.exists()}")         // например: exists=false
    println("isFile=${f.isFile}")           // isFile=false
    println("isDirectory=${f.isDirectory}") // isDirectory=false
}

Полезно понимать, что возможны такие ситуации:

exists()
isFile
isDirectory
Что это значит на практике
false
false
false
По пути ничего нет (ни файла, ни папки)
true
true
false
Это файл (можно читать/писать как файл)
true
false
true
Это директория (нельзя читать как файл)

Ситуация «exists() == true, но и isFile == false, и isDirectory == false» встречается редко (обычно это что-то специфическое на уровне файловой системы), но для нашего уровня достаточно помнить: если хотите читать текст — ожидайте isFile == true.

3. Подготовка директорий: parentFile, mkdir() и mkdirs()

Почти каждый новичок хотя бы раз наступает на грабли: «Я хочу записать файл data/reports/report.txt — почему программа ругается?». Причина банальна: файл-то вы хотите создать, но папки data/reports ещё не существует. Сама по себе запись файла обычно не обязана создавать цепочку каталогов — это ваша ответственность.

Чтобы это делалось аккуратно, мы знакомимся с двумя идеями. Во-первых, у файла есть «родительская папка» — parentFile. Во-вторых, папки можно создавать mkdir() или mkdirs(). Разница простая: mkdir() пытается создать ровно одну директорию, а mkdirs() создаёт цепочку, если нужно (и это чаще то, что нам надо в приложениях).

Мини-пример «готовим родительскую папку»:

import java.io.File

fun main() {
    val out = File("data/reports/report.txt")
    val parent = out.parentFile

    val ok = parent != null && (parent.exists() || parent.mkdirs())
    println("parentPath=${parent?.path}, ready=$ok") // parentPath=data/reports, ready=true/false
}

Здесь важно, что parentFile может быть null. Это случается, когда вы создаёте файл «без папки», например File("report.txt"). Тогда «родителя» как отдельного объекта нет, и это нормально.

Чуть более «прикладной» вариант — маленькая утилита (мы позже будем собирать такие вещи в аккуратный набор функций):

import java.io.File

fun ensureParentDirExists(file: File): Boolean {
    val parent = file.parentFile ?: return true
    return parent.exists() || parent.mkdirs()
}

fun main() {
    val out = File("data/reports/report.txt")
    println(ensureParentDirExists(out)) // true/false
}

Схема того, что делает эта функция (чтобы мозг не пытался каждый раз заново компилировать реальность):

flowchart TD
    A["Есть File для будущей записи"] --> B["Берём parentFile"]
    B --> C{parentFile == null?}
    C -->|Да| D["ОК: писать будем рядом, папка не нужна"]
    C -->|Нет| E{"parent.exists()?"}
    E -->|Да| F["ОК: папка уже есть"]
    E -->|Нет| G["mkdirs()"]
    G --> H["ОК/не ОК: зависит от результата mkdirs()"]

4. Сборка пути из частей без склейки слэшей

Строковая конкатенация путей выглядит как самый быстрый путь к счастью: "data" + "/" + "input.txt". Но в реальности это «самый быстрый путь к слезам»: где-то забыли /, где-то добавили два, где-то у вас Windows, и вы начинаете думать про \ (а потом ещё и про экранирование "\\\\", и вечер перестаёт быть томным).

В Java/Kotlin есть простой и человеческий выход: собирать путь из частей через готовые API. Это читаемо и снижает риск «кривых слэшей».

Сборка через File(parent, child)

Плохой пример (не запрещён, но подозрителен как шаурма в 4 утра):

fun main() {
    val path = "data" + "/" + "reports" + "/" + "report.txt"
    println(path) // data/reports/report.txt
}

Хороший пример (его проще читать, и он меньше зависит от ручных слэшей):

import java.io.File

fun main() {
    val base = File("data")
    val reportsDir = File(base, "reports")
    val reportFile = File(reportsDir, "report.txt")

    println(reportFile.path) // data/reports/report.txt
}

Здесь нам особенно приятно то, что переменные начинают «говорить»: reportsDir — явно папка, reportFile — явно файл. Это не магия, но это сильно помогает мозгу не путаться.

Небольшая таблица, чтобы зафиксировать «как принято»:

Подход Плюсы Минусы
Склейка строк Быстро написать Легко ошибиться со слэшами, хуже читается
File(parent, child)
Читаемо, меньше ошибок Нужно создавать несколько объектов (но это обычно ок)

Сборка через Path.of(...)

File — это классический инструмент, он популярный, но очень старый: он пришёл еще из java.io. Более современный подход в мире JVM — java.nio.file.Path. На нашем уровне важна простая мысль: Path — это тоже объект пути, который удобно собирать из частей, и он хорошо сочетается с современными API.

Почему Path бывает приятнее? Он прямо заточен под «путь как значение»: его удобно составлять, передавать, сравнивать, преобразовывать. А если вам всё же понадобятся операции «как у File», вы всегда можете конвертировать Path в File.

Мини-пример сборки пути через Path.of(...):

import java.nio.file.Path

fun main() {
    val p = Path.of("data", "reports", "report.txt")
    println(p.toString()) // data/reports/report.txt
}

А вот пример «стыковки миров» — иногда вы хотите собрать путь через Path, а потом использовать File-методы (или наоборот):

import java.io.File
import java.nio.file.Path

fun main() {
    val p: Path = Path.of("data", "input.txt")
    val f: File = p.toFile()

    println(f.path)         // data/input.txt
    println(f.absolutePath) // .../data/input.txt
}

И обратное преобразование тоже есть:

import java.io.File

fun main() {
    val f = File("data/input.txt")
    val p = f.toPath()

    println(p.toString()) // data/input.txt
}

Заметьте, что мы не обсуждаем здесь «чтение/запись через NIO» и прочие радости — сегодня наша цель проще: научиться уверенно создавать пути, проверять, что по ним находится, и готовить директории под будущие операции.

5. Мини‑рефакторинг: централизуем пути хранения

Когда приложение маленькое, можно прямо в main() написать File("data/input.txt") и быть довольным. Но как только у вас появляется два-три файла (например, «входные данные», «лог», «отчёт»), начинается хаос: пути разбросаны по коду, вы меняете папку data на app-data и пропускаете одно место, а потом полчаса ищете, почему «иногда работает, иногда нет».

Поэтому полезная привычка — централизовать построение путей. Мы пока не читаем и не пишем, но уже сейчас можем сделать «карту файлов» для нашего консольного приложения (условно назовём его трекером расходов, который раньше жил в памяти, а теперь готовится к сохранению в файлы).

Начнём с простого data class, который хранит наши ключевые пути:

import java.io.File

data class StoragePaths(
    val baseDir: File,
    val expensesFile: File,
    val reportFile: File
)

Теперь функция, которая строит эти пути из частей (без строковой склейки):

import java.io.File

fun buildStoragePaths(): StoragePaths {
    val baseDir = File("data")
    val expensesFile = File(baseDir, "expenses.txt")
    val reportFile = File(File(baseDir, "reports"), "last-report.txt")

    return StoragePaths(baseDir, expensesFile, reportFile)
}

И маленькая диагностика (она особенно полезна, когда студенты запускают проект «то из IDE, то из терминала»):

import java.io.File

fun main() {
    val paths = buildStoragePaths()

    println("workDir = ${File(".").absolutePath}")               // workDir = ...
    println("expenses = ${paths.expensesFile.absolutePath}")     // expenses = .../data/expenses.txt
    println("report = ${paths.reportFile.absolutePath}")         // report = .../data/reports/last-report.txt
}

Теперь — очень важный шаг: раз у нас появился файл отчёта в подпапке data/reports, мы должны заранее уметь подготовить папку. Мы уже написали ensureParentDirExists, используем её:

import java.io.File

fun main() {
    val paths = buildStoragePaths()

    val ok = ensureParentDirExists(paths.reportFile)
    println("reportParentReady=$ok") // reportParentReady=true/false
}

Пока мы не делаем writeText и не читаем readLines — это будет в следующих лекциях. Но именно этот рефакторинг делает будущие операции надёжнее: вы сначала уверены, что «место для файла» корректное, и только потом переходите к чтению/записи.

6. Типичные ошибки при работе с File и Path

Переход от «просто строк» к «объектам пути» обычно решает половину проблем, но вторую половину проблем новички радостно создают сами — просто потому что файловая система не обязана угадывать ваши намерения. Сейчас пройдёмся по самым частым ошибкам, чтобы вы узнавали их по звуку, как опытный механик узнаёт стук в двигателе.

Ошибка №1: думать, что File("x.txt") создаёт файл.
File создаёт объект пути, а не файл на диске. Это как написать адрес на конверте: конверт ещё не отправлен, дом ещё не построен, и почтальон пока вообще не в курсе. Реальные «действия» начинаются только при чтении/записи или явном создании.

Ошибка №2: проверять только exists() и не проверять isFile/isDirectory.
exists() отвечает на вопрос «что-то есть?», но не на вопрос «что именно?». Если по пути находится директория, а вы попробуете читать её как файл, получите ошибку уже в момент чтения. Гораздо спокойнее заранее договориться с реальностью: «я ожидаю файл» → значит, хочу exists() && isFile.

Ошибка №3: использовать mkdir() для вложенных папок и удивляться false.
mkdir() создаёт одну директорию и не обязан создавать родителей. Если вы хотите data/reports, а папки data ещё нет, mkdir() для reports не сможет «проложить путь». Для цепочки почти всегда нужен mkdirs().

Ошибка №4: не учитывать, что parentFile может быть null.
Если вы работаете с File("report.txt"), то «родитель» в виде отдельного объекта может быть отсутствующим. Это не баг. Это просто означает: файл лежит прямо в рабочей директории. В таких случаях «готовить папку» не требуется, и ваша утилита должна это корректно обрабатывать.

Ошибка №5: путать «папка проекта» и «рабочая директория запуска».
Очень хочется думать, что относительный путь считается «от папки с исходниками». Но процесс считает относительный путь от рабочей директории, которая зависит от того, как вы запускаете программу. Поэтому при странностях полезно печатать File(".").absolutePath и смотреть на ситуацию трезво.

Ошибка №6: склеивать путь строками и ловить мелкие ошибки со слэшами.
Если у вас в одном месте data/, в другом reports, а в третьем /report.txt, вы легко получите data//reports//report.txt или datareportsreport.txt. Сборка через File(parent, child) или Path.of(...) убирает целый класс этих ошибок.

Ошибка №7: размазывать пути по всему коду.
Сегодня вы используете "data/expenses.txt" в одном месте, завтра добавляете "data/reports/last-report.txt" в другом, послезавтра переименовываете папку — и вот у вас три разных «истины» о том, где хранятся данные. Куда проще завести одно место (функцию или data class), которое строит все пути, и дальше использовать только его. Это выглядит чуть «строже», но экономит много времени и нервов.

1
Задача
Kotlin SELF, 42 уровень, 1 лекция
Недоступна
Три координаты
Три координаты
1
Задача
Kotlin SELF, 42 уровень, 1 лекция
Недоступна
Проверка тропы
Проверка тропы
1
Задача
Kotlin SELF, 42 уровень, 1 лекция
Недоступна
Два навигатора
Два навигатора
1
Задача
Kotlin SELF, 42 уровень, 1 лекция
Недоступна
Подготовка папки
Подготовка папки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ