Почему Java-программисты плачут, когда переходят на Kotlin?
Представьте: вы пишете код на Java, открываете файл, читаете данные, а потом… забываете его закрыть. Знакомая боль? В Java для этого придумали конструкцию try-with-resources, которая автоматически вызывает close(). Но когда разработчики переходят на Kotlin, они часто впадают в ступор: «А где тут мой любимый try (Resource res = ...)?».
Спойлер: в Kotlin нет try-with-resources как отдельной языковой конструкции. И это хорошая новость. Вместо неё язык предлагает более элегантное и лаконичное решение — функцию use. Давайте разберёмся, как это работает и почему ваш код станет чище.
Проблема: ресурсы нужно закрывать
Любой внешний ресурс — файл, сетевое соединение, база данных — требует ручного управления. Если вы откроете FileInputStream и не закроете его, система может «потечь» (утечка памяти), а файл останется заблокированным для других процессов.
В Java эту проблему решают двумя способами:
- Старый способ: блок
finallyс явным вызовомclose(). - Новый способ:
try-with-resources(появился в Java 7).
Kotlin пошёл дальше: он использует функции расширения и лямбды, чтобы сделать код не только безопасным, но и красивым.
Как выглядит try-with-resources в Java?
Для сравнения посмотрим на классический Java-код:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// br закрывается автоматически
Всё хорошо, но есть нюансы:
- Нужно объявлять переменную внутри скобок
try. - Поддерживаются только классы, реализующие
AutoCloseable. - Код становится громоздким, если ресурсов несколько.
Kotlin-подход: функция use
В Kotlin для безопасной работы с ресурсами используется функция use. Она определена как расширение для интерфейса Closeable (и AutoCloseable для совместимости с Java).
Вот как выглядит тот же пример на Kotlin:
import java.io.BufferedReader
import java.io.FileReader
fun main() {
BufferedReader(FileReader("file.txt")).use { reader ->
val line = reader.readLine()
println(line)
}
// reader автоматически закрывается
}
Чисто, лаконично, без лишних try-catch? Не совсем. use сам по себе не ловит исключения. Он только гарантирует вызов close() в конце блока. Если нужно обработать ошибку, добавьте try-catch снаружи:
try {
BufferedReader(FileReader("file.txt")).use { reader ->
val line = reader.readLine()
println(line)
}
} catch (e: IOException) {
println("Ошибка при чтении файла: ${e.message}")
}
Как работает use под капотом?
Чтобы понять магию, заглянем в исходники Kotlin. Функция use определена в стандартной библиотеке примерно так:
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
exception == null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
// подавляем исключение, добавляем его к основному
exception.addSuppressed(closeException)
}
}
}
}
Что здесь важно:
- inline — функция встраивается в код на этапе компиляции, без накладных расходов.
- finally гарантирует закрытие ресурса.
- Подавленные исключения — если и
close()выбросит ошибку, она не «съест» основное исключение, а добавится к нему черезaddSuppressed().
Работа с несколькими ресурсами
В Java при использовании нескольких ресурсов приходится вкладывать try друг в друга или разделять точкой с запятой. В Kotlin всё проще — используйте вложенные use или создавайте цепочки:
import java.io.*
fun main() {
// Вложенные вызовы use
FileInputStream("input.txt").use { input ->
FileOutputStream("output.txt").use { output ->
input.copyTo(output)
}
}
println("Файл скопирован")
}
Или ещё более элегантный вариант с sequence и forEach, но для простоты оставим вложенность. Главное — каждый ресурс будет закрыт, даже если произойдёт ошибка.
Что делать, если класс не реализует Closeable?
Иногда вы работаете с библиотеками, где ресурсы нужно закрывать вручную, но они не наследуют Closeable. Например, старые Java-классы или свои собственные объекты.
В Kotlin можно использовать функцию use для любого объекта, реализующего AutoCloseable. Если ваш класс не реализует ничего — просто добавьте интерфейс:
class MyResource : AutoCloseable {
override fun close() {
println("Ресурс закрыт")
}
fun doWork() {
println("Работаем...")
}
}
fun main() {
MyResource().use { resource ->
resource.doWork()
}
// Вывод: Работаем... \n Ресурс закрыт
}
Теперь ваш кастомный ресурс работает как полноценный участник use-блока.
Работа с потоками ввода-вывода
Одна из самых частых задач — чтение или запись файлов. В Kotlin для этого есть удобные функции-расширения, которые уже используют use внутри:
import java.io.File
fun main() {
// Чтение всех строк файла (use вызывается неявно)
val lines = File("data.txt").readLines()
lines.forEach { println(it) }
// Запись в файл
File("output.txt").writeText("Hello, Kotlin!")
// Буферизованное чтение с ручным use
File("data.txt").bufferedReader().use { reader ->
val firstLine = reader.readLine()
println("Первая строка: $firstLine")
}
}
Функции readLines(), writeText(), bufferedReader() уже заботятся о закрытии ресурсов. Но если вам нужен более тонкий контроль — используйте use напрямую.