Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Ko'proq ko'rsatish📈 Telegram kanali Java: fill the gaps analitikasi
Java: fill the gaps (@java_fillthegaps) Rus til segmentidagi kanali faol ishtirokchi. Hozirda hamjamiyat 12 552 obunachidan iborat bo'lib, Texnologiyalar & Aralashmalar toifasida 10 101-o'rinni va Rossiya mintaqasida 52 755-o'rinni egallagan.
📊 Auditoriya ko‘rsatkichlari va dinamika
невідомо sanasidan buyon loyiha tez o‘sib, 12 552 obunachiga ega bo‘ldi.
05 Iyun, 2026 dagi oxirgi ma’lumotlarga ko‘ra kanal barqaror faollikka ega. Oxirgi 30 kunda obunachilar soni -49 ga, so‘nggi 24 soatda esa -4 ga o‘zgardi va umumiy qamrov yuqori darajada qolmoqda.
- Tasdiqlash holati: Tasdiqlanmagan
- Jalb etish (ER): Auditoriya o‘rtacha 34.71% darajada jalb etiladi. Nashrdan keyingi dastlabki 24 soatda kontent odatda umumiy obunachilar sonining N/A% ini tashkil etuvchi reaksiyalarni to‘playdi.
- Post qamrovi: Har bir post o‘rtacha 0 marta ko‘riladi; birinchi sutkada odatda 0 ta ko‘rish yig‘iladi.
- Reaksiyalar va o‘zaro ta’sir: Auditoriya faol: har bir postga o‘rtacha 0 ta reaksiya keladi.
- Tematik yo‘nalishlar: Kontent redis, hashmap, linkedhashmap, индекс, фича kabi asosiy mavzularga jamlangan.
📝 Tavsif va kontent siyosati
Muallif resursni shaxsiy fikrni ifoda etish maydoni sifatida ta’riflaydi:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Yuqori yangilanish chastotasi (oxirgi ma’lumot 07 Iyun, 2026 da olingan) sababli kanal doimo dolzarb va katta qamrovli bo‘lib qoladi. Analitika auditoriya kontent bilan faol hamkorlik qilishini, uni Texnologiyalar & Aralashmalar toifasidagi muhim ta’sir nuqtasiga aylantirishini ko‘rsatadi.
Enum: для использования дочернего типа при реализации интерфейса родителя. Это довольно экзотичный кейс. Сегодня покажу более практичный пример, как дженерики облегчили работу с иерархией и неизменяемыми переменными.
Дано: класс Delivery с информацией о доставке. Метод cancelled делает заказ недействительным. У класса есть наследник FastDelivery, в котором дополнительно хранится ID курьера:
class Delivery {
final long id;
final boolean isActive;
public Delivery(long id, boolean isActive) {…}
public Delivery cancelled() {
return new Delivery(this.id, false);
}
}
class FastDelivery extends Delivery {
private final long courierId;
public FastDelivery(…) {…}
public long getCourierId() {
return courierId;
}
}
Проблема: метод cancelled возвращает объект типа Delivery, и мы теряем информацию о курьере:
FastDelivery fast = new FastDelivery(…); Delivery cancelled = fast.cancelled(); ❌ long id = fast.getCourierId();В такой ситуации помогут self-referential generic и небольшой обходной манёвр: 🔸 Добавляем параметр в родителя
public class DeliveryᐸT extends DeliveryᐸTᐳᐳ🔸 Создаём метод create, который возвращает нужный экземпляр
protected T create(long id, boolean isActive) {
return (T) new Delivery(id, isActive);
}
🔸 Используем этот метод в cancelled
public T cancelled() {
return create(this.id, false);
}
🔸 Определяем параметр в наследнике
public class FastDelivery extends DeliveryᐸFastDeliveryᐳ🔸 Переопределяем метод create в наследнике
protected FastDelivery create(long id, boolean isActive) {
return new FastDelivery(this.id, this.isActive, courierId);
}
Всё! Теперь информация не теряется:
FastDelivery fast = new FastDelivery(…); FastDelivery cancelled = fast.cancelled(); ✅ long id = cancelled.getCourierId();Здесь используется комбо двух приёмов: 🔹 Метод create и его переопределение позволяют использовать поля, доступные в наследнике и вернуть нужный объект 🔹 Self-referential generic помогает вернуть нужный тип в методе cancelled Готовый код доступен здесь Резюме Рассмотрите использование self-referential generic, когда ▫️ У вас есть иерархия ▫️ Родительский тип упоминается в аргументах или возвращаемом значении Дополнительная типизация снизит количество кода, вытащит ошибки на этап компиляции и для некоторых случаев окажется очень изящным решением✨
еnum компилируется в наследник класса Enum:
public enum Animal {WOLF, TIGER}
↓
public class Animal extends Enum {
public static final Animal WOLF;
public static final Animal TIGER;
}
Подробнее об этом и енамах в целом можно почитать тут — раз, два и три.
В определении класса Enum используется конструкция, которая называется self-referential generic (или self-bound type, или recursive generic):
EnumᐸE extends EnumᐸEᐳᐳВ этом посте расскажу, что это такое и зачем нужно. Чтобы понять, какая проблема решается, представим, что этой конструкции нет. И определение енама выглядит так:
public abstract class MyEnum implements ComparableᐸMyEnumᐳПользователь определяет enum Animal и enum Converter. Компилятор превращает это в классы
Animal extends MyEnum Converter extends MyEnumКаждый класс должен реализовать интерфейс
ComparableᐸMyEnumᐳ и метод compareTo. Чтобы не сравнивать животных и конвертеры, придётся использовать instanceof:
public final int compareTo(MyEnum o) {
if (o instanceOf Animal other) {
// сравниваем зверюшек
// return ...
}
throw IllegalArgumentException();
}
В самом instanceOf нет ничего плохого. Тем более этот код генерируется при компиляции и остаётся за кадром.
Есть более важный момент. Пользователь может спокойно сравнить животное и конвертер, ошибка возникнет только в рантайме. Это выглядит странно, ведь enum Animal и enum Converter никак не связаны между собой.
Здесь дженерик выходит на сцену:
public abstract class EnumᐸE extends EnumᐸEᐳᐳ implements ComparableᐸEᐳ🔸 Добавляем параметр E, совместимый с классом
Enum
🔸 Используем E в интерфейсе Comparable
🔸 Компилируем enum Animal в
public class Animal extends EnumᐸAnimalᐳ🔸 Теперь
Comparable использует тип Animal, и метод compareTo станет таким:
public int compareTo(Animal o)✅ Убрали
instanceOf, код стал меньше и быстрее
✅ При компиляции происходит проверка типов:
Animal zebra = Animal.ZEBRA; Converter csv = Converter.CSV;❌
zebra.compareTo(csv); // не скомпилируется!
Self-referential generic позволяет использовать дочерний тип в интерфейсах и методах родителя. Для некоторых кейсов этот приём здорово упрощает код и снижает количество ошибок. В следующем посте покажу ещё один пример использования.
Ответ на вопрос перед постом: self-referential generic помогает ограничить сравнение разных enum между собой.default класс из другого пэкеджа (то есть повысить связность) не получится, код не скомпилируется. Либо придётся менять модификатор доступа, что точно будет заметно на ревью.
Но кое-что разрушает эту прекрасную картину: ✨фреймворки✨
Чтобы Spring мог сотворить волшебство, приходится немного жертвовать изоляцией. Начиная с public репозиториев и заканчивая одним контекстом на всё приложение.
При использовании спринга (или других фреймворков) связность между компонентами меньше ограничивается и с течением времени растёт.
Но выход есть!
Поделить функциональность не на пэкеджи, а на Maven/Gradle модули:
📂 registration
— 📂 src
— Controller, Service, Repository
— 📂 test
pom.xml
📂 export
⚠️ Обратите внимание, каждый модуль — просто набор классов и тестов, а не отдельный микросервис!
Связность при таком подходе снижается ещё больше:
✅ У каждого модуля свой набор зависимостей
✅ Нет общего контекста
Можно, наверное, поделить приложение на java модули, но модули Maven/Gradle встречаются гораздо чаще.
Совсем большие проекты идут ещё дальше. В Hexagonal/Clean/Onion/… architecture каждый бизнес-сценарий делится на модули бизнес-правил, адаптеров, инфраструктуры и тд.
✅ Минимальная связность, ультра простое тестирование
😐 Количество модулей, классов и интерфейсов увеличивается в разы
😐 Легко скатиться в карго-культ, нужен опыт для правильной реализации
Резюме
Spring — классный фреймворк, и здорово облегчает рутинные задачи. Но у него есть тёмная сторона — благодаря общему контексту связность кода неизбежно повышается. Чтобы проект не превратился в болото, в первую очередь нужен высокий профессиональный уровень всей команды.
Если приложение большое, имеет смысл поделить его на отдельные модули. У каждого бизнес-процесса будет свой контекст и набор зависимостей. Поддерживать такую структуру будет гораздо проще👍public.
В такой структуре естественным путём повышается связность. Если в UserService хочется узнать номер билета, то самое простое — добавить TicketRepository и вызвать нужный метод.
Всё связано со всем. Поменяешь в одном месте — сломается в другом. В пет-проекте с этим можно справиться, но для коммерческих проектов такая структура не подходит
Разделение по функциям
Складываем в один пекедж всё, связанное с какой-то сущностью. Оставляем 1-2 класса с модификатором public, остальным даём дефолтный модификатор доступа:
📂 user
— UserController, UserService, UserRepository
📂 ticket
— TicketController, TicketService, TicketRepository
📂 export
— ExportService, ExcelFormatter
Дефолтный модификатор ограничивает доступ между пэкеджами. Если UserService хочет сформировать отчёт по пользователям, он вынужден идти через ExportService, потому что ExcelFormatter ему не виден.
✅ Связность классов снижается, упрощается поддержка и тестирование
😐 Каждый класс решает не бизнес-задачу, а инфраструктурную. UserRepository — точка доступа к таблице users. UserService — класс по работе с классом User. Классы становятся огромными
😐 Высокая связность между бизнес-кейсами. Появляются десятки универсальных методов, которые "переиспользуются" в бизнес-сценариях. Например, создание и редактирование пользователя часто делают через один метод. Меняем одно — неизбежно задеваем похожие сценарии.
Разделение по бизнес-кейсам
Складываем в один пекедж все классы, связанные с бизнес-процессом. Большинство классов стоит с default модификатором и недоступна за пределами пэкеджа:
📂 newUser
— NewUserController, NewUserService, UserRepository
📂 buyTicket
— BuyTicketController, BuyTicketService, TicketRepository
📂 refundTicket — …
📂 export — …
Количество классов увеличивается, но они становятся меньше и более изолированными. Связность между бизнес-сценариями максимально снижается.
Итого: чёткая структура проекта и модификаторы доступа снижают связность между компонентами на уровне компиляции.
Однако очень мало проектов используют эту практику. Не потому что разработчики плохие, а потому что на большинстве проектов этот подход не сработает. Почему так получается и кто виноват — расскажу в следующем посте:)compare:
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() ✨TODO и выглядит так:
// TODO добавить тестыВсе такие комментарии можно посмотреть в окне TODO внизу экрана. Через него же можно перейти в нужное место кода в один клик. Если списка нет, ищите его через View → Tool Windows → TODO Помимо стандартных
TODO и FIXME можно добавить свои метки, например, OPTIMIZE, ASK, TEST. Сделать это можно в File → Settings → Editor → TODO
Очень удобно использовать TODO для текущих задач, чтобы ничего не забыть. Чтобы отметить код, который исправит кто-то другой, не забудьте закинуть соответствующую задачу в бэклог:)list.sort(). Хотя бы потому что в питоне есть такой метод.
Класс Integer реализует интерфейс Comparable, сортировка чисел — базовая функциональность любого языка программирования. Так что метод sort() максимально логичен.
Однако в интерфейсе List нет такого метода, только
void sort(Comparator<? super E> 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 нет.-classpath, -server, -version
⚙️ Нестандартные
Начинаются на -Х и определяют базовые свойства JVM. Могут не работать во всех JVM, но если поддерживаются, то вряд ли удалятся.
Пример: -Xmx, -Xms
⚙️ Продвинутые
Начинаются на -ХХ и касаются внутренних механизмов JVM. Не поддерживаются всеми JVM, часто меняются и удаляются.
Пример: -XX:MaxGCPauseMillis=500
Некоторые продвинутые опции требуют дополнительных флажков. Для экспериментальных фич обязателен -XX:+UnlockExperimentalVMOptions. Многие фичи диагностики не заработают без -XX:+UnlockDiagnosticVMOptions
Количество опций часто меняется. В 11 версии OpenJDK 1504 опции, а в 17 на 200 опций меньше.
Цикл отключения опций не совсем стандартный. В обычном коде что-то помечается Deprecated, и спустя время удаляется. VM Options используют более длинный цикл:
🔸 Deprecate: функционал работает, при запуске появляется warning
🔸 Obsolete: функция не выполняется, JVM пишет предупреждения
🔸 Expired: JVM не запускается
Многие опции очень нестабильны и часто меняются. Чтобы безопасно обновить версию java, нужно проверить набор опций через JaCoLine . Он подсветит устаревшие или уже бесполезные опции.
Полезные опции для java 11
(да, недавно вышла java 20, но самая популярная версия всё ещё 11)
1️⃣ Память
▫️ Начальный размер хипа: -Xms256m в абсолютных значениях, -XX:InitialRAMPercentage=60 - в процентах от RAM
▫️ Максимальный размер хипа: -Xmx8g или -XX:MaxRAMPercentage=60
▫️ Снять heap dump при переполнении памяти: -XX:+HeapDumpOnOutOfMemoryError. Адрес выходного файла задаётся в -XX:HeapDumpPath
2️⃣ Сборщик мусора
▫️ Serial GC: -XX:+UseSerialGC
▫️ Parallel GC: -XX:+UseParalllGC
▫️ CMS: -XX:+UseConcMarkSweepGC
▫️ G1: -XX:+UseG1GC (вариант по умолчанию)
▫️ ZGC: -XX:+UnlockExperimentalVMOptions -XX:+UseZGC
▫️ Shenandoah: -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
Вывести статистику сборщика при завершении работы: -XX:+UnlockDiagnosticVMOptions ‑XX:NativeMemoryTracking=summary ‑XX:+PrintNMTStatistics
Базовое логгирование коллектора: -Xlog:gc
Максимально информативное: -Xlog:gc*
3️⃣ Посмотреть все доступные опции
⚙️ Нестандартные: java -X
⚙️ Продвинутые: java -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:+PrintFlagsFinalBigInteger — неизменяемый класс. sum.add(10) создаёт новый объект, а исходная переменная не меняется. В итоге ни один add не влияет на sum. В консоль напечатается 0.
Давным-давно основной парадигмой разработки было ООП. Вся работа строилась на изменении объектов, и этот подход отражался в названиях методов: sum.add(4), user.setName("Alisa").
Исключения встречались редко, их нужно было просто запомнить. С юных лет все знают, что String — неизменяемый, и просто вызвать метод недостаточно:
String str = " Java "; ❌ str.trim(); ✅ str = str.trim();Последние годы растёт тренд на неизменяемость. При вызове метода должно быть понятно: ▫️ меняется текущий объект, и надо просто вызвать метод или ▫️ создаётся новый объект, который надо куда-то присвоить Для такого случая есть best practice: ✅ Когда метод меняет внутреннее состояние объекта, имя метода начинается с глагола ✅ Методы НЕизменяемых объектов используют другие конструкции Простой пример. Чтобы изменить внутренние поля, используем метод
set*:
order.setDeliveryDate(…);Создать новый объект на основе текущего — метод
with*:
order = order.withDeliveryDate(…);Для более сложных операций нужно включить креативность. Здесь помогут: 🔸 Причастия:
order.cancel(); // изменить текущий объект
Order o = order.cancelled(); // создать новый
🔸 Предлоги и союзы:
String s = str.toLowerCase(); LocalDate l = now().plusDays(12);// вместо addDays 🔸 Существительное в чистом виде:
String sub = str.substring(1);Цель здесь одна — показать, что текущий объект не меняется ❗️ Исключение: если класс использует Fluent API, обычно используются глаголы:
Optional opt = … opt.map(…).filter(…)Итого: с изменяемыми и неизменяемыми объектами работа идёт по-разному. Имена методов подсказывают, как правильно пользоваться классом. Хорошей практикой считается использовать глаголы для изменяемых объектов, и что-то другое для неизменяемых☀️
Endi mavjud! Telegram Tadqiqoti 2025 — yilning asosiy insaytlari 
