Kotlin – Telegram
Kotlin
2.16K subscribers
301 photos
140 videos
18 files
438 links
Подборки полезного материала по Kotlin. По всем вопросам @evgenycarter
Download Telegram
Ktor Server Fundemantals. Часть 1

Освойте разработку бэкенда на Ktor с использованием Kotlin! Эта серия материалов охватывает основы Ktor, маршрутизацию, обработку запросов, аутентификацию и другие ключевые концепции, которые помогут вам эффективно создавать надежные серверные приложения.

источник

✍️ @kotlin_lib
👍3🥰1
This media is not supported in your browser
VIEW IN TELEGRAM
Маленький экран — серьёзный вызов!

В VK мобильные разработчики создают опыт, который помещается в карман, но работает на миллионах устройств. Узнайте об их подходах к сложным задачам и ключевых результатах. По ссылке — ролики и даже вакансии!
Debounce vs Sample в Kotlin Flow

Ну что ж, пора снова погрузиться в мир Flow! Сегодня мы выносим на первый план два недооценённых инструмента: debounce и sample.

Про debounce многие из вас уже слышали, а вот про sample — гораздо реже. И, если быть честными, некоторые вообще используют debounce неправильно. Так что сейчас мы разложим всё по полочкам и сделаем эти концепции предельно понятными. Погнали! 🔥

https://proandroiddev.com/debounce-vs-sample-in-kotlin-flow-a89b4a94c893

✍️ @kotlin_lib
👍2
Kotlin: val != Immutable? 🤔

Многие новички (и не только) живут с убеждением, что ключевое слово val гарантирует неизменяемость данных. Но так ли это на самом деле?

В недавней статье на ProAndroidDev разбирают популярное заблуждение: val - это read-only (доступ только для чтения), но никак не immutable (неизменяемость).

Вот два кейса, когда ваш «неизменяемый» val может измениться:

1️⃣ Изменяемость самого объекта
val гарантирует только то, что ссылка на объект останется той же. Но если объект внутри изменяемый - его состояние можно менять без проблем.


val list = mutableListOf(1, 2, 3)
list.add(4) // Ссылка та же, содержимое изменилось


2️⃣ Кастомные геттеры
Это самый коварный момент. Свойство val может возвращать разные значения при каждом обращении, если у него переопределен get().


val random: Int
get() = Random.nextInt()


В статье также приводят интересную статистику: в опросе 41% разработчиков ответили, что считают val именно immutable, что технически неверно.

По-настоящему неизменяемым объект становится только тогда, когда он состоит из примитивов или других неизменяемых объектов (например, Data Class, где все поля val и нет ссылок на мутабельные типы).

https://proandroiddev.com/the-val-property-immutable-in-kotlin-2e4cf49207d0

✍️ @kotlin_lib
👍3🤡3🫡1
🩸 Value Classes: Когда «оптимизация» убивает перформанс

Мы привыкли думать, что value class - это серебряная пуля для Type Safety без оверхеда. Обернули Int в UserId, и в рантайме это просто int, верно?

Не всегда. Есть сценарии, где value class начинает вести себя хуже, чем обычный data class, порождая скрытый боксинг на ровном месте.

Рассмотрим классический кейс: попытка написать обобщенный обработчик.


@JvmInline
value class UserId(val id: Int)

fun <T> handle(item: T) {
// Какая-то логика
println(item)
}

fun main() {
val uid = UserId(42)
handle(uid) // <--- Здесь происходит магия (плохая)
}



Что происходит под капотом?

В момент вызова handle(uid) компилятор сталкивается с проблемой. Функция handle ожидает T (который в JVM стирается до Object), а UserId в скомпилированном виде - это примитив int.

Чтобы передать int туда, где ожидается Object, JVM обязана его упаковать.

1. Создается экземпляр-обертка UserId в куче (heap allocation).
2. Этот объект передается в функцию.
3. Если внутри функции мы снова кастуем его к конкретному типу, происходит анбоксинг.

Еще хуже: Интерфейсы

Если ваш value class реализует интерфейс, и вы работаете с ним через ссылку на этот интерфейс - вы гарантированно получаете боксинг.


interface Identity {
fun raw(): Int
}

@JvmInline
value class GroupId(val id: Int) : Identity {
override fun raw() = id
}

fun process(id: Identity) { ... } // 100% боксинг при вызове



Почему это важно?

Если вы используете value classes в горячих циклах (hot paths) или высоконагруженных стримах, думая, что экономите память, использование их в качестве дженериков или через интерфейсы приведет к обратному эффекту:

🖤GC Pressure: Вы создаете короткоживущие объекты-обертки тысячами.
🖤Снижение производительности: Аллокация + боксинг/анбоксинг стоят дороже, чем передача ссылки на обычный объект.

