1. Введение
Когда программа маленькая, соблазн хранить всё в одном .kt файле огромен. Это как “одна коробка для всего”: и носки, и зарядки, и документы — пока коробка маленькая, вроде даже удобно. Но как только код растёт, коробка превращается в чёрную дыру: вы точно помните, что где-то была функция readIntOrNull(), но где именно — уже нет.
Пакеты (package) в Kotlin — это способ дать коду понятный “адрес”, чтобы вы (и IDE) могли быстро находить нужные функции, а одинаковые имена не дрались друг с другом за право жить в одном пространстве.
Важно сразу уловить ключевую мысль дня:
package — это “пространство имён” (namespace), логическая группировка кода.
Папки в проекте — это уже “физическая организация” файлов на диске. Обычно они согласованы, но это разные вещи.
2. Что такое package: пространство имён и “адрес” объявлений
Если смотреть на Kotlin‑файл как на письмо, то package — это строка “куда доставить”. В начале файла вы пишете:
package app
И всё, что объявлено в этом файле (функции, константы и т.д.), начинает “жить” по адресу app.
Минимальный пример: файл в пакете app
Давайте представим, что мы делаем консольную игру “Угадай число” (из мини‑проекта). Пока всё простое, у нас может быть один файл Main.kt:
package app
fun main() {
println("Игра началась!") // Игра началась!
}
Теперь main формально имеет “полное имя” (fully qualified name): app.main. Обычно его так не пишут, но мыслить этим полезно.
Один файл — один package
В Kotlin package задаётся для файла целиком. Вы не можете в одном .kt файле объявить половину функций в app, а половину в app.util. Если нужно другое пространство имён — создаём другой файл.
Top-level объявления “принадлежат” пакету
Вы уже видели, что переменные и функции в Kotlin можно объявлять на верхнем уровне файла (вне классов). Так вот: такие объявления тоже живут в пакете файла. Пакет — это “дом” для объявлений, а файл — скорее “контейнер/лист бумаги”, где вы эти объявления записали.
3. Полное имя: как Kotlin различает одинаковые имена
Когда проект растёт, вы почти неизбежно напишете две функции с одинаковым “коротким” именем. Например, format() — отличное имя, и его хочется использовать в разных местах.
С точки зрения Kotlin, уникальность имени обеспечивается не только “именем функции”, а сочетанием:
package + имя
(и ещё сигнатура, но это уже отдельная история).
Две одинаковые функции в разных пакетах — нормально
Файл 1: src/main/kotlin/app/util/format.kt
package app.util
fun formatScore(score: Int): String {
return "Score: $score"
}
Файл 2: src/main/kotlin/app/cli/format.kt
package app.cli
fun formatPrompt(text: String): String {
return ">> $text"
}
Обе функции называются по-разному (formatScore и formatPrompt), но даже если бы они обе назывались просто format, Kotlin всё равно мог бы их различить по пакетам. Проблема начнётся не в “существовании”, а в “использовании” — когда вы попытаетесь вызвать format() из третьего места. Это мы будем аккуратно решать через import в следующей лекции, а сегодня нам важно понять сам принцип адресации.
Важный нюанс: файл не является областью видимости для разрешения имён
Этот момент звучит как фраза из документа про компилятор, но он реально практичный. Kotlin (и IDE) стараются сделать так, чтобы перенос функции в другой файл того же пакета не ломал код только потому, что “она теперь в другом файле”. В спецификации разрешения имён прямо обсуждается идея, что “нет отдельной области видимости файла” — важен пакет, а не конкретный .kt.
Перевод на человеческий: файл — это не “владелец” функции. Владелец — пакет. Поэтому пакет можно мыслить как “папку в телефонной книге”: не важно, на какой страничке записан номер, важно, в какой категории он лежит.
4. Папки и package: договорённость, которая спасает нервы
Сейчас будет важная практическая часть: как принято организовывать проект на Kotlin/JVM.
В Gradle‑проекте (а у большинства из вас он именно такой) код обычно лежит здесь:
src/main/kotlin/
И дальше начинается самое интересное: внутри src/main/kotlin обычно делают папки, которые повторяют package.
Соответствие “точки ↔ папки”
Есть простое правило‑ассоциация:
| В package | На диске |
|---|---|
|
|
|
|
|
|
|
|
То есть:
- точки в package превращаются в “папки”;
- порядок папок повторяет “адрес”.
Например, если вы пишете:
package app.util
то файл обычно лежит по пути:
src/main/kotlin/app/util/ИмяФайла.kt
Почему это именно “договорённость”, а не жёсткий закон
Компилятор Kotlin технически может собрать проект, где файл лежит в “не той” папке (особенно если вы вручную настроили исходные директории). Но IDE и люди будут страдать.
IntelliJ IDEA умеет подсвечивать проблему примерно в стиле Package directive doesn't match file location. И это тот редкий случай, когда IDE не занудствует, а реально пытается спасти вас от будущего хаоса.
Аналогия: адрес и склад
package — это “адрес на конверте” (как вас найти логически). Папки — это “расположение коробок на складе” (как это хранится физически).
Если адрес и склад совпадают — вы счастливы. Если не совпадают — вы всё ещё можете найти коробку, но будете делать это как герой квеста “принеси артефакт из подвала, но карта у тебя от другой игры”.
5. Практика на уровне проекта: разнос игры “Угадай число” по пакетам
Сейчас мы не будем добавлять новые языковые конструкции. Мы возьмём уже знакомые функции и просто разложим их по смысловым “адресам”.
Представим, что у нас есть логика:
- ввод числа,
- генерация секрета,
- основной игровой цикл,
- печать сообщений пользователю.
Если держать всё в одном файле Main.kt, он быстро раздуется. Поэтому разделим на пакеты:
- app — точка входа и сборка сценария (то есть main)
- app.game — логика игры
- app.io — ввод и валидация (утилиты чтения)
- app.util — мелкие помощники форматирования
Нарисуем это как “карту адресов”:
flowchart TD
A["package app
Main.kt"] --> B["package app.game
Game.kt"]
A --> C["package app.io
Input.kt"]
A --> D["package app.util
Text.kt"]
app: точка входа
Файл: src/main/kotlin/app/Main.kt
package app
fun main() {
println("Добро пожаловать в 'Угадай число'!") // Добро пожаловать в 'Угадай число'!
}
Пока тут мало. Но идея такая: main — это “дирижёр”. Он вызывает функции из других пакетов и соединяет сценарий в одно приложение.
app.util: нормализация текста
Файл: src/main/kotlin/app/util/Text.kt
package app.util
fun normalizeCommand(raw: String): String {
return raw.trim().lowercase()
}
Эта функция маленькая, но очень “пакетная”: она не про игру, не про ввод чисел, а про обработку строк в целом. Значит, ей логично жить в util.
app.io: чтение числа “по-человечески”
Файл: src/main/kotlin/app/io/Input.kt
package app.io
fun readIntOrNull(prompt: String): Int? {
print(prompt)
val raw = readln().trim()
return raw.toIntOrNull()
}
Обратите внимание: здесь нет никакой магии. Это та же логика, которую вы писали раньше, просто она теперь в отдельном файле и с понятным адресом app.io.readIntOrNull.
app.game: генерация секрета
Файл: src/main/kotlin/app/game/Secret.kt
package app.game
import kotlin.random.Random
fun generateSecret(from: Int, until: Int): Int {
return Random.nextInt(from, until)
}
Тут мы снова используем уже знакомую тему Random (вы её проходили раньше). Важно, что логика, относящаяся к игре, начинает жить в app.game, и это делает проект читабельнее: вы прямо по пути файла понимаете, “о чём он”.
6. Дисциплина пакетов: default package и именование
Пакет по умолчанию и почему он почти всегда плохая идея
Иногда вы видите файл без строки package в начале. Это означает, что файл находится в пакете по умолчанию (default package).
Технически это работает. Практически — это как “жить без адреса”: курьер всё равно может вам позвонить и спросить “ну вы где-то возле подъезда, да?”, но нормальная жизнь так не строится.
Основные проблемы default package обычно проявляются не сразу. Сначала всё хорошо: один файл, второй файл, третий… А потом вы внезапно хотите:
- разделить код на подпакеты,
- переиспользовать функции в другом проекте,
- понять, где какая ответственность,
- избежать конфликтов имён.
И вот тут отсутствие пакетов начинает мстить.
Хорошее правило для учебных и маленьких проектов: как только у вас больше одного файла — заведите нормальный package, хотя бы package app.
Да и для одного файла это часто полезно: вы привыкаете к дисциплине и меньше страдаете при росте проекта.
Нюансы именования пакетов
В индустрии есть общие соглашения:
Имена пакетов пишутся в нижнем регистре, обычно через точки, например app.util или com.example.myapp. Такая форма удобна и читаема, и она почти не конфликтует с именами классов/функций.
Если очень хочется назвать пакет MyCoolStuff, остановитесь на секунду и представьте, как вы будете жить с этим через полгода. Скорее всего, вы всё равно придёте к mycoolstuff или my.cool.stuff. Kotlin‑код читается много раз, пишется один раз — поэтому лучше сразу выбрать вариант, который не раздражает глаз.
Ещё один момент: пакет — это логическая группировка. Не пытайтесь сделать “пакет на каждую функцию”. Это как в супермаркете: если у вас отдельная полка для каждого батона, вы не упорядочили магазин, вы построили музей батонов.
7. Типичные ошибки при работе с package и папками
Ошибка №1: package не соответствует расположению файла в src.
Это самая распространённая история: файл лежит в src/main/kotlin/app/util, а в начале написано package app.utils (лишняя s) или вообще package app. Формально вы можете даже какое-то время жить так, но IDE будет путаться, навигация станет странной, а проект начнёт напоминать шкаф, где футболки лежат в ящике “ложки”.
Ошибка №2: всё складывается в один пакет “потому что так проще”.
Пакет app быстро превращается в “пакет‑мусорку”, где живёт и ввод, и логика игры, и форматирование. Это сначала кажется удобным, а потом вы боитесь открывать автодополнение, потому что там 30 функций с похожими именами. Лучше добавлять подпакеты по смыслу: app.io, app.game, app.util.
Ошибка №3: ожидание, что “один файл = один пакет”.
Наоборот: один пакет почти всегда состоит из множества файлов. Пакет — логическая область, а файл — просто место, где вы записали часть объявлений. Важно привыкнуть мыслить пакетами, а не файлами, иначе вы будете “искать функцию по имени файла” — это не самая приятная форма археологии.
Ошибка №4: оставлять файлы в default package “пока проект маленький”.
Это ловушка. Проект маленький ровно до тех пор, пока вы не добавили второй файл и не захотели чуть-чуть порядка. А дальше начинается миграция: перенос файлов, починка ссылок, переезд директорий. Гораздо дешевле завести нормальный пакет сразу, даже если он один.
Ошибка №5: странные имена пакетов (пробелы, верхний регистр, “как попало”).
Технически некоторые вещи могут даже собраться (до первого серьёзного конфликта), но читаемость и культура проекта падают. Нижний регистр и понятные слова — скучно, зато работает. Как и большинство хороших инженерных решений.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