JavaRush /Курсы /Kotlin SELF /Предпроверки перед чтением и записью: существование, тип,...

Предпроверки перед чтением и записью: существование, тип, права, директории

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

1. Зачем нужны предпроверки

Когда вы пишете file.readText() или file.writeText(...), вы как будто говорите ОС: «Дай мне данные» или «Запиши вот это». И ОС может ответить: «Ок»… или «Нет, потому что…».

Предпроверки — это не попытка “отменить существование ошибок”. Это попытка раньше заметить очевидные проблемы и сказать человеку (или себе) нормальную причину, а не “что-то пошло не так”. Это особенно полезно в CLI-приложениях: пользователь любит конкретику, а не философию.

Важно сразу принять неприятную, но полезную правду: предпроверки не гарантируют успех операции. Даже если вы проверили exists() и canRead(), файл может исчезнуть через миллисекунду. Поэтому предпроверки — это первая линия обороны, а реальная операция всё равно должна быть готова к I/O-сбою (обработка исключений будет отдельно, а сегодня держим фокус именно на проверках).

Нарисуем короткую ментальную модель:

flowchart TD
    A[Хотим прочитать/записать файл] --> B[Делаем предпроверки]
    B -->|проверки не прошли| C[Не начинаем I/O, объясняем проблему]
    B -->|проверки прошли| D[Пробуем реальный I/O]
    D -->|успех| E[Работаем с данными]
    D -->|сбой| F[Это уже область обработки исключений]

2. File как путь и вопросы к файловой системе

Перед тем как проверять чтение и запись, важно привыкнуть к мысли: File("data/input.txt") — это не “файл в руках”, а объект пути, с помощью которого вы задаёте вопросы файловой системе: существует ли что-то по этому пути, папка это или файл, можно ли читать/писать и так далее.

В Kotlin/JVM вы обычно используете java.io.File. У него есть базовые методы, которые мы будем комбинировать:

  • exists() — существует ли что-то по пути
  • isFile — это файл?
  • isDirectory — это директория?
  • canRead(), canWrite() — есть ли права на чтение/запись (важное “кажется”, а не “гарантирую”)
  • parentFile — родительская директория (может быть null)
  • mkdirs() — создать директорию и всех родителей

Давайте начнём с маленькой «диагностической привычки»: прежде чем спорить с компьютером, полезно спросить у него, что он видит по этому пути.

import java.io.File

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

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

Да, это выглядит как “лишние строчки”. Но когда у вас однажды будет ситуация «почему не читается?!», эти четыре строки экономят полчаса жизни и одну микровспышку ненависти к человечеству.

Что проверять перед чтением

Когда мы читаем файл, нам нужно убедиться в трёх вещах: по пути что-то есть, это именно файл (а не папка), и у нас есть право читать.

Самый практичный минимум для чтения обычно выглядит так:

import java.io.File

fun canReadFile(file: File): Boolean {
    return file.exists() && file.isFile && file.canRead()
}

fun main() {
    val ok = canReadFile(File("data/input.txt"))
    println("canReadFile=$ok") // например: canReadFile=false
}

Обратите внимание на порядок: exists()isFilecanRead(). Он не только логичный, но и психологически приятный: мы идём от “вообще есть ли что-то” к “это правильного типа” и только потом к правам.

Почему одного exists() недостаточно

Очень частая ошибка новичка — сделать вот так:

if (file.exists()) {
    val text = file.readText()
}

Проблема в том, что exists() говорит: «да, что-то есть». Но это “что-то” может быть директорией. А чтение директории как текста — это уже игра «угадай, какую ошибку вернёт ОС сегодня».

Поэтому isFile — это не придирка, а защита от логической ошибки: “я думал это файл”.

3. canRead() и canWrite(): полезные, но не гарантия

Права доступа — тема, где легко впасть в иллюзию контроля. Методы canRead() и canWrite() действительно дают полезный сигнал: «скорее всего, читать/писать можно». Но они не являются договором с реальностью.

