JavaRush /Курсы /Kotlin SELF /Абсолютные и относительные пути

Абсолютные и относительные пути

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

1. Введение

Когда мы говорим «прочитать файл», мы на самом деле говорим: «операционной системе нужно объяснить, где именно лежит этот файл». Для этого и существует путь (path) — строка (или объект), описывающая адрес файла/папки в файловой системе. На практике путь часто выглядит как "data/input.txt" или "C:\\Projects\\App\\data\\input.txt" — и именно здесь начинаются приключения.

Проблема в том, что одна и та же строка пути может означать разные места на диске в зависимости от того, откуда запущена программа. Поэтому тема путей — это не занудство, а способ сэкономить себе пару часов жизни и один нервный тик. Потому что ошибка «файл не найден» обычно означает не то, что файла нет, а то, что программа смотрит не туда.

Абсолютный путь

Абсолютный путь — это путь, который полностью описывает местоположение файла, начиная от «корня» файловой системы. Он не зависит от того, где вы находитесь и откуда запустили программу. Если абсолютный путь корректный, то файл либо существует в этой точке, либо нет — без «магии» и «ну у меня на компьютере работало».

На Windows абсолютный путь обычно начинается с буквы диска (например, C:\...), а на macOS/Linux — с /.... Но нам важнее идея: абсолютный путь — это адрес с городом, улицей, домом и квартирой, а не «примерно где-то рядом с домом».

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

import java.io.File

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

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

В этом примере файл может даже не существовать — и это нормально. File("...") здесь не «открывает файл», а просто хранит путь и умеет его показывать в разных формах.

Относительный путь

Относительный путь — это путь, который не является полным адресом, а считается «откуда-то». И вот это «откуда-то» — ключ к загадке.

Например, путь "data/input.txt" означает: «найди папку data в текущей рабочей директории процесса, а в ней файл input.txt».

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

Относительные пути очень удобны, потому что делают проект переносимым: вы можете хранить данные рядом с проектом, коммитить структуру папок, не зашивать C:\Users\... в код. Но с ними нужно дружить правильно: всегда помнить, какая рабочая директория сейчас.

2. Рабочая директория и поиск файла

Рабочая директория

Рабочая директория — это папка, относительно которой интерпретируются относительные пути. И это одна из самых частых причин, почему в IDE всё работает, а при запуске из терминала — «файл не найден», или наоборот.

Проверка «где мы сейчас» делается максимально просто:

import java.io.File

fun main() {
    val wd = File(".").absolutePath
    println("Working dir = $wd") // Working dir = ... (абсолютный путь текущей папки)
}

File(".") означает «текущая папка». А absolutePath — это способ увидеть, что именно JVM считает текущей рабочей директорией в этом запуске.

Почему это так важно? Потому что вы можете честно написать File("data/input.txt"), быть уверенным, что папка data есть, но программа всё равно скажет «не найдено» — если она запустилась из другой директории и ищет data не там, где вы ожидали.

Почему в IDE и в терминале может быть по-разному

Есть классический сюжет.

Вы делаете проект, структура такая:

MyApp/
  src/
  data/
    input.txt

Вы запускаете из IDE — и IDE может поставить рабочую директорию как MyApp/. Относительный путь "data/input.txt" резолвится как MyApp/data/input.txt — всё хорошо.

Потом вы запускаете иначе: например, из терминала не из MyApp/, а из родительской папки или вообще из другой директории. Тогда "data/input.txt" резолвится как другая_папка/data/input.txt — и файла там, естественно, нет.

Та же логика встречается не только при запуске программы, но и при любых инструментах, которые читают файлы по относительным путям: относительные пути почти всегда считаются именно от текущей рабочей директории процесса.

Мораль простая: относительный путь — это не «путь внутри проекта», а «путь от рабочей директории запуска».

Диагностика «файл не найден»

Когда вы видите FileNotFoundException или просто понимаете, что программа «не видит файл», первая реакция новичка — «почему Kotlin не может найти файл, он же вот лежит!». Реакция чуть более опытного человека — «где программа ищет файл?».

И вот золотой мини-шаблон диагностики:

import java.io.File

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

    println("Working dir = ${File(".").absolutePath}")
    println("Trying file = ${f.absolutePath}")
}

Смысл в том, что вы получаете две важные подсказки:

  • Working dir — точка отсчёта относительных путей
  • Trying file — абсолютный путь, куда реально смотрит программа

После этого «магия» исчезает: вы буквально видите адрес, по которому программа ищет файл. И дальше выясняется, что вы ожидали .../MyApp/data/input.txt, а получился .../MyApp/src/data/input.txt или .../SomeOtherFolder/data/input.txt.

Мини-схема: как относительный путь становится абсолютным

Слова словами, но полезно один раз увидеть это как процесс. Условно:

flowchart TD
    A["Относительный путь: data/input.txt"] --> B["Рабочая директория процесса: WD"]
    B --> C["Склейка: WD + data/input.txt"]
    C --> D["Абсолютный путь: /.../WD/data/input.txt"]

