Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
نمایش بیشتر📈 تحلیل کانال تلگرام Java: fill the gaps
کانال Java: fill the gaps (@java_fillthegaps) در بخش زبانی روسی بازیگری فعال است. در حال حاضر جامعه شامل 12 552 مشترک است و جایگاه 10 101 را در دسته فناوری و برنامهها و رتبه 52 755 را در منطقه روسيا دارد.
📊 شاخصهای مخاطب و پویایی
از زمان ایجاد در невідомо، پروژه رشد سریعی داشته و 12 552 مشترک جذب کرده است.
بر اساس آخرین دادهها در تاریخ 05 ژوئن, 2026، کانال فعالیت پایداری دارد. در ۳۰ روز گذشته تغییر اعضا برابر -49 و در ۲۴ ساعت گذشته برابر -4 بوده و همچنان دسترسی گستردهای حفظ شده است.
- وضعیت تأیید: تأیید نشده
- نرخ تعامل (ER): میانگین تعامل مخاطب 34.71% است و در ۲۴ ساعت نخست پس از انتشار، محتوا معمولاً N/A% واکنش نسبت به کل مشترکان کسب میکند.
- دسترسی پستها: هر پست به طور میانگین 0 بازدید دریافت میکند. در اولین روز معمولاً 0 بازدید جمعآوری میشود.
- واکنشها و تعامل: مخاطبان بهطور فعال حمایت میکنند؛ میانگین واکنش به هر پست 0 است.
- علایق موضوعی: محتوا بر موضوعات کلیدی مانند redis, hashmap, linkedhashmap, индекс, фича تمرکز دارد.
📝 توضیح و سیاست محتوایی
نویسنده این فضا را محل بیان دیدگاههای شخصی توصیف میکند:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
به لطف بهروزرسانیهای پرتکرار (آخرین داده در تاریخ 07 ژوئن, 2026)، کانال همواره بهروز و دارای دسترسی بالاست. تحلیلها نشان میدهد مخاطبان بهطور فعال با محتوا تعامل دارند و آن را به نقطه اثرگذاری مهم در دسته فناوری و برنامهها تبدیل کردهاند.
List list = new ArrayList(); process(user, list);Во входной параметр
list потом запишется результат. Такой стиль часто идёт из учебных заданий по алгоритмам, где экономится каждый бит и максимально сокращается количество объектов.
В системах со сложной бизнес-логикой такой подход усложняет чтение, тестирование и возможную параллелизацию.
Как лучше: использовать выходные параметры, не менять входные данные, давать осмысленные имена методам:
List orders = getOrders(user);2️⃣ Сложные универсальные методы Задача: по-разному считать скидку для новых пользователей и пользователей с картами лояльности. Новички часто используют принцип Don't Repeat Yourself на максималках и пишут универсальный метод с кучей параметров и десятком if внутри:
getDiscount(user, true, true, limit, true)Как лучше: сфокусированные методы для разных ситуаций
getDiscountNew(user); getDiscountLoyal(user, limit)3️⃣ Длинные методы Сложно читать и тестировать, страшно менять. 4️⃣ Любовь к статическим методам Как лучше: небольшие методы, связанные с конкретным классом. Связность ниже, ошибок меньше. 5️⃣ Сложное проектирование Задача: завести три типа пользователей: новые, обычные и VIP. Новички скорее всего сделают интерфейс, 3 класса и статический класс с фабричными методами и билдером. Как лучше: как можно проще. Например, один класс пользователя с полем Тип. Усложнять при реальной необходимости PS Все "как лучше" не всегда лучше. Но думаю, идея понятна. 6️⃣ Нулевое или минимальное покрытие тестами как следствие больших сложных методов и недостаточной инкапсуляции. 7️⃣ Низкий уровень ответственности Пункт не относится к разработке, но очень актуален для начинающих. Проявляется в двух формах: 🔸 Непонятно, что происходит. Человек сидит и молчит до последнего, пока не спросишь статус задачи или про возможные трудности. Он умалчивает проблемы или переносит на других: — Что с задачей, которую я тебе дала 3 дня назад? — Я не понял, куда смотреть, потом меня HR позвал бумаги подписывать, потом я настраивал гит, увидел другую задачу и переключился на неё. 🔸 Код не решает проблему, а заметает симптомы: — Приходил пустой параметр, и я выставил дефолтный. Тесты мешали сделать пул-реквест, и я их отключил. 8️⃣ Слабые коммуникативные навыки — Как ты починил баг с расчётом ставки? — Там через геттер фабричный метод нашёл, и потом докер с постгрёй поднял посмотреть, в логах был фильтр урезанный, я письмо отправил тебе в цц, но вроде скоуп не тот или тот, короче, запушил — В чём была ошибка? — Там два двоеточия вылезло — Где? — В дебаге — 🤯 Эти ошибки ожидаемы в начале работы, я тоже их совершала🙂 Чем быстрее вы перестроитесь на командный стиль разработки, тем вероятнее пройдёте испытательный срок и быстрее вольётесь в проект.
public void saveOrder(…) {
kafkaTemplate.send("orders", new OrderCreated(…));
}
Сервис прекращает быть "главным" и становится обычным потребителем событий. Получает сообщение — делает запись в БД. Другие сервисы тоже получают сообщение и что-то делают.
В случае проблем с сетью стратегия сервиса такая:
1️⃣ Отправь сообщение, пока не получится
2️⃣ Получив сообщение, пиши в БД, пока не получится
Вместо координации двух компонентов мы добиваемся исполнения гарантий на каждом шаге.
✅ Доступны все возможности Kafka: роутинг, разные гарантии доставки, репликация сообщений
✅ БД не становится узким местом
❌ Самое долгое время между отправкой сообщения и сохранением в БД среди всех вариантов
❌ Возможна неконсистентность данных и нарушение порядка изменений
Другие сервисы могут получить сообщение об изменениях раньше, чем "основной" сервис.
Если развивать вариант "убираем БД" дальше, придём к Event Sourcing. Это подход, в котором основной источник данных — события, а не БД. Но это уже другая история🧚♀️
Вариант 3: добавляем координатор
Оставляем БД исходную задачу по хранению данных, а для отслеживания изменений добавляем отдельный компонент. Общая схема выглядит так:
🔸 В базе данных что-то меняется
🔸 Координатор обнаруживает это изменение
🔸 Транслирует изменение в кафку
База может записывать изменения в отдельную таблицу с помощью триггера, materialized view или логической репликации. Координатор может смотреть на таблицу, а может читать логи БД.
Всё это многообразие объединяется термином Change Data Capture (CDC).
✅ Чёткие зоны ответственности. Postgres хранит данные, Kafka шлёт сообщения, координатор координирует
✅ Огромная гибкость, множество вариантов реализации
❌ Возможна дупликация сообщений
❌ Сложность системы увеличивается
Теперь у нас не 2 компонента (БД и Kafka), а гораздо больше. Больше инстансов, настроек, мониторинга и потраченных человеко-часов.
Реализация
Большинство инструментов базируются на Kafka Connect, в том числе популярная CDC система Debezium. Здесь и остановимся, пока всё просто и понятно:) Оценить, насколько глубока кроличья нора, можно в документации Debezium.
Резюме
Для слаженной работы Postgres и Kafka есть три основных подхода. Они отличаются
✍️ гарантиями доставки сообщения
✍️ нагрузкой на БД
✍️ временем между сохранением в БД и доставкой сообщения
✍️ сложностью
✍️ дополнительными возможностями
Выбираем решение по приоритетам, требованиям и нагрузке и ставим огоньки хорошему посту🔥public Order saveOrder(…) {
Order saved = orderRepo.save(…);
kafkaTemplate.send("orders",new OrderCreated(…));
}
Другие сервисы, подписанные на orders, получат сообщение и что-то сделают. Посчитают скидки, обновят статистику, запишут заказ в свою БД и тд.
В чём проблема?
Мы обращаемся к двум отдельным компонентам — базе данных и брокеру сообщений. Каждый из них в любой момент может отвалиться, например, пропадёт связь по сети. В зависимости от порядка строк в saveOrder возможны 2 негативных исхода:
😢 запись в базу сделали, сообщение не отправили
😢 отправили сообщение, но запись в БД не прошла
Получим несоответствие. Поэтому иногда хочется, чтобы события выполнились атомарно: либо оба успешно завершаются, либо ни одно из них.
Большинство разработчиков нетерпеливо скажут: "Что тут думать, нужен transaction outbox!!1". Но если спросить 10 человек, что они под этим понимают, получится 10 разных ответов.
В лучших традициях канала обсудим всё простыми словами:) Очень грубо все решения можно назвать так:
1️⃣ Убираем кафку
2️⃣ Убираем БД
3️⃣ Добавляем координатор
Сегодня рассмотрим первый вариант, в следующем посте — остальные два.
Вариант 1: убираем кафку
У Postgres есть механизм notify/listen, который отправляет уведомления заинтересованным лицам. И вместо отправки сообщений через кафку мы возьмём механизм подписки внутри БД.
База становится единственным компонентом и выполняет оба действия (сохранить в таблицу, уведомить заинтересованных) в одной транзакции.
Чтобы не решать проблемы с координацией двух компонентов, мы переложили всю работу на один.
✅ Образцовая транзакция: атомарность и доставка exactly once
✅ Минимальная задержка между сохранением в базу и уведомлением
❌ Ограниченная функциональность уведомлений
❌ Размытие ответственности — часть уведомлений делает Kafka, часть — Postgres
❌ Увеличение нагрузки на БД
Последний пункт — главный ограничитель, поэтому подход "база делает всё" не очень популярен.
Реализация
Можно взять spring-integration-jdbc и для отправки сообщений, и для получения уведомлений. Документация максимально скудная, дополнительные детали есть в этой статье (под VPN)
В следующем посте обсудим ещё 2 варианта🔥ThreadLocal<Integer> value;и для каждого потока будет своё независимое значение value. В бизнес-логике это редко нужно, но фреймворки активно пользуются этим классом. Spring Security использует ThreadLocal для хранения информации о текущем пользователе. Давайте на этом кейсе посмотрим недостатки ThreadLocal, и что предлагает ScopedValue. Как работает секьюрити: 1️⃣ Когда приходит новый запрос, Spring вытаскивает информацию о пользователе и записывает в ThreadLocal переменную:
public static ThreadLocal<Principal> PRINCIPAL = …
void serve(Request request, Response response) {
…
var principal = ADMIN;
PRINCIPAL.set(principal);
…}
2️⃣ Бизнес-логика. В любом месте кода можно узнать, кто выполняет запрос:
var principal = PRINCIPAL.get();Обычно каждый запрос обрабатывается в своём потоке, поэтому данные между запросами не пересекаются. 3️⃣ В конце работы с запросом удаляем информацию из ThreadLocal переменной Что в итоге: ✅ Не надо передавать Principal в параметрах ❌ Надо явно очищать значение ThreadLocal переменной в конце работы ❌ В любом месте можно вызвать set/remove и всё сломать ❌ Подход несовместим с виртуальными потоками Scoped Value намерен решить проблемы выше. Как это выглядит:
public static ScopedValue<Principal> PRINCIPAL = …
void serve(Request request, Response response) {
…
var principal = ADMIN;
ScopedValue.where(PRINCIPAL, principal)
.run(() -> process(request, response));
…}
Переменная PRINCIPAL со значением principal будет доступна только внутри конкретного вызова метода process. Достать значение внутри process:
var principal = PRINCIPAL.get();
Кроме run есть метод call, который возвращает значение из переданной функции:
var result = ScopedValue.where(Server.PRINCIPAL, guest) .call(() -> getResult());Сначала кажется, что для java синтаксис Scoped Value очень необычный — как будто переменная главнее основного действия. Но такое в java уже есть, вспомните try-with-resources. Что получаем: ✅ Видимость переменной задаётся для конкретного вызова метода ✅ У ScopedValue нет метода set, переменную нельзя обнулить/поменять внутри блока ✅ Код совместим с виртуальными потоками Что вызывает вопросы: 🤔 Сценарии использования Неизменяемый аналог ThreadLocal, совместимый с Project Loom точно нужен, но не вижу смысла задавать область видимости настолько гранулярно 🤔 Нельзя использовать несколько ScopedValue без использования вложенности. Хотя это легко реализовать по аналогии с try-with-resources ⚒ Где использовать: пока вижу только как замену ThreadLocal при переходе на виртуальные потоки. Фича сейчас в стадии превью, посмотрим, как она будет развиваться. Если будет, конечно:)
getFirst(), getLast(), reversed() и другие. Подробный пост тут
Интересные preview фичи:
🔹 String templates — интерполяция строк, возможность писать
String str = "Hello, ${name}!";
Подробнее в этом посте
🔹 Scoped Values — аналог ThreadLocal c ограниченной областью действия
🔹 Structured Concurrency — способ организации подзадач в многопоточной среде
Остальные фичи очень специфичные и вряд ли пригодятся большинству разработчиков:
▫️ Generational ZGC — в сборщик добавили поколения, как следует из названия. Молодые объекты будут собираться чаще, и сборщик будет работать эффективнее
▫️ Record Patterns — records можно использовать внутри case
▫️ Foreign Function & Memory API (Third Preview) — методы для работы с нативным кодом и управлению памятью за пределами JVM. Это нужно для приложений, которые хотят сами управлять размещением объектов в памяти и не зависеть от сборщика мусора.
▫️ Unnamed Patterns and Variables (Preview) — можно не указывать имя переменной, если оно не нужно. Вместо него ставить _
Например, в case:
case Point(int x, _) → …Или при ловле исключения:
catch (IllegalStateException _)Было бы удобно сделать такое для лямбд, но для этого есть отдельный JEP, который сделают чёрт знает когда ▫️ Deprecate the Windows 32-bit x86 Port for Removal — перестать работать с Windows 32-bit x86, в будущем удалить ▫️ Prepare to Disallow the Dynamic Loading of Agents Агент — компонент, который изменяет классы при загрузке или меняет уже загруженные классы в JVM. Используется для мониторинга, профайлинга и других служебных целей ▫️ Key Encapsulation Mechanism API — методы для защиты ключей симметричного шифрования ▫️ Unnamed Classes and Instance Main Methods (Preview) Видимо чтобы короче писать Hello World:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
Вот такой релиз. Конечно, самая ожидаемая фича — это виртуальные потоки. Если планируете в ближайшее время внедрять их, отпишитесь по впечатлениям, мне очень интересно😊if (obj instanceof String) {
String str = (String) obj;
// используем str
}
Теперь эти операции объединены. Рядом с instanceOf объявляем имя переменной и сразу ей пользуемся:
if (obj instanceof String str) {
// используем str
}
Тонкий момент— переменная определяется только при успешном выполнении instanceOf, поэтому есть нюанс с областью видимости.
В логическом И можно использовать переменную в том же if:
✅ if (obj instanceof String s && s.length() > 5)
Логическое ИЛИ такого не позволяет, будет ошибка компиляции:
❌ if (obj instanceof String s || s.length() > 5)
2️⃣ Компактный switch
У древнейшей конструкции доступен новый синтаксис:
▫️ Стрелочка вместо двоеточия
▫️ Break в конце case по умолчанию, и его можно не писать
▫️ Можно объединить несколько case в один
Старый синтаксис никуда не делся, им можно пользоваться:
switch (value) {
case 1:
case 3: println("Odd"); break;
case 2:
case 4: println("Even"); break;
default: println("Other");
}
В "новом стиле" это будет так:
switch (value) {
case 1,3 -> println("Odd");
case 2,4 -> println("Even");
default -> println("Other")
}
Важный момент — break добавляется по умолчанию только в вариант "со стрелочками". В "двоеточиях" break всё ещё добавляется вручную.
🔥 Ответ на вопрос перед постом: выведется Even Other Even. Чтобы работало как надо, нужно переписать на стрелочки или добавить break.
3️⃣ Присвоение элемента через switch
int value = switch(…) {…};
4️⃣ Проверка в switch по типам и сложные case
Раньше в case принимались только константы. Теперь можно проверить тип переменной:
switch (obj) {
case Integer i -> …
case Long l -> …
case Double d -> …
}
Если произошёл мэтч, для переменной сразу доступна доп. информация, и можно добавить условия в case:
switch (response) {
case String s when s.length() == 10 -> …
}
А теперь самое интересное! Обсудим, чего в текущей реализации нет:
❌ Нет работы с массивами
Один из кейсов pattern matching — работа с набором данных, в которых мы ищем определённые признаки. Такого в java пока нет:
double discount = switch(transaction) {
case ["vip", Long, _, _, Double sum] → sum*0,9
case _ → 0
}
В других языках такой функционал есть. Например, List patterns в С# или Matching sequences в питоне.
❌ Нет паттернов по полям класса
Под капот спрятан только instanceOf, остальные условия выглядят как в обычном if. Вся работа с полями происходит через методы:
switch (figure) {
case Square s when s.getLength() != 10 → …
}
В том же питоне запись гораздо компактнее:
match point: case (0, 0): … case (0, y): … case _: …JEP Record Patterns мог быть как раз об этом, ведь records позиционируются как лаконичные контейнеры данных. Но увы. Резюме В текущем виде pattern matching выглядит слабо, особенно по сравнению с другими языками. Покрыты только базовые кейсы, в таком виде область применения очень ограничена. Возможно это лишь промежуточный этап. Посмотрим, как фича будет развиваться и использоваться✨
[Object, Object, Object].
Паттерн — схема того, что мы ищем. Паттерн для поиска координат может выглядеть так:
▫️ [Double, Double] — ищём массив из двух чисел с плавающей запятой
▫️ [(-90;90), (-180,180)] — массив с двумя числами в указанных диапазонах
Дальше идём по набору данных и проверяем их на соответствие паттерну.
Традиционно для такой задачи используется связка if + instanceOf. Вариант рабочий, но читаемость ужасная.
Вот что нужно написать, чтобы проверить, является ли координатами массив [Object, Object] data:
if (data.length == 2 && data[0] instanceOf Double && data[1] instanceOf Double) {
double n = (Double) data[0];
double e = (Double) data[1];
if (n ≥ -90 && n≤ 90 && e ≥ -180 && e ≤ 180) {
// что-то делаем
}
}
В pattern matching многие проверки убираются под капот, и код выглядит симпатичнее:
switch(data) {
case [(-90; 90), (-180, 180)]:
// что-то делаем
}
Близкий родственник паттерн матчинга — регулярные выражения. Строка — это набор символов, внутри этого набора ищутся паттерны. Можно сделать ту же работу через if, но регулярка удобнее. Плюс в строке элементы однородные (символы), а паттерн матчинг работает с разными типами данных.
Ещё пример:
double discount = switch(transaction) {
case ["vip", Long, _, _, Double sum] → sum*0,9
case _ → 0
}
Здесь ищем список из 5 элементов, где первый — строка "vip", второй и пятый — число. Если нашли — можно сразу работать с полем sum. Если не нашли — используем паттерн по умолчанию _
С if-ами эта конструкция будет гораздо объёмнее
Резюме
✅ Pattern matching нужен, когда мы пытаемся найти что-то знакомое в слабо- или неструктурированных данных.
✅ Большинство instanceOf и if отправляются под капот, и мы получаем более компактный код.
✅ Разумеется, применить pattern matching можно и в других сценариях, но здесь видится наибольший профит в корпоративном царстве ООП.
В следующем посте распишу возможности pattern matching конкретно в джаве.
Спойлер: пока не впечатляет😑SequencedCollection. В него войдут методы, которые должны были появиться в джаве ещё в 98 году. Простые операции, для которых каждый раз пишется маленький велосипедик🚲
♨️ Пример 1: получить последний элемент в списке
Сейчас это так:
last = list.get(list.size() - 1);
В java 21 наконец-то появится специальный метод:
last = list.getLast();♨️ Пример 2: пройти список в обратном порядке Сейчас это так:
for(int i=list.size(); i>0; i--){
int value = list.get(i));
}
Выглядит жутко. Альтернатива — использовать Collections.reverse:
List<Integer> reversed = new ArrayList<>(list); Collections.reverse(reversed); reversed.forEach(…);Выглядит симпатичнее, но здесь море лишних действий: создаём новую(!) коллекцию, переставляем её элементы и только потом делаем обход. В java 21 всё гораздо проще:
list.reversed().forEach(…)Метод
reversed не меняет исходную коллекцию и возвращает view с обратным порядком обхода.
♨️ Пример 3: обойти LinkedHashSet
LinkedHashSet — список с уникальными элементами. Хотя это список, класс реализует только интерфейс Set. Поэтому работы с индексами нет вообще.
Получить первый элемент ещё можно:
first = linkedHashSet.iterator().next();А вот последний — никак, надо полностью обходить структуру. Код писать не буду, слишком громоздкий. В java 21 те же операции выполняются легко и просто:
first = linkedHashSet.getFirst(); last = linkedHashSet.getLast();Резюме В java 21 появится интерфейс SequencedCollection с методами ▫️
SequencedCollection<E> reversed()
▫️ void addFirst(E)
▫️ void addLast(E)
▫️ E getFirst()
▫️ E getLast()
▫️ E removeFirst()
▫️ E removeLast()
Плюс интерфейсы SequencedSet и SequencedMap с тем же функционалом.
Новые методы появятся в ArrayList, LinkedList, HashSet, LinkedHashMap, LinkedHashSet, частично в TreeSet и некоторых других классах.
Было бы здорово увидеть эти методы 25 лет назад, но лучше поздно, чем никогда:)@Configuration
@Profile(”dev”)
public class DBConfiguration {…}
А вот это конфиг:
-Dspring.profiles.active=devЗадача разработчика простая: отделить конфигурацию от основного кода. Тогда приложение легко запустить и настроить на работу в разных средах. Где хранить конфиги? Оригинальный документ категоричен: конфиг должен передаваться через environment переменные, а конфиг-файлы не должны существовать. Потому что: 🔸 Рано или поздно сервис с продакшн конфигами попадёт в лапки разработчиков, и они сделают delete table users 🔸 Файлы с конфигами расползаются по всей системе, это небезопасно и сложно в управлении 🔸 Environment переменные не зависят от ОС, фреймворка и языка разработки Мотивация понятна, но на практике всё работает не так:) Конфиги группируются в файлы, а в гите часто хранится дефолтный файлик для разработки. Конфиги могут лежать в другом сервисе, например, в Kubernetes, Zookeeper или даже в HashiCorp Vault. Последний поддерживает версионирование и следит, кто и когда запрашивал данные. Для ежедневной разработки есть следующие best practices: ✅ В параметрах конфига нет префикса среды выполнения, а в коде — логики по их разделению:
❌ @Value("dev.datasource")
✔️ @Value("datasource")
✅ В коде нет констант вроде "8080", "admin", имён хостов, логинов и паролей
Кажется, что это само собой разумеется, но нет:) В 2020 году утекли личные данные 243 миллионов бразильцев, потому что пароль БД был записан константой в исходном коде сервиса минздрава. А согласно этому исследованию более 100к GitHub репозиториев содержат токены и криптографические ключи прямо в исходном коде. Будем надеяться, что это всё pet проекты, которые нигде не используются🤞
اکنون در دسترس! پژوهش تلگرام ۲۰۲۵ — مهمترین بینشهای سال 