Как лечить?

1. Избегайте интерфейсов на value classes, если критична производительность.
2. Специализируйте функции. Вместо fun <T> handle(t: T) пишите перегрузки для конкретных value типов, если это возможно.
3. Ждите Project Valhalla. Настоящие примитивные классы в JVM решат эту проблему фундаментально, но пока мы живем с ограничениями Type Erasure.

Итог: value class идеален для доменной модели и сигнатур функций, но будьте предельно осторожны, как только они попадают в полиморфный контекст.

#kotlin #performance #jvm #senior

✍️ @kotlin_lib
Please open Telegram to view this post
VIEW IN TELEGRAM
👍42
💀 ThreadLocal в мире Coroutines: Цена магии

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

Проблема: M:N Threading

В мире корутин один и тот же бизнес-процесс может начать выполняться на Thread-1, приостановиться (IO), и продолжить на Thread-2.
Если вы положили данные в ThreadLocal (например, RequestID для логирования) и не передали их в контекст корутины - после саспенда вы эти данные потеряете. Или, что хуже, прочитаете "грязные" данные от другой корутины, которая использовала этот поток ранее.

Решение: ThreadContextElement

Kotlin предлагает мост: ThreadLocal.asContextElement(value).
Выглядит просто, но под капотом происходит постоянное перекладывание данных.


val myTL = ThreadLocal<String>()

launch(Dispatchers.Default + myTL.asContextElement("foo")) {
println(myTL.get()) // "foo"
delay(10) // Suspend -> поток освобождается
println(myTL.get()) // "foo" (хотя поток может быть уже другим)
}



Что происходит в DispatchedContinuation?

Каждый раз, когда корутина возобновляется (resume) на потоке, срабатывает механизм перехватчиков.

1. Mount (updateThreadContext): Перед выполнением блока кода значение из контекста корутины принудительно устанавливается в ThreadLocal текущего потока. Старое значение потока запоминается.
2. Execution: Код корутины выполняется.
3. Unmount (restoreThreadContext): Как только корутина снова саспендится или завершается, в ThreadLocal потока возвращается старое значение.

Это гарантирует, что мы не загрязняем потоки пула.

📉 Скрытый оверхед (Performance Penalty)

Это не бесплатно. Если у вас в стеке корутины 5 элементов ThreadContextElement (MDC, SecurityContext, Tracing, TenantId и т.д.), и ваша корутина делает 100 саспендов (например, частые мелкие IO или yield()), то 100 раз произойдет цикл:
Save Old State -> Set New State -> Run -> Restore Old State.

На высоконагруженных системах (High Throughput) это создает ощутимое давление, так как эти операции часто включают работу с ThreadLocalMap, что не так быстро, как доступ к регистрам.

Best Practices для Senior-разработчика

1. Не используйте ThreadLocal как шину данных. Передавайте явные параметры в функции (context receivers в будущем помогут).
2. Ограничьте Scope. Используйте withContext(myTL.asContextElement()) только вокруг тех участков кода, где это действительно нужно (например, вызов легаси библиотеки, зависящей от ThreadLocal), а не на уровне всего GlobalScope или корневой корутины.
3. MDC Integration. Для логирования лучше использовать специализированные библиотеки, которые умеют эффективно работать с контекстом, или копировать контекст только на границах системы, а не таскать его везде.

Вывод: asContextElement - это костыль для совместимости с Thread-bound миром. В чистом Coroutine-мире данные должны течь через аргументы или CoroutineContext, но не через ThreadLocal.

#kotlin #coroutines #concurrency #jvm #senior

✍️ @kotlin_lib
3👍3
Скрытый боксинг value-классов. Мы всё еще аллоцируем?

Все любят value class (бывшие inline-классы). Обертка без оверхеда, типизация примитивов, красота. Но сеньор должен знать, когда эта магия ломается.

Смотрим код:


@JvmInline
value class UserId(val id: String)

fun processUser(id: UserId) { ... } // (1)
fun <T> logItem(item: T) { ... } // (2)

val uid = UserId("101")
processUser(uid) // Всё ок, передается String
logItem(uid) // 💥 А вот тут — боксинг!



Почему?
В строке (2) функция принимает дженерик T. JVM не знает, что там внутри value class, и вынуждена создать объект-обертку (instantiate UserId), чтобы передать его как Object.

Где еще ловим боксинг?

1. Использование внутри коллекций (List<UserId>).
2. Реализация интерфейсов (value class UserId : IEntity).
3. Nullable-обертки над примитивными value-классами (value class Code(val x: Int) -> Code?).

Вывод: Используйте value class для доменной выразительности, но не обманывайте себя, что это всегда zero-allocation. Если гонитесь за перформансом в горячих циклах, проверяйте байт-код или профайлер.