Эта схема не означает «простое сложение строк» на уровне ОС, но смысл для нас именно такой: относительный путь становится абсолютным через рабочую директорию.

3. File как объект пути и сборка путей

File(path) — это объект пути

Слово File сбивает с толку: кажется, что File("data/input.txt") «открывает файл». На самом деле в Java/Kotlin File — это в первую очередь объект пути. Он хранит строку пути и умеет:

  • выдавать path (как вы задали),
  • выдавать absolutePath (как ОС увидит этот путь),
  • позже — проверять существование, читать, писать и так далее.

То есть File — это как объект «Адрес», а не как «Содержимое посылки». Вы можете создать File("data/input.txt"), даже если файл не существует: это просто «адрес, который вы хотите использовать».

Как не склеивать пути строками

Когда новичок впервые начинает работать с файлами, возникает сильное желание делать так:

val path = "data/" + "input.txt"

И это ещё относительно безобидно. Потом начинается:

val path = "data" + "/" + "input.txt"

А потом внезапно Windows, обратные слэши, двойные слэши, пробелы, и всё превращается в археологические раскопки.

Даже в рамках этой лекции (ещё до Path.of(...), который будет дальше) можно сделать гораздо аккуратнее: собрать путь через File(parent, child). Это не «волшебная» функция, а просто более безопасный способ склеить части.

import java.io.File

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

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

Так код читается как «файл input.txt внутри директории data», а не как «строка, собранная шаманством».

4. Встраиваем пути в приложение и делаем вывод полезным

«Где лежат данные проекта»

Чтобы примеры не были абстрактными, представим наше практическое CLI-приложение: допустим, это небольшой трекер расходов. До файлов он жил в памяти, а сейчас мы хотим хранить данные в папке data/.

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

import java.io.File

private fun dataFile(): File {
    return File("data/expenses.txt")
}

fun main() {
    val f = dataFile()

    println("Working dir = ${File(".").absolutePath}")
    println("Data file (relative) = ${f.path}")         // Data file (relative) = data/expenses.txt
    println("Data file (absolute) = ${f.absolutePath}") // Data file (absolute) = .../data/expenses.txt
}

Здесь мы сделали сразу две хорошие вещи.

Во‑первых, путь к файлу теперь в одном месте, и позже (когда начнём реально читать/писать) мы не будем ловить ситуацию «в одном месте data/expenses.txt, а в другом ./data/expenses.txt, а в третьем вообще data\expenses.txt».

Во‑вторых, вывод Working dir и абсолютного пути к файлу — это встроенная диагностика. В момент, когда вы получите «файл не найден», вы уже будете видеть, куда программа смотрит.

Печатаем путь так, чтобы его можно было проверить руками

Когда вы печатаете пути, полезно печатать их так, чтобы человек мог буквально скопировать и вставить, открыть в проводнике/терминале и проверить. То есть лучше печатать абсолютный путь и рядом — пояснение.

import java.io.File

fun main() {
    val reportFile = File("data/reports/today.txt")

    println("I will look for report at:")
    println(reportFile.absolutePath)
    // .../data/reports/today.txt
}

Да, это выглядит слишком просто. Но это тот самый случай, когда простота — не признак слабости, а признак того, что вы не хотите дебажить «невидимый» путь.

5. Типичные ошибки при работе с путями

Ошибка №1: считать, что относительный путь считается от папки с кодом.
Очень логичное предположение, потому что в голове проект = папка с src. Но JVM так не думает. Относительный путь считается от рабочей директории запуска, которая зависит от IDE/терминала/настроек Run Configuration. Лучшее лекарство — печатать File(".").absolutePath при проблемах.

Ошибка №2: путать «путь внутри проекта» и «путь при запуске».
Папка data/ может лежать рядом с src/, но если рабочая директория указывает на src/, то data/input.txt будет ожидаться как src/data/input.txt. Снаружи кажется, что путь «правильный», но он правильный только в вашей голове. Абсолютный путь через absolutePath быстро возвращает в реальность.

Ошибка №3: склеивать пути строками и ловить странные слэши.
Строковая конкатенация быстро приводит к двойным разделителям, пропущенным /, и к «починил на Windows — сломал на Linux». Даже если вы пока не используете Path.of(...), сборка пути через File(parent, child) уже делает код заметно надёжнее и читабельнее.

Ошибка №4: пытаться диагностировать чтение файла, не диагностируя путь.
Часто новички сразу лезут в try/catch, ловят исключение и печатают «ошибка чтения». Но без вывода рабочей директории и абсолютного пути это сообщение почти бесполезно: вы знаете, что «не найден», но не знаете «где искали». Правильный порядок мышления: сначала выяснить «куда смотрим», потом уже разбираться с чтением.

Ошибка №5: держать путь к файлу размазанным по всему проекту.
Когда строка "data/expenses.txt" встречается в пяти местах, однажды вы поменяете её в четырёх и получите баг, который выглядит как мистика: «то читает старое, то пишет в новое». Гораздо спокойнее, когда путь создаётся в одном месте (функция или константа), а дальше передаётся по коду как File.

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