JavaRush /Курсы /Kotlin SELF /Чтение текста: readText, readLines, forEachLine

Чтение текста: readText, readLines, forEachLine

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

1. Введение

Когда говорят «прочитать файл», новички часто представляют себе что-то вроде: “программа открыла файл как блокнот и увидела буковки”. На самом деле программа всегда делает более скучную (и честную) работу: она берёт файл и превращает его содержимое в данные внутри вашего приложения — чаще всего в String или в List<String>.

С текстовыми файлами в Kotlin/JVM удобно то, что стандартная библиотека и File дают готовые методы «из коробки», и мы можем выбирать стиль чтения под задачу: прочитать всё целиком, прочитать по строкам в память, или обрабатывать файл построчно, почти не используя память.

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

Как выбрать метод чтения

Выбор между readText, readLines, forEachLine — это не вопрос “какой метод правильный”, а вопрос “какая форма данных мне нужна дальше” и “какого размера файл”.

Чтобы не выбирать каждый раз на кофейной гуще, полезно держать перед глазами сравнение.

Таблица выбора

Метод Что возвращает Память Когда удобно Типичный “послевкусие-код”
readText()
String
читает всё целиком файл — “одна строка смысла”: шаблон, небольшой текст, конфиг работа со строками: trim, split, replace, поиск подстрок
readLines()
List<String>
читает всё целиком файл логически состоит из строк, и вы хотите коллекционные операции map/filter/count/groupBy, сортировки, анализ строк
forEachLine {}
ничего не возвращает напрямую (вы сами накапливаете результат) построчно файл большой или результат можно посчитать “на лету” счётчики, поиск, максимум/минимум, накопление в MutableList

Тут есть важный психологический момент: readText() и readLines() дают вам готовый результат сразу, а forEachLine заставляет думать “как я буду накапливать ответ”. Это нормально: построчная обработка почти всегда немного более “алгоритмическая”.

Блок-схема выбора

Иногда проще запомнить не таблицу, а маленький маршрут выбора:

flowchart TD
    A[Нужно прочитать текст из файла] --> B{Нужен весь файл как единая строка?}
    B -->|Да| C["readText()"]
    B -->|Нет| D{Нужно иметь список строк в памяти?}
    D -->|Да| E["readLines()"]
    D -->|Нет, можно обработать на лету| F["forEachLine { ... }"]

Если вы поймали себя на мысли “мне неудобно с forEachLine, я хочу список” — это честный сигнал, что вам, возможно, реально нужен readLines(). Просто помните цену: список = память.

Нюанс про производительность вывода

Иногда чтение файла делают, чтобы потом вывести много строк в консоль. И тут появляется классическая ловушка: “я в цикле сделаю println на каждую строку”. Технически это работает, но при больших объёмах вывода может быть медленно. В Kotlin-сообществе часто советуют собирать вывод и печатать одним println через joinToString("\n"), когда строк много.

Мы сегодня не превращаем лекцию в гонку оптимизаций, но привычку «не печатать миллион println без причины» можно начать выращивать уже сейчас.

2. readText() — когда нужен весь файл целиком

Иногда файл — это просто “одна штука”: шаблон письма, маленький конфиг, сохранённый отчёт, текст заметки. В таких случаях удобнее всего получить его как одну строку. Для этого у File есть метод readText().

После readText() у вас появляется String, и вы можете делать со строкой всё, что уже умеете: length, trim(), split(), replace(), искать подстроки, строить отчёты через StringBuilder и так далее.

Минимальный пример readText()

Сначала — самый маленький пример, чтобы почувствовать механику:

import java.io.File

fun main() {
    val text: String = File("data/input.txt").readText()
    println("chars=${text.length}") // chars=...
}

Если файл небольшой, этот способ почти всегда самый «приятный для мозга»: одна переменная, один тип, дальше чистая работа со строками.

Пример: прочитал → нормализовал → использовал

Часто текст из файла хочется сначала «привести в порядок»: убрать лишние пробелы по краям, проверить, что там вообще не пусто.

import java.io.File

fun main() {
    val raw = File("data/title.txt").readText()
    val title = raw.trim()

    println("title='$title'") // title='...'
}

