Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
إظهار المزيد📈 نظرة تحليلية على قناة تيليجرام Java: fill the gaps
تُعد قناة Java: fill the gaps (@java_fillthegaps) في القطاع اللغوي الروسية لاعباً نشطاً. يضم المجتمع حالياً 12 549 مشتركاً، محتلاً المرتبة 10 121 في فئة التكنولوجيات والتطبيقات والمرتبة 52 862 في منطقة روسيا.
📊 مؤشرات الجمهور والحراك
منذ تأسيسه في невідомо، حقق المشروع نمواً سريعاً وجمع 12 549 مشتركاً.
بحسب آخر البيانات بتاريخ 07 يونيو, 2026، تحافظ القناة على نشاط مستقر. خلال آخر 30 يوماً تغيّر عدد الأعضاء بمقدار -46، وفي آخر 24 ساعة بمقدار 0، مع بقاء الوصول العام مرتفعاً.
- حالة التحقق: غير موثّقة
- معدل التفاعل (ER): يبلغ متوسط تفاعل الجمهور 34.72%. وخلال أول 24 ساعة من النشر يحصد المحتوى عادةً N/A% من ردود الفعل نسبةً إلى إجمالي المشتركين.
- وصول المنشورات: يحصل كل منشور على متوسط 0 مشاهدة. وخلال اليوم الأول يجمع عادةً 0 مشاهدة.
- التفاعلات والاستجابة: يتفاعل الجمهور بانتظام؛ متوسط التفاعلات لكل منشور يبلغ 0.
- الاهتمامات الموضوعية: يركز المحتوى على مواضيع رئيسية مثل redis, hashmap, linkedhashmap, индекс, фича.
📝 الوصف وسياسة المحتوى
يصف المؤلف القناة بأنها مساحة للتعبير عن الآراء الذاتية:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
بفضل وتيرة التحديث المرتفعة (أحدث البيانات بتاريخ 08 يونيو, 2026) تحافظ القناة على حداثتها ومستوى وصول مرتفع. وتُظهر التحليلات تفاعلاً نشطاً من الجمهور، ما يجعلها نقطة تأثير مهمة ضمن فئة التكنولوجيات والتطبيقات.
public final class Number extends Enum<Number
>
Элементы енума станут статическими полями:
public static final Number ONE; public static final Number TWO; public static final Number THREE;Внутри нового класса появится массив:
Number[] VALUES = { ONE, TWO, THREE};
И его копия будет возвращаться в методе values:
return VALUES.clone();При каждом вызове values возвращается новая копия массива. Дело в том, что массивы — это изменяемый объект. Если возвращать ссылку на VALUES напрямую, любой желающий сможет поменять исходный массив:
Number.values()[2] = ONE;Это небезопасно, поэтому каждый раз возвращается копия. Если цикл с values используется в высоконагруженном коде, то разумно сохранить массив в отдельную переменную и переиспользовать её:
static Number[] numbers = Number.values();
for (Number n : numbers) {…}
Если код вызывается редко, то смысла в отдельной переменной нет.
Пример из жизни
В Spring Web 5.2 в классе HttpStatus есть такой код:
for (HttpStatus status : values()) {
if (status.value == statusCode) {
return status;
}
}
Этот цикл вызывается почти в каждом запросе, но только в этом году завели баг. К описанию прилагался бенчмарк: при нагрузке 600 запросов/сек код производил мегабайт мусора каждую секунду.
Теперь код выглядит так:
private static final HttpStatus[] VALUES;
static {
VALUES = values();
}
for (HttpStatus status : VALUES) {
if (status.value == statusCode) {
return status;
}
}
Ответ на вопрос перед постом
Будет создано 3 массива: один внутри класса Number и два клона при вызове values()Service записывает логи в файл через класс FileLogger:
class FileLogger {…}
class Service {
FileLogger logger=new FileLogger();
}
Сделаем код чуть лучше с помощью разных принципов:
1️⃣ Dependency injection
— компоненты создаются не внутри класса, а где-то в другом месте.
Как реализовать: перенести инициализацию логгера в конструктор или сеттер:
class Service {
FileLogger logger;
Service (FileLogger logger) {
this.logger=logger;
}
}
✅ Класс занимается только своей бизнес-логикой
✅ Можно вынести всю конфигурацию в одно место. Или спихнуть часть забот фреймворку, например, Spring
⚔️Историческая справка
Когда Spring ещё не был популярен, в проектах использовался паттерн Service Locator.
Суть: компоненты создаются в классе ServiceLocator, а другие классы получают к ним доступ через статические методы:
class ServiceLocator {
static Logger logger = …
static Logger getLogger() {
return logger;
}
}
class Service {
Logger logger=ServiceLocator.getLogger();
}
2️⃣ Dependency invertion (D из SOLID)
▫️Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций
▫️Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции
Как реализовать: использовать интерфейс логгера, а не конкретный класс
interface Logger {…}
class FileLogger implements Logger {…}
class Service {
Logger logger=new FileLogger();
}
✅ Интерфейс проще использовать, так как методов меньше
✅ Реализацию легко заменить
✅ Оба класса проще тестировать
Термин "абстракция" используется, потому что SOLID не привязан только к джаве. Группу методов можно выделить в интерфейс, в абстрактный класс и даже в обычный класс. Но интерфейс — наилучший вариант
3️⃣ IoC - Inversion of Control
В маленьких программах жизнь начинается в методе main. Программист создаёт объекты и вызывает их методы, все шаги явно прописаны.
Inversion of Control — это когда ход выполнения программы задаёт фреймворк. Например, Spring создаёт объекты, принимает запросы и не даёт программе завершиться.
Как реализовать: использовать аннотации фреймворка
@Component class FileLogger {…}
@Component class Service {
@Autowired
FileLogger logger;
}
✅ Меньше скучного кода
✅ Низкая связность — код легко читать, менять и тестировать
Резюме:
🔸Dependency injection — класс не создаёт компоненты напрямую, они передаются через конструктор или сеттер
🔸Dependency invertion — класс работает с другими компонентами через интерфейс
🔸Inversion of Control — ход программы задаёт фреймворк
❗️Ответ на вопрос перед постом:
Это словоблудие относится к Dependency injectionA service1 = new A(); A service2 = new B();Для наблюдателя service1 и service2 ведут себя совершенно одинаково. Класс-наследник дополняет поведение родителя, а не замещает его. В результате система работает более предсказуемо. Как это выглядит на практике: 1️⃣ Выходной тип метода в наследнике такой же как у родителя или расширенный Базовый класс:
Info getInfo()
Наследник:
✅ BigInfo getInfo() ❌ Object getInfo()2️⃣ Подклассы не бросают дополнительных исключений, но могут уменьшить их список Базовый класс:
void save() throws FileNotFoundException
Наследник:
✅ void save() ❌ void save() throws FileNotFoundEx, InterruptedExJava — типизированный язык, поэтому пункты 1 и 2 контролируются компилятором. 3️⃣ Типы входных параметров те же или менее строгие. Пункт для общего понимания, тк для Java это неприменимо Базовый класс:
void add(Account acc)
Наследник:
✅ void add(Object acc) ❌ void add(AdminAccount acc)Следующие пункты компилятор уже не проверит, это целиком ответственность программиста. 4️⃣ Метод подкласса делает то же, что и метод базового класса Базовый класс: метод
countVisitors считает пользователей
Наследник:
✅ Считает пользователей чуть по-другому
❌ Считает пользователей, обновляет статистику, сохраняет результат в БД
5️⃣ Метод наследника взаимодействует с теми же сущностями:
▪️ Метод родителя увеличивает счётчик - подкласс тоже увеличивает
▪️ Метод родителя не меняет поле - подкласс тоже не меняет
▪️ Метод родителя вызывает другие методы в определённом порядке - подкласс делает то же самое
А что можно вообще?
Если в подклассе объявлены новые поля, то методы подкласса могут делать с ними что угодно. На этом всё🙂
Правила выше - очень строгие. Но и наследование — штука непростая, это самая сильная связь между сущностями. Часто единственный плюс — это краткость кода, но по ходу развития проекта ограничения доставляют всё больше проблем.
Нарушения принципа подстановки — повод пересмотреть иерархию наследования или совсем от неё отказаться.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)К ситуациям выше в комплекте идут 3️⃣ Длинные методы 4️⃣ Любовь к статическим методам Как лучше: небольшие методы, связанные с конкретным классом. Связность ниже, ошибок меньше. 5️⃣ Сложное проектирование Задача: завести три типа пользователей - новые, обычные и привилегированные. Новички скорее всего сделают интерфейс, 3 класса и статический класс с фабричными методами и билдером. Как лучше: как можно проще. Например, один класс пользователя с полем Тип. PS Все предложенные "как лучше" не всегда лучше. Но думаю, вы поняли идею. 6️⃣ Нулевое или минимальное покрытие тестами как следствие больших сложных методов и недостаточной инкапсуляции. 7️⃣ Низкий уровень ответственности Пункт не относится к разработке, но очень актуален для начинающих. Проявляется в двух формах: 🔸 Непонятно, что происходит. Человек сидит и молчит до последнего, пока не спросишь статус задачи или про возможные трудности. Он умалчивает проблемы или переносит на других: — Что с задачей, которую я тебе дала 3 дня назад? — Я не понял, куда смотреть, потом меня HR позвал бумаги подписывать, потом я настраивал гит, увидел другую задачу и переключился на неё. 🔸 Код не решает проблему, а заметает симптомы: — Тесты мешали сделать пул-реквест, так что я их отключил. 8️⃣ Слабые коммуникативные навыки — Как ты починил баг с расчётом ставки? — Там через геттер фабричный метод нашёл, и потом докер с постгрёй поднял посмотреть, в логах был фильтр урезанный, я письмо отправил тебе в цц, но вроде скоуп не тот или тот, короче, запушил — В чём была ошибка? — Там два двоеточия вылезло — Где? — В дебаге Эти ошибки ожидаемы в начале работы, и ничего страшного в этом нет. Я их тоже делала🙂 Чем быстрее вы перестроитесь на командный стиль разработки, тем вероятнее пройдёте испытательный срок и быстрее вольётесь в проект.
-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
Опции очень нестабильны и часто меняются. Чтобы безопасно обновить версию java, проверьте ваш набор опций через JaCoLine. Он подстветит устаревшие или бесполезные опции.
Полезные опции для java 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:+PrintFlagsFinalCtrl + C Ctrl + V Ctrl + Xработают со всей строкой, на которой стоит курсор, не нужно ничего выделять. Удалить всю строку:
Ctrl + YДублировать строку:
Ctrl + DВыделить часть кода:
Ctrl + WПри каждом нажатии W захватывается всё большая область. Переместить выделенный код:
Ctrl + Shift + ⬆️
Ctrl + Shift + ⬇️@Before и @After.
А как посчитать время для всех классов? Здесь варианта два:
🔸 Вынести общий код в отдельный класс, в каждый класс-тест добавить методы Before и After. Решение рабочее, но придётся копипастить методы в каждый класс.
🔸 Внедрить логику где-то на верхнем уровне и включать/выключать её через настройки или аннотации.
Это и есть кастомизация - предусмотренные библиотекой места "встраивания" новой логики. JUnit 4 и 5 используют для этого разные механизмы. Давайте кратко их обсудим.
JUnit 4 Runner
Переопределяем жизненный цикл теста целиком. Наследуемся от интерфейса Runner или абстрактного класса, в нужных местах добавляем нужные действия. Теперь тесты запускаются не по стандартной схеме, а по той, что прописана в новом классе.
Примеры:
▫️ @RunWith(Parameterized.class) запускает параметризованные тесты
▫️ @RunWith(Suite.class) запускает наборы тестов
▫️ @RunWith(SpringJUnit4ClassRunner.class) добавляет спринговые активности до и после запуска теста
▫️ @RunWith(MockitoJUnitRunner.class) позволяет использовать заглушки
Главный минус - жизненный цикл только один, значит Runner для теста может быть только один. Не получится совместить несколько фич, например, параметризованные тесты с заглушками.
JUnit 4 Rule
Переопределяем интерфейс TestRule и задаём действие до и после выполнения теста. В тестах выглядит как просто поле:
@Rule public Timeout globalTimeout = Timeout.seconds(10);В JUnit 4 есть несколько готовых правил: ▪️
TemporaryFolder - создать временную папку для теста
▪️ ExternalResource - открыть и закрыть внешний ресурс(файл, сокет, БД)
Плюсы-минусы:
✅ Можно использовать несколько rule в одном классе
❌ Работает в рамках одного метода и по сути похож на before/after.
JUnit 5 Extension
Жизненный цикл теста разбивается на 10+ фаз. К каждой из них можно присоединиться, если переопределить нужный интерфейс:
▫️BeforeAllCallback - действие перед всеми тестами
▫️ParameterResolver - передача параметров в тест
Реализуем нужные интерфейсы, регистрируем класс и готово. Похожий механизм используется в Spring.
✅ Класс может использовать несколько экстеншенов
✅ Можно вклиниться на любых этапах жизненного цикла
✅ В интерфейсах доступен контекст выполнения и вся информация про тесты, в итоге возможностей гораздо больше
В JUnit 5 полностью убрали поддержку Runner и Rule, всё переписано на Extension API. Кодовые базы стали несовместимы между собой, поэтому и нужна библиотека Vintage с адаптерами.
______
Разбирать чужие кейсы полезно, но не всегда увлекательно. Поэтому вот интересный факт про разработку JUnit.
JUnit - опенсорсный проект, где никто никому не платил за работу.
Но рефакторинг назревал много лет. Однажды ребята решили, что такие грандиозные планы требуют фулл тайм и объявили краудфандинг на JUnit 5.
Сумма требовалась небольшая - 25 тысяч евро, меньше двух миллионов рублей. В итоге собрали в 2 раза больше, и уже через 6 недель был готов первый прототип.
Меня это очень впечатляет, особенно в сравнении со стоимостью и скоростью разработки в энтерпрайзе🙈@Test, @Before, методы assertEquals и тд. Здесь всё классно.
Дальше эти тесты запускает IDE или система сборки.
И вот им приходится тяжело. В JUnit 4 API для запуска и анализа тестов очень ограниченный, поэтому IDE и сборщики используют рефлекшн и другие обходные пути.
Чем плох такой подход - понятно. Любое изменение внутренней реализации ломает логику внутри IDE/системы сборки.
JUnit 5 учёл эту проблему и содержит три отдельных артефакта:
🔸 Jupiter - апи для разработчиков
🔸 Platform - апи для запуска и анализа тестов. Целевая аудитория - IDE, плагины и системы сборки. Теперь каждый из них может использовать библиотеку, а не писать свой велосипед
🔸 Vintage - для запуска JUnit 4 тестов на новой платформе
Почему у JUnit 4 и 5 разные аннотации?
У JUnit 5 абсолютно другая кодовая база. Для совместимости с 4 версией пришлось бы наворотить много кода. Гораздо практичнее вынести все адаптеры в отдельный компонент.
Тогда
▫️ Старые тесты будут работать
▫️ Чётко видно, где старые тесты, а где новые. А значит есть шанс, что со временем кодовая база с тестами перейдёт на новую версию.
Что здесь особенного?
В целом выглядит как обычный рефакторинг. Продукт развивается, мир меняется, монолит делится на составные части.
Но в этой истории есть две важные детали.
1️⃣ На страничке принципов разработки команды JUnit есть такие строки:
▫️ JUnit has never tried to be a swiss army knife
▫️ Third party developers move more quickly than we do
Отсюда видна ещё одна мотивация: поощрение развития других библиотек и фреймворков.
Другие разработчики тестовых библиотек теперь могут использовать JUnit платформу и автоматически получать поддержку библиотек во всех IDE и системах сборки.
2️⃣ Вторая инициатива команды JUnit - проект Open Test Alliance for the JVM.
В чём суть: есть много тестовых фреймворков и библиотек. Все они работают по-разному - бросают разные исключения, отличается формат и набор данных и тд. IDE и системам сборки приходится учитывать все особенности.
Идея проекта - создать общую спецификацию для тестовых библиотек. Проект поддержали TestNG, Spock, Hamcrest, AssertJ, Eclipse, IntelliJ, Gradle, Maven и Allure.
Неизвестно, закончится ли эта история удачно, но идея классная.
Здорово, когда компания делает не только хороший продукт, но и способствует развитию отрасли в целом😇 @Test.
Через аннотацию @DisplayName задаётся симпатичное имя теста в отчёте.
Чтобы выполнить что-то до или после выполнения теста, используются методы с аннотациями
▫️ @Before, @BeforeAll ▫️ @After, @AfterAllJUnit создаёт новый экземпляр класса на каждый тестовый метод. Класс
ServiceTest с пятью методами @Test во время запуска превратится в 5 экземпляров класса ServiceTest.
Благодаря этому тесты выполняются независимо.
Этим JUnit отличается от TestNG, где создаётся один экземпляр класса на все тестовые методы. Если хочется как в TestNG, добавьте над классом аннотацию @TestInstance(Lifecycle.PER_CLASS)
2️⃣ Проверки
Сердце каждого теста - методы с приставкой assert*:
🔸 assertTrue 🔸 assertEquals 🔸 assertInstanceOfВ самом JUnit мало методов, более удобные ассерты есть в библиотеках Hamсrest и AssertJ. AssertJ, на мой взгляд, более читабельный, но Hamсrest используется чаще. 3️⃣ Группировка тестов Аннотация
@Tag("groupName") объединяет тесты в группы. Работает и для одного теста, и для класса.
Можно указывать тэги в системе сборки и при запуске тестов из IDE.
4️⃣ Отключение тестов
Аннотация @Disabled. Продвинутые варианты для:
▫️ операционной системы
@DisabledOnOs(WINDOWS)▫️ версии java
@DisabledOnJre(JAVA_9) @DisabledForJreRange(min = JAVA_9)▫️ системных переменных:
@DisabledIfSystemProperty(named = "ci-server", matches = "true") @DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")5️⃣ Параметризированные тесты Помогают запустить один тест с разными аргументами. Выглядит так:
@ParameterizedTest
@ValueSource(ints={100,-14})
public void test(int input) {}
Такой тест запустится дважды - с аргументом 100 и -14.
Вместо готового списка можно брать значения
🔸 из CSV файла @CsvSource
🔸 из метода @MethodSource
6️⃣ Проверка таймаута
▫️ Через ассерт
assertTimeout(ofMinutes(2), ()->{});
▫️ Через аннотацию
@Timeout(value=42,unit=SECONDS)7️⃣ Полезные библиотеки ▫️ Hamсrest, AssertJ - расширенные библиотеки методов-ассертов ▫️ Mockito для заглушек. Добавляете библиотеку в pom.xml или build.gradle, а в тест - аннотацию
@ExtendWith(MockitoExtension.class)
▫️ Testcontainers для запуска внешних компонентов в докере. Добавляем библиотеку, аннотацию @Testcontainers над классом и @Container над компонентом
▫️ Java Faker - генератор данных для тестов
متاح الآن! بحث تيليغرام 2025 — أهم رؤى العام 