Почему так? Потому что между “проверил” и “сделал” может поменяться всё: файл мог быть заблокирован, права могли измениться, файл мог исчезнуть, вы могли работать в среде с сетевой файловой системой, где проверки и реальный доступ ведут себя хитрее.

С точки зрения разработки это означает простое правило поведения: canRead()/canWrite() помогают не начинать заведомо обречённую операцию, но не заменяют обработку реального сбоя ввода/вывода. Ошибки такого типа обычно “живут” под зонтиком IOException.

Потрогаем эти флаги руками:

import java.io.File

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

    println("exists=${file.exists()}")     // например: true
    println("canRead=${file.canRead()}")   // например: true
    println("canWrite=${file.canWrite()}") // например: false
}

Если canWrite() внезапно false, это не “Kotlin сломался”. Это подсказка: возможно, вы пытаетесь писать туда, где у вас нет прав (например, в системную директорию), или файл помечен read-only.

4. Предпроверки перед записью: родительская директория

Запись отличается от чтения тем, что файла может ещё не существовать — и это нормально. Поэтому набор проверок тут другой.

Ключевая мысль: чтобы записать файл "data/out/report.txt", вам в первую очередь нужна директория "data/out". И именно её состояние чаще всего ломает запись.

То есть перед writeText() нас интересует:

  • существует ли родительская папка
  • является ли она директорией (не файлом)
  • можем ли мы в неё писать
  • если папки нет — можем ли мы её создать

parentFile и почему он может быть null

Если путь относительный и без папок, например File("report.txt"), то parentFile будет null. Это не ошибка, просто “родитель не указан”.

В таком случае удобно считать, что родитель — текущая директория ".".

import java.io.File

fun parentOrCurrent(target: File): File {
    return target.parentFile ?: File(".")
}

fun main() {
    val a = File("data/out/report.txt")
    val b = File("report.txt")

    println(parentOrCurrent(a).path) // data/out
    println(parentOrCurrent(b).path) // .
}

mkdirs(): готовим директорию под запись

Теперь самое практичное: функция, которая гарантирует (насколько это возможно), что директория существует и является директорией. Если её нет — пытается создать.

Важно: mkdirs() возвращает Boolean. Это не “для галочки”. Это единственный прямой сигнал “получилось / не получилось”.

import java.io.File

fun ensureDir(dir: File): Boolean {
    if (dir.exists()) return dir.isDirectory
    return dir.mkdirs()
}

fun main() {
    val dir = File("data/out/reports")
    val ok = ensureDir(dir)

    println("dirReady=$ok")               // например: dirReady=true
    println("exists=${dir.exists()}")     // exists=true
    println("isDir=${dir.isDirectory}")   // isDir=true
}

Здесь мы сознательно не используем списки проверок “всё и сразу”, а читаем код как историю: “если есть — убедись, что папка; если нет — создай”.

mkdir() vs mkdirs() на пальцах

mkdir() создаёт одну директорию. Если вам нужно создать "data/out/reports", а "data/out" не существует, mkdir() обычно не справится.

mkdirs() пытается создать всю цепочку.

Для новичка правило простое: почти всегда для вложенных путей нужен mkdirs().

Готовность к записи через parentFile

Соберём это в ещё одну маленькую утилиту: подготовить родительскую директорию под файл.

import java.io.File

fun ensureParentDir(target: File): Boolean {
    val parent = target.parentFile ?: File(".")
    return ensureDir(parent)
}

fun main() {
    val out = File("data/out/report.txt")
    println("parentReady=${ensureParentDir(out)}") // например: parentReady=true
}

Эта функция кажется скучной (а скучное — это часто надёжное). Но именно она чаще всего превращает сценарий “не записывается, потому что нет папки” в сценарий “папка создалась, всё ок”.

Шпаргалка: что проверяем перед чтением и записью

Иногда полезно зафиксировать логику в виде таблицы, чтобы не держать всё в голове. Считайте это “шпаргалкой” на крайний случай.

Действие Что хотим гарантировать (насколько можем) Типовые проверки
Чтение По пути есть файл, и мы можем читать
exists() && isFile && canRead()
Запись Есть директория, куда можно записать файл
ensureDir(parentFile) + parent.canWrite()