Обратите внимание: trim() здесь — не “косметика”, а реальная защита от сюрпризов вида «в конце файла случайно был перенос строки».

Когда readText() — плохая идея

readText() читает весь файл в память как одну строку. Если файл большой (например, лог на сотни мегабайт), то вы сначала тратите память на огромный String, а потом ещё тратите время на операции по этой строке. Тут уже хочется построчную обработку.

Простая житейская эвристика такая: если вы способны открыть файл в блокноте и он не заставляет блокнот задуматься о смысле жизни — readText() обычно норм. Если блокнот зависает и вы успеваете попить чай — лучше не надо.

3. readLines() — когда нужен список строк

Следующая типичная ситуация: файл — это набор строк, и каждая строка что-то значит. Например, список покупок, список команд, список заметок, список расходов. В таких задачах удобно сразу получить List<String>, где каждый элемент — отдельная строка файла. Для этого существует readLines().

readLines() хорош тем, что превращает файл в привычную коллекцию, а с коллекциями мы уже умеем многое: map, filter, count, firstOrNull, sortedBy, группировки и так далее.

Минимальный пример readLines()

import java.io.File

fun main() {
    val lines: List<String> = File("data/input.txt").readLines()
    println("lines=${lines.size}") // lines=...
}

Теперь у вас есть не «туманная строка с переносами», а конкретные элементы списка. Это часто делает код проще и читаемее.

Пример: найти первую непустую строку

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

import java.io.File

fun main() {
    val firstNonBlank = File("data/input.txt")
        .readLines()
        .firstOrNull { it.isNotBlank() }

    println(firstNonBlank) // например: Hello
}

Здесь важная привычка: мы используем firstOrNull, а не first. Потому что файл может оказаться пустым, и тогда first() устроит вам “вечеринку исключений”.

Пример: читаем расходы из текстового файла

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

amount|category|comment

Например:

350|food|coffee
1200|transport|metro card

Тогда чтение в память через readLines() выглядит естественно: мы хотим получить список строк, а потом распарсить каждую строку в объект.

import java.io.File

data class Expense(val amount: Int, val category: String, val comment: String)

fun parseExpenseOrNull(line: String): Expense? {
    val parts = line.split("|")
    val amount = parts.getOrNull(0)?.toIntOrNull() ?: return null
    val category = parts.getOrNull(1)?.trim().orEmpty()
    val comment = parts.getOrNull(2)?.trim().orEmpty()
    return Expense(amount, category, comment)
}

А теперь читаем файл и превращаем строки в расходы:

import java.io.File

fun main() {
    val lines = File("data/expenses.txt").readLines()
    val expenses = lines.mapNotNull { parseExpenseOrNull(it) }

    println("loaded=${expenses.size}") // loaded=...
}

Почему тут удобно именно readLines()? Потому что мы хотим коллекцию и хотим применить к ней “коллекционный стиль” (mapNotNull), вместо ручных циклов и временных переменных.

Ограничение readLines()

Как и readText(), readLines() читает файл целиком, только не в одну строку, а в список строк. То есть по памяти это всё ещё “загрузить всё сразу”. Для небольших и средних файлов — отлично. Для очень больших — лучше перейти к построчной обработке.

4. forEachLine { ... } — построчная обработка

Иногда нам не нужно хранить файл целиком. Мы хотим пройтись по нему и посчитать, найти, отфильтровать, но без создания огромного списка строк. Для этого у File есть построчный метод forEachLine { line -> ... }. Идея простая: Kotlin читает файл по строкам и отдаёт вам каждую строку в лямбду.

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

Минимальный пример forEachLine

import java.io.File

fun main() {
    var nonBlank = 0

    File("data/input.txt").forEachLine { line ->
        if (line.isNotBlank()) nonBlank++
    }

    println("nonBlankLines=$nonBlank") // nonBlankLines=...
}

Заметьте: мы не создавали List<String>. Мы просто прошли по файлу и посчитали.

Пример: найти максимальную сумму расхода без хранения строк

Если файл расходов может быть большим, а нам нужен, например, максимум по amount, то forEachLine хорошо подходит:

import java.io.File

