Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Больше📈 Аналитический обзор Telegram-канала Java: fill the gaps
Канал Java: fill the gaps (@java_fillthegaps) языкового сегмента Русский является активным участником. Сейчас сообщество объединяет 12 552 подписчиков, занимая 10 101 место в категории Технологии и приложения и 52 755 место в регионе Россия.
📊 Показатели аудитории и динамика
С момента создания невідомо проект демонстрирует стремительный рост, собрав аудиторию из 12 552 подписчиков.
Согласно последним данным от 05 июня, 2026, канал показывает стабильную активность. За последние 30 дней изменение числа участников составило -49, а за последние 24 часа — -4, при этом общий охват остаётся высоким.
- Статус верификации: Не верифицирован
- Уровень вовлечённости (ER): Средний показатель вовлечённости аудитории составляет 34.71%. В первые 24 часа после публикации контент обычно набирает N/A% реакций от общего числа подписчиков.
- Охват публикаций: В среднем каждый пост получает 0 просмотров. В течение первых суток публикация набирает 0 просмотров.
- Реакции и взаимодействия: Аудитория активно поддерживает контент: среднее количество реакций на один пост — 0.
- Тематические интересы: Контент сосредоточен на ключевых темах, таких как redis, hashmap, linkedhashmap, индекс, фича.
📝 Описание и контентная политика
Автор описывает ресурс как площадку для выражения субъективного мнения:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Благодаря высокой частоте обновлений (последние данные получены 07 июня, 2026) канал поддерживает актуальность и высокий уровень охвата публикаций. Аналитика показывает, что аудитория активно взаимодействует с контентом, что делает его важной точкой влияния в категории Технологии и приложения.
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
Работает пока только для Linux, иногда показывает небольшой прирост скорости. Так что имеет смысл попробовать!
В GraalVM есть ещё одно интересное направление, и о нем расскажу в следующем посте🔥Курс великолепный, не пожалел ни одного рубля, что потратил. Это уникальный курс в своем сегменте, особенно на русском рынке. Всем советую, на курсе вы найдете все ответы на интересующие вас вопросы. Более того из курса вы сможете узнать то, что просто нет в открытом доступе нигде, исключительно авторские наработки. Однозначно советую всем бэкэнд разработчикам, даже если вы не особо используете многопточку - это очень поможет вам в понимании многопоточного кода фреймворков и вообще сильно улучшит ваш кругозор.Так что с многопоточкой всё давно решено, все ссылки в закрепе. Остальные ответы ещё разгребаю, буду писать посты по мере сил. Ещё раз спасибо, очень ценю вашу помощь❤️
SELECT * FROM t ORDER BY …
LIMIT 100
Для второй и последующих добавляем offset:
SELECT * FROM t ORDER BY …
LIMIT 100 OFFSET 100
Offset показывает, сколько строк пропустить.
В чем подвох: кажется, что при offset 100 постгрес сразу прыгнет на 101 строку, но это не так. Он честно пройдет 100 строк, и потом отдаст следующие 100.
❌ В случае большого оффсета такая операция будет работать долго даже при наличии индекса
✅ Просто реализовать. Минимум действий со стороны клиента и поддержка в фреймворках.
🌸 Keyset пагинация. Она же cursor, она же seek.
Здесь мы используем where в паре с индексом и сразу прыгаем на нужную позицию. Первую страницу получаем так:
SELECT * FROM t ORDER BY name
LIMIT 10
Если записи отсортированы по имени, запоминаем имя последнего элемента. Пусть это будет х. Чтобы получить вторую страницу, добавляем where:
SELECT * FROM t ORDER BY name
WHERE name > x
LIMIT 10
✅ Работает быстрее, чем вариант с оффсетом. Чем больше оффсет, тем значительнее разница
❌ Сложнее в реализации, надо запоминать поле в последней пачке
❌ Нельзя перейти к произвольной странице, только последовательная выдача данных
Когда что использовать?
Во многих докладах/статьях часто встречается мысль, что оффсет это ужас, и надо всегда использовать кейсет. На практике все не так категорично:
▫️ Если на сайте infinite scrolling или пользователь вряд ли будет много смотреть, вполне ок использовать простую оффсет пагинацию.
▫️ Если данные выкачиваются большими пачками для обработки, аналитики, миграции и тд, берём keyset пагинацию.
▫️ Если надо выгрузить много данных и прыгать по ним туда-сюда или у вас другой сложный случай, придется экспериментировать с индексами и запросами. Этот доклад хорошо показывает, что пагинация - далеко не тривиальная задача.
А поставить огонек качественному контенту очень просто, поэтому это нужно обязательно сделать🔥public int compare(T o1, T o2) {…}
Если метод вернул
▫️ число больше нуля — первый элемент больше второго
▫️ 0 — элементы равны
▫️ число меньше нуля — первый меньше второго
Простейшая и популярная реализация — вычесть одно значение из другого:
(o1, o2) -> (int) (o1.getSum() - o2.getSum())
❓ Что с этим не так?
Я всегда сомневаюсь, что из чего вычитать. Если вы отвечали на опрос дольше одной секунды, значит мы в одном лагере:) Компаратор — совсем не то место, где мозг должен спотыкаться.
В Java 8 в интерфейсе Comparator появился удобный метод:
orders.sort(comparing(Order::getSum))
Что классно:
✅ Не надо вспоминать, что из чего вычитать
✅ Легко сделать сравнение в обратном порядке:
comparing(Order::getSum).reversed()
✅ Можно учесть null:
nullsFirst(comparing(Order::getSum))
nullLast(…)
✅ Удобно сортировать по нескольким полям:
comparing(Order::getSum).thenComparing(Order::getId)
Самостоятельно обрабатывать null и писать сложные сортировки очень утомительно. Помню, как с удовольствием удаляла из проекта компараторы на 20 строк после перехода на Java 8😊
Важные нюансы:
1️⃣ comparing*
В интерфейсе Comparator также доступны методы comparingInt, comparingLong и comparingDouble. Используются для полей примитивного типа, чтобы избежать лишнего боксинга. Если в классе Order
Long id → используем comparing(Order::getId)
long id → comparingLong(Order::getId)
Не указывайте тип лишний раз. Для работы с объектами подойдёт обычный comparing
2️⃣ Нетривиальная работа с null*
В обычных методах легко понять, что происходит:
comparing(A).reversed().thenComparing(Б)
=
отсортировать по полю А в обратном порядке, дубликаты отсортировать по Б
Методы null* выбиваются из этой схемы.
nullsFirst(comparing(Order::getSum))
означает, что первыми будут null объекты, а существующие заказы отсортируются по сумме. Этот компаратор работает для такого кода:
orders.add(null); // эти элементы будут впереди
orders.add(new Order(…)); // эти отсортируются по полю sum
Если в списке нет null объектов, но в поле sum возможен null, придётся писать так:
…comparing(Order::getSum, nullsFirst(naturalOrder()));
Сравнение по нескольким nullable полям выглядит совсем плохо. К счастью, на практике такие задачи встречаются редко.
Ответ на вопрос перед постом:
(o1, o2) -> (int) (o2.getSum() - o1.getSum())
Но лучше использовать comparing(Order::getSum).reversed() ✨void sort(Comparator<?super Е> c) {…}
Для элементарной операции сортировки чисел приходится писать
list.sort(Comparator.naturalOrder())
Код с Comparator.naturalOrder() похож на какой-то костыль. Под капотом не происходит ничего особенного, реализация компаратора очень простая:
(с1, с2) -> c1.compareTo(c2)
❓ Так зачем писать так сложно? Почему в интерфейсе List нет метода sort()?
Сейчас расскажу:)
Java создавался как язык для больших и долгоживущих приложений, и его основные ценности — стабильность и обратная совместимость.
C начала 2000-х в JDK есть метод Collections.sort(List). Статический метод, который меняет внутреннее состояние аргумента. Сейчас это порицается, но в те времена было норм.
В больших компаниях классы JDK часто расширяли удобными методами, в том числе сортировкой в функциональном стиле:
CustomList sorted = list.sort();
Спустя много лет стало понятно, что экземплярные методы сортировки — это классно, и надо добавить такой метод в JDK. Чтобы текущие реализации списков не сломались, это должен быть дефолтный метод в интерфейсе List.
Но есть проблема. Допустим, на проекте есть такой класс:
public class CustomList implements List {
public CustomList sort() {…}
}
Допустим, в java 8 в интерфейс List добавили бы метод
default void sort() {…}
Старый метод не может переопределить дефолтный. тк возвращаемые значения не совместимы. Поэтому проекты, которые определили свой функциональный sort в начале 2000-х, перестанут компилироваться. Пользователи будут недовольны😡
Многие проекты полагаются на свой sort, поэтому разработчики JDK не стали добавлять его в интерфейс. Метод sort(Comparator) использовался редко, поэтому теперь он с нами.
У Stream API нет проблем с совместимостью, так что для стримов есть прекрасный метод sorted(). Для коллекций метод sorted() есть в Kotlin💖
(обратите внимание на суффикс -ed, всё по правилам функционального подхода)
Ответ на вопрос перед постом: отсортировать список можно так:
✅ list.sort(Comparator.naturalOrder());
✅ list = list.stream().sorted().toList();
Если вам понравился list.sort(), значит у вас хороший вкус на API. К сожалению, у java свои загоны, поэтому этого метода в JDK нет.WITH cte_name AS (
SELECT …
)
SELECT … FROM cte_name;
cte_name используется как источник данных
⭐️ VIEW и MATERIALIZED VIEW выглядят похоже:
CREATE (MATERIALIZED) VIEW view_name AS
SELECT … ;
Полученное вью также используется как источник данных:
SELECT … FROM view_name;
В чём же разница?
1️⃣ Что именно хранится
VIEW — это просто сокращение запроса, результат выполнения не сохраняется. При каждом FROM view_name запрос выполняется заново.
MATERIALIZED VIEW сохраняет результат на момент выполнения. По сути создаётся временная таблица с копией данных.
Важный момент: если исходные данные поменяются, они не повлияют на данные в MATERIALIZED VIEW. Для актуализации данных надо отдельно вызвать команду REFRESH.
СТЕ как MATERIALIZED VIEW сохраняет результат выполнения и считается один раз.
2️⃣ Видимость
CTE не существует сам по себе, за ним обязательно должен следовать запрос, который его использует. Можно сказать, что область видимости и время жизни CTE — один запрос.
VIEW и MATERIALIZED VIEW доступны на уровне схемы, можно пользоваться много раз из разных мест. Удалять вью надо явно командой DROP.
Это основные отличия. Есть ещё несколько, но не будем углубляться:)
Примерные кейсы использования:
🔧 CTE — сделать сложные запросы более читаемыми
🔨 View — синоним для популярного запроса на выборку
🪛 Materialized view — снимок данных, которые долго считать с нуля, но к которым много обращений
Теперь закрепим знания из этого и предыдущего поста небольшой задачкой✍️
Есть 2 таблицы:
🔸 from_table со столбцами id, firstname, lastname.
🔸 to_table со столбцами id, name
Задача: перенести все строки из from_table в to_table, соединив firstname и lastname в одно поле name. После выполнения запроса from_table должен стать пустым.
Попробуйте выполнить задачу ОДНИМ запросом.
Онлайн Postgres: pgplayground
Исходный код для экспериментов:
CREATE TABLE from_table(id int, firstname text, lastname text);
INSERT INTO from_table VALUES(1, 'F1', 'L1');
CREATE TABLE to_table(id int, name text);
✨ РЕШЕНИЕ ✨
Вспоминаем, что DELETE возвращает удалённые строки. Формируем из них СТЕ и передаём в INSERT:
WITH deleted_rows AS (
DELETE FROM from_table
RETURNING id, firstname || ' ' || lastname
)
INSERT INTO to_table
SELECT * FROM deleted_rows;SELECT * FROM table1
INNER JOIN table2 USING (user_id);
2️⃣ INSERT + SELECT
Чтобы вставить в одну таблицу значения из другой, нет смысла вытаскивать данные отдельно. INSERT и SELECT прекрасно комбинируются:
INSERT INTO users (id, name)
SELECT user_id, fullname FROM customers;
3️⃣ RETURNING
Чтобы после вставки вернуть новые строки, отдельный SELECT не нужен. Добавьте RETURNING для нужных столбцов:
INSERT INTO users VALUE (…)
RETURNING id;
Обычно возвращают id, но можно вернуть несколько столбцов и даже *.
Для DELETE RETURNING тоже работает и возвращает удалённые строки.
4️⃣ Тестовые данные
Для генерации простейших тестовых данных через SQL вам пригодятся:
🔸 generate_series(a, b) — последовательность целых чисел от a до b. Той же функцией генерятся даты в заданном диапазоне, синтаксис смотрите в документации
🔸 random() — случайное число от 0 до 1
🔸 md5(а) — хэш числа а. Помогает превратить результат random() в строку
Дальше комбинируем. Например, так:
INSERT INTO t
SELECT id, // 1, 2,…, 100
'name' || id, // name1,…, name100
random(),
md5(random()::text)
FROM generate_series(1,100) id;
Одним запросом получаем 100 строк в базе!
Приёмы выше хоть и простые, но пригодятся на большинстве проектов. Пользуйтесь и ставьте огонёчек, если нужна вторая часть🔥Ctrl + F12
Быстро найти нужный метод или узнать, что вообще умеет класс
🚀 Найти класс или файл в проекте: Shift-Shift
Откроется строка поиска, можно ввести начало имени или аббревиатуру класса
🚀 Перейти к определению: Ctrl + B
Для переменных — переходит к месту, где она была объявлена, для методов — к их реализации
🚀 Вернуться в предыдущий класс:
Ctrl + Alt + ⬅️ Ctrl + Alt + ➡️IDEA хранит небольшую историю перемещений, по которой можно перемещаться стрелками. Так очень удобно править несколько связанных файлов 🚀 Найти строку по номеру:
Ctrl + G
Когда коллега пишет: "проверь условие в строке 850", можно не проматывать огромный класс, а быстро перейти на нужную строку
Очень удобные шорткаты, обязательно попробуйте🔥
Уже доступно! Исследование Telegram 2025 — ключевые инсайты года 