Обратите внимание: для записи мы чаще проверяем директорию, а не “сам файл”. Файл может появиться только в момент записи — и это нормальный сценарий.

5. Пример: CLI-приложение с предпроверками

Представим, что у нас есть простое консольное приложение, которое хранит данные в текстовом файле "data/storage.txt" и иногда пишет отчёт в "data/out/report.txt". Неважно, что именно внутри — расходы, заметки или список “обид на компилятор”. Нам важно другое: как аккуратно проверять пути перед операцией.

Загрузка данных

import java.io.File

fun loadStorageText(storageFile: File): String? {
    if (!canReadFile(storageFile)) return null
    return storageFile.readText()
}

fun main() {
    val file = File("data/storage.txt")
    val text = loadStorageText(file)

    println(text?.length ?: "NO DATA") // например: NO DATA
}

Здесь нет обработки исключений — и это намеренно. Сегодня мы фокусируемся на предпроверках как на фильтре. Обработка реальных I/O-сбоев будет следующей лекцией дня. Но даже в этом виде вы уже получили более “умное” поведение: если файла нет — мы не лезем читать и не провоцируем падение.

Сохранение отчёта

import java.io.File

fun saveReportText(reportFile: File, report: String): Boolean {
    if (!ensureParentDir(reportFile)) return false
    reportFile.writeText(report)
    return true
}

fun main() {
    val ok = saveReportText(
        File("data/out/report.txt"),
        "Отчёт: всё хорошо\n"
    )
    println("saved=$ok") // например: saved=true
}

Если папка "data/out" не существовала — она создастся. Если создать нельзя — вернётся false, и вы можете корректно завершить сценарий (или попросить пользователя выбрать другой путь).

6. Быстрая диагностика пути

Когда начинаются I/O-проблемы, вы обычно хотите быстро понять: что именно у меня по этому пути? файл? папка? существует? читается?

Сделаем маленький “диагностический текст”, который можно печатать в консоль (и который вы потом скажете себе спасибо, что написали).

import java.io.File

fun describe(file: File): String {
    return "path=${file.path}, exists=${file.exists()}, isFile=${file.isFile}, isDir=${file.isDirectory}, canRead=${file.canRead()}, canWrite=${file.canWrite()}"
}

fun main() {
    val file = File("data/input.txt")
    println(describe(file))
    // например: path=data/input.txt, exists=false, isFile=false, isDir=false, canRead=false, canWrite=false
}

Это не “логирование” (до него ещё далеко), а просто человеческая диагностика в стиле “что видит файловая система”.

7. Типичные ошибки при предпроверках

Ошибка №1: проверять только exists() и считать, что этого достаточно.
exists() не говорит, что это файл, и не говорит, что его можно читать. Очень легко получить ситуацию “exists=true”, но “isDirectory=true”, и тогда чтение ломается. Привыкайте к формуле exists() + isFile + canRead() для чтения.

Ошибка №2: при записи проверять canWrite() у файла, который ещё не существует.
Это звучит логично (“я же буду писать в файл”), но на практике часто бессмысленно: если файла нет, то и право “писать в файл” оценивать нечего. Реально важно, можно ли писать в директорию, где файл должен появиться. Поэтому проверка начинается с parentFile.

Ошибка №3: вызывать mkdirs() и игнорировать результат.
mkdirs() может вернуть false, и это не редкость: нет прав, конфликт имён (по пути уже лежит файл), странный путь, проблемы с диском. Если вы проигнорировали результат, то позже “падение” будет выглядеть загадочнее и дальше от причины.

Ошибка №4: считать canRead()/canWrite() гарантией успеха.
Эти методы полезны как ранний фильтр, но среда может измениться между проверкой и операцией, а ещё реальные ошибки бывают сложнее (например, временная блокировка или проблемы с файловой системой). Держите в голове: предпроверки снижают число ошибок и улучшают сообщения, но не отменяют реальный риск I/O-сбоев, которые относятся к семейству IOException.

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