✍️ @kotlin_lib
👍3
🚀 Sequence не всегда быстрее. Разрушаем мифы оптимизации

Каждый Kotlin-разработчик знает мантру: "Если цепочка операторов длинная, бери Sequence, чтобы избежать промежуточных коллекций".

Это правда. Но часто на код-ревью я вижу asSequence() на списках из 50-100 элементов. И вот тут начинается оверхед, о котором забывают.

В чем подвох?

Операторы над Iterable (map, filter) - это inline functions.
Когда вы пишете list.filter { ... }, компилятор фактически вставляет код цикла for прямо в место вызова. JIT это обожает, процессор счастлив, аллокаций минимум (кроме результирующего списка).

Операторы над Sequence, это создание объектов.
Вызов .map { ... } создает новый инстанс TransformingSequence. Вызов .filter { ... } - FilteringSequence.
Это цепочка объектов-декораторов. Каждый элемент протаскивается через виртуальные вызовы next().

📉 Бенчмарки (JMH, List vs Sequence):

Допустим, у нас простая цепочка: filter + map + first.

1. Маленький список (10-100 элементов):
🔵Iterable: Быстрее в 1.5 - 2 раза.
🔵Причина: создание объектов-оберток для Sequence стоит дороже, чем тупой проход циклом и создание мелких промежуточных ArrayList'ов.


2. Средний список (~1 000 элементов):
🔵Паритет. Разница становится невидимой.


3. Гигантский список (100k+ элементов) или тяжелые операции:
🔵Sequence: Win. Здесь ленивость спасает от OutOfMemory и лишней работы.



💡 Не ставьте asSequence() по дефолту везде.

1. Если коллекция маленькая (помещается на экран телефона) - Iterable.
2. Если вы делаете map, а потом сразу toList() - Iterable (Sequence тут просто лишний посредник).
3. Если стриминг данных, чтение файла построчно или поиск первого совпадения в огромном массиве - Sequence.

Оптимизация должна быть осознанной, а не рефлекторной.

А вы используете Sequence по дефолту или только там, где профайлер подсветил? 👇

✍️ @kotlin_lib
Please open Telegram to view this post
VIEW IN TELEGRAM
4👍2
ИИ уже стал частью разработки. Вопрос больше не в том, использовать его или нет, а в том, кто управляет процессом — вы или модель?

28 января OTUS проводит открытый урок «Вайбкодинг: практика разработки (Kotlin) проекта при участии ИИ».

На вебинаре разберём, как современные ИИ-помощники влияют на код, архитектуру и качество решений.
Вы увидите несколько подходов к работе с ИИ, сравните форматы взаимодействия и поймёте, почему неумелое использование нейросетей ломает проекты вместо ускорения разработки. Отдельное внимание уделим постановке задач ИИ и тому, как меняется дизайн приложений, когда разработчик выступает не исполнителем, а контролёром и архитектором решений.

Встречаемся в преддверии старта курса «Kotlin Backend Developer. Professional».

Зарегистрируйтесь, чтобы понять, как встроить ИИ в разработку осознанно и использовать его как инструмент, а не источник технического долга: https://vk.cc/cTzvxh

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
🤡9
Обратная сторона inline функций.

Все знают, что inline, это круто для лямбд (экономим на создании объектов). Но многие лепят это ключевое слово куда попало, думая, что это аналог static final или просто "ускоритель".

Сейчас я объясню, почему за это бьют по рукам.


🛑 Вы злоупотребляете inline. Остановитесь.

В Kotlin inline - это священная корова. Мы используем его, чтобы убрать оверхед при работе с лямбдами и получить доступ к reified.

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

1. Code Bloat (Раздувание байт-кода)
Вспомним механику: компилятор берет тело инлайн-функции и копипастит его в каждое место вызова.
Если ваша функция занимает 5 строк и вызывается 100 раз - это ок.
Если функция занимает 50 строк сложной логики и вызывается 100 раз - вы только что добавили ~5000 строк инструкций в свой classes.dex или JAR.

Это не только увеличивает размер артефакта, но и бьет по Instruction Cache процессора. Процессор любит короткий, горячий код, который влезает в кэш.

2. Нарушение инкапсуляции (Synthetic Accessors)
Это самый коварный момент.
Допустим, у вас есть inline функция внутри класса, которая обращается к private полю:


class Manager {
private var token: String = "secret"

inline fun execute(action: () -> Unit) {
println("Token: $token") // Обращение к приватному полю
action()
}
}



Что происходит под капотом?
Код функции execute вставляется в место вызова (во внешний мир). Но этот внешний код не имеет права читать private token.
Чтобы это сработало, компилятор Kotlin тихо генерирует публичный синтетический метод (акцессор): access$getToken$p(), который возвращает значение.

