Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
显示更多📈 Telegram 频道 Java: fill the gaps 的分析概览
频道 Java: fill the gaps (@java_fillthegaps) 俄语 语言赛道中的 是活跃参与者。目前社区聚集了 12 558 名订阅者,在 技术与应用 类别中位列第 10 100,并在 俄罗斯 地区排名第 52 754 位。
📊 受众指标与增长动态
自 невідомо 创建以来,项目保持高速增长,吸引了 12 558 名订阅者。
根据 04 六月, 2026 的最新数据,频道保持稳定运转。过去 30 天订阅人数变化为 -43,过去 24 小时变化为 -6,整体触达仍然可观。
- 认证状态: 未认证
- 互动率 (ER): 平均受众互动率为 34.70%。内容发布后 24 小时内通常能获得 N/A% 的反应,占订阅者总量。
- 帖子覆盖: 每篇帖子平均可获得 0 次浏览,首日通常累积 0 次浏览。
- 互动与反馈: 受众积极参与,单帖平均反应数为 0。
- 主题关注点: 内容集中在 redis, hashmap, linkedhashmap, индекс, фича 等核心主题上。
📝 描述与内容策略
作者将该频道定位为表达主观观点的平台:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
凭借高频更新(最新数据采集于 05 六月, 2026),频道始终保持新鲜度与高覆盖。分析显示受众积极互动,使其成为 技术与应用 类别中的关键影响点。
CREATE INDEX CONCURRENTLY idx ON t(f);
Если всё так круто, почему опция не выбрана по умолчанию?
Потому что появляются новые проблемы.
Обычный индекс один раз проходит по таблице, которая не меняется, поэтому успех неизбежен.
Если индекс строится во время активной работы с базой — привет гонки, параллельные транзакции и вся классика многопоточных проблем. В итоге
▫️ Индекс строится гораздо дольше. Скан таблицы происходит 2 раза, индекс постоянно ждёт завершения транзакций и борется с внутренними противоречиями
▫️ Индекс может не получиться и остаться в статусе invalid. В таких случаях надо снести неполучившийся индекс и начать построение заново. Возможно не один раз💔
Второй важный момент, который касается неблокирующих индексов — партиционированные таблицы.
Если добавить обычный индекс для "основной" таблицы, для текущих и будущих(!) партиций индекс создаётся автоматически. Очень удобно
Но для CONCURRENTLY индекса такая схема не работает. Чтобы создать неблокирующие индексы для партиций придётся делать так:
▫️ Создать индекс на "основную" таблицу
▫️ Создать неблокирующий индекс для каждой партиции
▫️ Присоединить эти индексы к "основному"
Примерно так:
CREATE INDEX idx ON ONLY t(f);
CREATE INDEX CONCURRENTLY p1_idx ON p1_t(f);
ALTER INDEX idx ATTACH PARTITION p1_idx;
В общем, при создании индекса для большой таблицы придётся делать выбор:
🪑 CREATE INDEX и заблокировать работу с таблицей на десятки минут
🪑 CREATE INDEX CONCURRENTLY и выполнить кучу дополнительной работы, но меньше затронуть пользователя
Универсального решения нет, выбираем стул в зависимости от ситуации:)spring.task.scheduling.pool.size=5Задачки будут летать параллельно и не блокировать друг друга👯♀️ 🚨Ситуация 2: у сервиса несколько экземпляров а задача по расписанию должна выполниться один раз. Какие решения я видела: 🕳 Отдельный сервис для скедьюлд задач 🕳 Синхронизация через базу или ShedLock, чтобы решить, какой сервис выполнит задачу. На одном проекте для этой цели использовался leader election в Zookeeper🙈 Всё это интересно реализовать и вписать красивую строчку в резюме, но есть способ проще. Смысл в том, чтобы разделить полезную работу и вызов по расписанию. Например: ▫️ Делаем в сервисе нужный метод ▫️ Добавляем контроллер, который его вызывает ▫️ Внешний компонент следит за расписанием и дергает контроллер Кто этот внешний компонент? Кто угодно, в инфраструктуре полно инструментов для регулярных задач. Начиная от CronJobs в Kubernetes до баш скрипта с crontab. Проблемы со Scheduled задачами часто проявляются не сразу и не явно. Если миграция данных в другую систему запустилась слишком поздно, данные в разных системах разойдутся. А если задача вообще не выполнилась? Я такое разгребала неоднократно, поэтому совет от души: когда добавляете задачу по расписанию, учтите 2 момента из этого поста. Сэкономите проекту десятки человекодней🔥
public class HashSet implements Set {
HashMap map;
public HashSet() {
map = new HashMap<>();
}
...
}
Прошло 4 года. В java 1.4 добавились Linked* реализации:
▫️ LinkedHashMap стал наследником HashMap. Тут всё сложилось удачно
▫️ LinkedHashMap стал наследником HashSet. И здесь не всё гладко.
У HashSet внутри HashMap, доступа снаружи к нему нет. А внутри LinkedHashSet должен быть LinkedHashMap. Как заменить объект внутри родителя без изменения существующих методов?
Решение в итоге ужасное. В HashSet добавили такой package private конструктор:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
поле dummy нужно, чтобы этот конструктор отличался от уже существующих.
Только вдумайтесь: в родителя добавили специальный конструктор для конкретного потомка. С его деталями реализации!
Код получился бы чище, если вместо наследования от хэшсета использовать композицию:
class LinkedHashSet implements Set {
private LinkedHashMap map;
public LinkedHashSet() {
this.map = new LinkedHashMap<>();
}
...
}
✅ В HashSet нет лишнего
😑 Нужно скопировать кучу мелких методов типа add, size, contains
На примере LinkedHashSet чётко видна проблема наследования.
У родителя по всем заветам инкапсуляции скрыта реализация. Если наследование изначально не предусмотрено, к внутренним полям родителя нет прямого доступа. Код сложнее переиспользовать, и появляются странные конструкции.
Даже если дети спланированы заранее, зависимость от родителей ограничивает их развитие. Детям приходится подстраиваться и выбирать не самые оптимальные для себя решения.
(этот абзац всё ещё про разработку)
Композиция и в моменте, и в перспективе гораздо удобнее. Но 30 лет назад это было не так очевидно.LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
map.put(2, 2);
int lastKey = map.sequencedKeySet().getLast(); // 2
Если в конструкторе передать accessOrder=true, список запоминает порядок извлечения элементов. Вызов get(1) отправляет 1 в конец списка:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(16, 0.8f, true);
map.put(1, 1);
map.put(2, 2);
map.get(1);
int lastKey = map.sequencedKeySet().getLast(); // 1
С теорией закончили, переходим к интересному.
В первую очередь, бросается в глаза флажок в конструкторе. Гораздо симпатичнее в параметрах выглядел бы enum. Что-то вроде ELEMENT_ORDER.INSERT. Подробно бэд пректис с флажками и альтернативы разбирала тут.
Второй момент касается проектирования. Зачем вообще нужен LinkedHashMap с accessOrder=true?
Документация пишет, что это отличная база для LRU кэша. С первого взгляда похоже на правду, но есть пара нюансов:
▫️ У LinkedHashMap нет ограничений на размер. У кэша — есть
▫️ С кэшами работают много потоков. LinkedHashMap - не потокобезопасен, единственный вариант для корректной работы — synchronized обёртка:
Map map = Collections.synchronizedMap(new LinkedHashMap(…))Получится кэш с пропускной способностью в один поток😐 В итоге: ❌ В чистом виде LinkedHashMap с accessOrder=true нужен либо никому, либо в редких случаях ❌ В качестве LRU кэша (как предлагается в документации) класс использовать сразу не получится. Либо доделывать, либо взять уже готовые и более эффективные реализации кэша. Подобные опции в API - лишние. Хорошие библиотеки и фреймворки состоят из двух частей: базовые многофункциональные элементы + удобные методы для популярных кейсов. Так достигается баланс между простотой и гибкостью. ✨ Хороший пример: экзекьюторы ✨ Базовый элемент для экзекьютора — класс ThreadPoolExecutor. В конструкторе 5 параметров, можно переопределить методы. Есть готовые варианты, которые подойдут для большинства задач:
Executors.newFixedThreadPool(int nThreads) Executors.newSingleThreadExecutor() Executors.newCachedThreadPool()Всё вместе - приятное и удобное апи. Примеры выше простые, но сам подход универсален. Комбо базовых и удобных методов отлично подходит для библиотек интеграции или для общих модулей. Ответ на вопрос перед постом: с флажком true запоминается порядок извлечения элементов. Подписчики канала - умнички независимо от каких-то флажков🥰
CREATE TABLE tweets (
id BIGINT ... PRIMARY KEY,
text VARCHAR(255) NOT NULL,
parent_id BIGINT REFERENCES tweets(id)
);
Код сохранения твита примерно такой:
public Tweet saveTweet(Tweet tweet) {
// проверяем, что родитель существует
if (tweet.getParentId() != null) {
boolean parentExists = tweetRepository.existsById(tweet.getParentId());
if (!parentExists) {
throw new IllegalArgumentException("Parent not exists");
}
}
return tweetRepository.save(tweet);
}
Какую аннотацию ставим над методом saveTweet?PostConstruct или ловят событие ApplicationReadyEvent. Но у таких вариантов есть существенный минус.
Если "прогрев" находится в PostConstruct, как отключить его в тестах? Можно добавить флажок, сделать подкласс и заменить его в тестовом конфиге, но это не всегда помогает и часто выглядит как костыль.
Расскажу более аккуратный способ "прогреть кэши". Однажды точно пригодится💯
Смотрите, SpringApplication.run(...) возвращает полностью готовый контекст. Можно достать из него компонент и вызвать нужный метод "прогрева". В коде выглядит так:
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(MainApplication.class, args);
AccountService accService = ctx.getBean(AccountService.class);
accService.loadDictionary();
}
}
В чём плюс?
В интеграционных тестах с @SpringBootTest метод main не запускается. Соответственно, код внутри не выполняется. Нет костылей вокруг PostConstruct, всё чисто и красиво❤️🩹
Когда в тестах все же нужен "прогрев", добавляем параметр "использовать main метод":
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)
Итого. Если код должен выполниться после старта, но будет мешаться в тестах — пишите его в main. Очень полезный приём🔥class Employee extends Person {
private static int verifyAge(int value) {
if (age < 18)
throw new IllegalArgumentException(...);
return value;
}
Employee(int age) {
super(verifyAge(age));
}
}
С новым JEP эти костыли не нужны, нужные проверки пишем в начале конструктора:
class Employee extends Person {
Employee(int age) {
if (age < 18)
throw new IllegalArgumentException(...);
super(age);
}
}
Области кода вокруг this/super называются очень литературно: пролог и эпилог🥰
public Person {
// prologue
super();
// epilogue
}
В пролог нельзя вставить любой код:
❌ Нельзя обращаться к переменным родителя
❌ Нельзя вызывать нестатические методы
❌ Нельзя вызвать return
✅ Можно присвоить поля текущего класса
Особо не разгуляешься, всё же основной сценарий фичи — валидация входных параметров.
Ещё из интересного:
1️⃣ В JVM не пришлось ничего менять
Потому что правила "this обязательно первый" в JVM нет. Это ограничение только на уровне языка, чтобы упростить работу компилятора:)
2️⃣ Меняется ответ на частый собесный вопрос "в каком порядке инициализируются переменные". Раньше порядок для нестатических полей был такой:
Поля Parent - Конструктор Parent - Поля Child - Конструктор Child
В Java 25 поля наследника можно инициализировать ДО вызова конструктора родителя:
Employee(int age, String officeID) {
this.officeID = officeID;
super(age);
}
Общая схема с этими прологами-эпилогами очень усложняется.
3️⃣ Фича называется Flexible Constructor Bodies. Я не смогла придумать адекватный перевод, поэтому очень интересно, как её переведут в статьях-обзорах:)
现已上线!2025 年 Telegram 研究 — 年度关键洞察 
