Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
显示更多📈 Telegram 频道 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 年 Telegram 研究 — 年度关键洞察 