Фактически, ваша приватность убита. Любой Java-код в том же пакете теоретически может дернуть этот акцессор.

3. Невозможность скрыть стектрейс
При краше внутри заинлайненной функции стектрейс иногда выглядит странно, указывая на строку вызова, а не на логику внутри функции (хотя LineNumberTable старается помогать, но в сложных цепочках отлаживать это - боль).

📝 Чек-лист Сеньора:
Используйте inline только если принимаете функцию-лямбду как аргумент.

Используйте inline, если жизненно нужен reified.

Не ставьте inline для обычных маленьких функций без лямбд (JIT сам их заинлайнит при выполнении, если посчитает нужным, не мешайте ему).

Избегайте обращения к private членам внутри больших inline функций.

У кого проект худел на пару мегабайт после чистки лишних inline? Делитесь историями 👇

✍️ @kotlin_lib
👍31👎1
🔓 @PublishedApi: Как открыть internal и не покраснеть

В прошлом посте мы говорили, что inline функции нарушают инкапсуляцию. Но что делать, если вам реально нужно обратиться к internal свойству внутри инлайн-функции?

Компилятор ударит по рукам:
"Public-API inline function cannot access non-public declarations"

И он прав. Поскольку код инлайнится в модуль клиента, он физически не сможет вызвать ваш internal метод, так как байт-код клиента ничего не знает о ваших внутренних секретах.

Тут на сцену выходит @PublishedApi.

Смотрим код:


class FastCache {
// Обычный internal - доступен только внутри модуля
internal val backingMap = hashMapOf<String, Any>()

// ОШИБКА КОМПИЛЯЦИИ:
// Inline функция видна всем, а backingMap - нет.
inline fun getOrPut(key: String, block: () -> Any): Any {
return backingMap.getOrPut(key, block)
}
}



Чтобы это заработало, мы должны сказать компилятору: "Я понимаю риски, сделай это поле публичным в байт-коде, но скрой его в IDE от чужих глаз".

Как надо:


class FastCache {
@PublishedApi // 👈 Магия здесь
internal val backingMap = hashMapOf<String, Any>()

inline fun getOrPut(key: String, block: () -> Any): Any {
return backingMap.getOrPut(key, block) // Работает!
}
}



Что происходит Under the Hood?

1. В Source Code: Поле остается internal. Если кто-то попробует написать fastCache.backingMap из другого модуля - IDE не подскажет, а компилятор выдаст ошибку.

2. В Bytecode: Поле становится public (или генерируются публичные акцессоры). Это нужно, чтобы заинлайненный код на стороне клиента мог технически выполнить вызов.

⚠️ Warning для архитекторов:
Используя @PublishedApi, вы подписываете кровью контракт о бинарной совместимости.
Если вы в следующей версии библиотеки переименуете или удалите backingMap, код всех клиентов, кто заинлайнил вашу функцию getOrPut, упадет с NoSuchMethodError или NoSuchFieldError в рантайме (даже без перекомпиляции).

Вердикт:
Инструмент мощный для создания zero-overhead оберток, но требует дисциплины. Меняете @PublishedApi поле? Поднимайте мажорную версию библиотеки.

Кто пишет свои либы, часто приходится так "оголять" кишки классов?

✍️ @kotlin_lib
👍3
Какое утверждение о типе Nothing в Kotlin является технически верным с точки зрения иерархии типов?
Anonymous Quiz
62%
A. Это подтип всех типов (Bottom Type).
10%
Б. Это псевдоним для типа Unit.
6%
В. Это супертип всех типов (аналог Any).
22%
Г. Это аналог типа void из Java.
Монолитная архитектура часто воспринимается как что-то устаревшее. Но на практике именно монолит остаётся основой большинства production-систем — особенно там, где важны скорость разработки, прозрачность логики и предсказуемость сопровождения.

На открытом уроке OTUS разберём разработку монолитного приложения со Spring, используя Kotlin. Поговорим о плюсах и минусах монолитной архитектуры, разберём фреймворк Spring и особенности его использования в Kotlin-проектах. Покажем, как интегрировать транспортные модели, собрать приложение и подготовить его к запуску.

Вы увидите полный путь от архитектурного решения до работающего приложения. Попрактикуетесь в разработке монолита, разберётесь в особенностях Spring + Kotlin и научитесь упаковывать приложение в Docker. Урок будет полезен backend-разработчикам, которые хотят глубже понимать архитектуру, уверенно работать со Spring и собирать production-готовые сервисы.

Встречаемся 4 февраля в 19:00 МСК в преддверии старта курса «Kotlin Backend Developer. Professional».
Регистрация открыта:
https://vk.cc/cTOtxI

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576