fun main() {
    var maxAmount = 0

    File("data/expenses.txt").forEachLine { line ->
        val expense = parseExpenseOrNull(line) ?: return@forEachLine
        if (expense.amount > maxAmount) maxAmount = expense.amount
    }

    println("maxAmount=$maxAmount") // maxAmount=...
}

Здесь появился return@forEachLine. Это не “возврат из main”, а “пропустить текущую строку”. Мы делаем это, если строка битая или пустая.

Почему forEachLine часто безопаснее по памяти

forEachLine позволяет работать с файлом как с «ленточкой»: строка пришла → вы её обработали → пошли дальше. Память не раздувается от списка строк. Это особенно приятно, когда вы не знаете заранее, насколько большой файл вам подсунут (жизнь любит внезапные сюрпризы).

5. Мини-сборка: читаем файл и готовим данные для приложения

Сейчас сделаем аккуратный “скелет” чтения для нашего консольного приложения (условно — трекер расходов), но так, чтобы он оставался простым и не залезал в темы следующей лекции (запись) и следующих дней (надёжное I/O).

Смысл: мы хотим одну функцию, которая принимает File и возвращает список корректных расходов. Всё. Без магии.

Функция чтения через readLines()

import java.io.File

fun loadExpenses(file: File): List<Expense> {
    if (!file.exists() || !file.isFile) return emptyList()

    return file.readLines()
        .mapNotNull { line -> parseExpenseOrNull(line.trim()) }
}

Это читается почти как русский текст: “если файла нет — пусто; иначе прочитать строки и распарсить”.

Альтернатива через forEachLine

import java.io.File

fun loadExpensesStreaming(file: File): List<Expense> {
    if (!file.exists() || !file.isFile) return emptyList()

    val result = mutableListOf<Expense>()
    file.forEachLine { line ->
        val e = parseExpenseOrNull(line.trim()) ?: return@forEachLine
        result.add(e)
    }
    return result
}

Да, кода чуть больше. Зато он не требует держать List<String> всех строк. Мы аккуратно копим только “хорошие” записи.

6. Типичные ошибки при чтении текста из файлов

Ошибка №1: выбрать readText() “по привычке”, а потом удивляться, что программа ест память.
readText() — отличный метод, но он буквально означает “прочитать ВСЁ”. Если файл внезапно оказался большим, вы получите огромный String, а дальше — тяжёлые операции над ним. В таких случаях полезно переключиться на readLines() или forEachLine() и мыслить строками.

Ошибка №2: путать “строка файла” и “строка как String целиком”.
После readText() у вас одна большая строка, внутри которой есть "\n". Новички иногда ожидают, что “раз в файле много строк, значит и переменная будет как-то построчно”. Не будет. Если дальше вам нужно работать именно со строками, быстрее (и яснее) сразу взять readLines().

Ошибка №3: не нормализовать строки перед разбором.
Даже “чистый” файл часто имеет хвостовые пробелы, пустые строки, лишние переносы. Если сразу делать split("|") без trim() и без проверки isBlank(), вы получите странные баги, которые выглядят как “вчера работало, сегодня нет”. Минимальная защита — line.trim() и аккуратные toIntOrNull().

Ошибка №4: забывать, что файл может отсутствовать, а путь может указывать на директорию.
Если вы сразу делаете File("...").readLines(), а файл не найден, программа упадёт с исключением. Мы уже умеем базовые проверки exists() и isFile — и это тот самый случай, когда они реально экономят нервы. Особенно в учебных проектах, где директории и файлы постоянно переезжают.

Ошибка №5: пытаться “вернуть значение” из forEachLine обычным return.
Внутри лямбды forEachLine обычный return может вести себя не так, как ожидает новичок (там важны правила возврата из лямбд). Если вы хотите пропустить строку — используйте return@forEachLine. Если хотите накапливать результат — заведите переменную снаружи и обновляйте её внутри лямбды.

Ошибка №6: смешивать чтение и сложную бизнес-логику в одном огромном блоке.
Когда внутри forEachLine или после readLines() сразу начинается “простыня” из условий, подсчётов и форматирования отчёта, код становится трудно читать и тяжело отлаживать. Гораздо спокойнее держать стиль: “прочитал → получил структуру данных → дальше обработал”, даже если структура данных пока простая.

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