Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Show more📈 Analytical overview of Telegram channel Java: fill the gaps
Channel Java: fill the gaps (@java_fillthegaps) in the Russian language segment is an active participant. Currently, the community unites 12 544 subscribers, ranking 10 113 in the Technologies & Applications category and 52 793 in the Russia region.
📊 Audience metrics and dynamics
Since its creation on невідомо, the project has demonstrated rapid growth, gathering an audience of 12 544 subscribers.
According to the latest data from 10 June, 2026, the channel demonstrates stable activity. Although there has been a change in the number of participants by -50 over the last 30 days and by 1 over the last 24 hours, overall reach remains high.
- Verification status: Not verified
- Engagement rate (ER): The average audience engagement rate is 34.74%. Within the first 24 hours after publication, content typically collects N/A% reactions from the total number of subscribers.
- Post reach: On average, each post receives 0 views. Within the first day, a publication typically gains 0 views.
- Reactions and interaction: The audience actively supports content: the average number of reactions per post is 0.
- Thematic interests: Content is focused on key topics such as redis, hashmap, linkedhashmap, индекс, фича.
📝 Description and content policy
The author describes the resource as a platform for expressing subjective opinions:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Thanks to the high frequency of updates (latest data received on 11 June, 2026), the channel maintains relevance and a high level of publication reach. Analytics show that the audience actively interacts with content, making it an important point of influence in the Technologies & Applications category.
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 проекты, которые нигде не используются🤞
Available now! Telegram Research 2025 — the year's key insights 
