Джун пишет шесть @Test методов подряд: создание, обновление, удаление. Работает, но в отчёте — плоская простыня, а в коде — каша. Сеньор группирует тесты через @Nested. Разбираем за 16 секунд.
💡 Что такое @Nested?
@Nested — аннотация JUnit 5, которая позволяет создавать вложенные тестовые классы внутри основного. Каждый вложенный класс — это логическая группа тестов: например, Create, Update, Delete для сервиса. В отчёте такие тесты отображаются деревом, что упрощает навигацию.
Внутри @Nested можно использовать свои @BeforeEach, @AfterEach, @BeforeAll, @AfterAll — они выполняются только для тестов этой группы. Это позволяет настроить общие фикстуры для каждой группы отдельно: для Create — пустая база, для Update — уже заполненная.
✅ Как правильно?
class UserServiceTest {
@Nested class Create {
@BeforeEach void setUp() { /* пустая база */ }
@Test void ok() {}
@Test void fail() {}
}
@Nested class Update {
@BeforeEach void setUp() { /* база с пользователем */ }
@Test void ok() {}
}
}
⚠️ Нюанс: @Nested работает только с нестатическими вложенными классами (inner classes). Если сделать класс static, JUnit выдаст ошибку. Также @Nested не поддерживается в JUnit 4 — только в JUnit 5. И помни: @BeforeEach внешнего класса выполняется до @BeforeEach вложенного — это позволяет настроить общий контекст, а потом уточнить его для группы.
Shorts
- @Mock и @Spy — два инструмента Mockito, которые часто путают. Один создаёт заглушку, другой — обёртку над реальным объектом. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не путаться в моках и шпионах — это важно для написания честных тестов. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает разницу.
- 💡 Что такое BufferedReader? BufferedReader — это обёртка над Reader, которая добавляет буфер в памяти. Вместо того чтобы дёргать операционную систему на каждый read(), она читает файл крупными блоками (по умолчанию 8192 символа) и отдаёт данные из буфера. Это снижает количество системных вызовов и ускоряет чтение в разы. Метод readLine() читает строку до \n, \r или \r\n, возвращает её без символа перевода строки. В конце файла возвращает null. Пустая строка в файле — это "", а не null. ✅ Как правильно? try (BufferedReader br = Files.newBufferedReader( Path.of("data.txt"), StandardCharsets.UTF_8)) { String line; while ((line = br.readLine()) != null) { System.out.println(line); } } Files.newBufferedReader сразу создаёт BufferedReader с явной кодировкой — это современный и безопасный способ. Для старых API: new BufferedReader(new InputStreamReader(new FileInputStream(...), UTF_8)). ⚠️ Нюанс: Не читай огромный файл целиком в память через Files.readAllLines — для больших файлов это OutOfMemoryError. readLine() обрабатывает по одной строке, память не растёт. Также readLine() не подходит для бинарных данных — для них используй InputStream. И не забывай закрывать BufferedReader через try-with-resources: он держит файловый дескриптор.
- @WebMvcTest — slice-тест, который поднимает не всё приложение, а только часть контекста. Но что именно попадает в этот slice? В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не тратить секунды на медленные тесты — slice-тесты ускоряют CI в разы. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает, что поднимает @WebMvcTest.
- Джун оборачивает вызов внешнего сервиса в цикл с тремя попытками и sleep. Пока сервис работает — всё нормально. Когда он не отвечает — каждая ошибка превращается в три запроса и три секунды ожидания. Сеньор ставит @CircuitBreaker из Resilience4j и получает защиту, которая работает умнее. 💡 Что такое Circuit Breaker? Это паттерн «предохранитель». Он работает как электрический автомат: • CLOSED — всё нормально, запросы проходят. • OPEN — слишком много ошибок. Запросы сразу идут в fallback, не доходя до сервиса. • HALF_OPEN — через некоторое время пропускается несколько пробных запросов. Если они успешны — цепь замыкается. Если нет — снова OPEN. Зачем это нужно: • Экономия ресурсов. Не тратим потоки и время на заведомо провальные вызовы. • Защита сервиса. Не создаём дополнительную нагрузку на сервис, который и так не справляется. • Быстрый fallback. Пользователь получает ответ мгновенно, а не ждёт три секунды в цикле.
- COUNT(*) и COUNT(column) выглядят почти одинаково, но NULL меняет всё. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не ошибиться в аналитике — эта разница ломает отчёты чаще, чем кажется. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает, как NULL влияет на агрегаты.
- Map.of — удобный способ создать неизменяемую мапу. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не полагаться на порядок в Map.of — это частая причина «плавающих» тестов. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает контракт Map.
- Джун генерирует число через (int) (Math.random() * (max - min) + min). Формула работает, но есть три проблемы: Math.random() использует общий Random с внутренним AtomicLong — в многопоточной среде потоки конкурируют за один генератор. Плюс ручное приведение диапазона легко ошибиться на единицу. Плюс нет контроля над включением/исключением границ. Сеньор использует ThreadLocalRandom. 💡 Почему ThreadLocalRandom лучше? Math.random() внутри вызывает Random.nextDouble() у статического экземпляра Random. Этот Random использует AtomicLong для seed, и каждый вызов — атомарная операция с CAS. В многопоточке это создаёт конкуренцию: потоки борются за один генератор, производительность падает. ThreadLocalRandom.current() возвращает генератор, привязанный к текущему потоку — никакой синхронизации, никаких CAS. Он в разы быстрее в многопоточной среде. Кроме того, nextInt(min, max) сразу принимает границы: min включается, max — нет. Не надо считать (max - min) + min, не надо приводить типы, не надо думать про inclusive/exclusive. Для криптографии ни Math.random, ни ThreadLocalRandom не подходят — только SecureRandom. ⚠️ Нюанс: ThreadLocalRandom нельзя использовать в ForkJoinPool с parallelStream без осторожности — но в большинстве случаев он работает корректно. Для криптографии всегда SecureRandom — ThreadLocalRandom предсказуем.
- Set.of — удобный способ создать неизменяемое множество. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не ловить IllegalArgumentException при создании множеств — это происходит чаще, чем кажется. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает контракт Set.of.
- 💡 Что такое SRP? Single Responsibility Principle (SRP) — первый принцип SOLID. Формулировка Роберта Мартина: «У класса должна быть одна и только одна причина для изменения». Это не значит «один метод» или «одна строка». Это значит: класс отвечает за одну зону ответственности в системе. Когда UserService валидирует данные, сохраняет в БД и отправляет email, у него три причины для изменения: изменились правила валидации, сменилась БД, поменялся шаблон письма. Любое из этих изменений заставляет трогать один и тот же класс. Это ведёт к хрупкости: правка в валидации может сломать отправку email, а тесты придётся писать для всего сразу. ✅ Как исправить? Разделить обязанности на отдельные классы: • UserValidator — только валидация. • UserRepository — только работа с БД. • EmailService — только отправка писем. • UserService — координация: вызывает три сервиса по порядку. Теперь каждый класс меняется по своей причине. UserService знает только порядок шагов, а не их реализацию. Это упрощает тестирование: можно замокать EmailService и проверить только логику регистрации. ⚠️ Нюанс: SRP не означает «один метод на класс». Класс может иметь несколько методов, если они служат одной цели. Например, UserRepository может иметь save, findById, delete — все они про доступ к данным. Проблема начинается, когда в одном классе смешиваются разные слои: бизнес-логика, доступ к данным, UI, интеграции.
- Метод openStream() может вернуть null. Мы оборачиваем результат в try-with-resources. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не бояться null в try-with-resources — этот механизм обрабатывает его безопасно. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает внутренности JVM.
- 💡 Почему @CookieValue лучше? @CookieValue — это аннотация Spring MVC, которая связывает значение HTTP-cookie с параметром метода контроллера. Spring сам: • находит cookie по имени, • извлекает значение, • конвертирует в нужный тип (String, int, UUID, enum), • при необходимости подставляет defaultValue, • возвращает 400 Bad Request, если cookie обязательна и отсутствует. Преимущества: • Меньше кода: одна аннотация вместо цикла и null-проверок. • Типобезопасность: Spring сконвертирует значение в нужный тип. • Слабая связанность: метод не зависит от HttpServletRequest, его проще тестировать. • Валидация: можно совместить с @Validated, @NotNull, @Pattern. ⚠️ Нюанс: Если cookie отсутствует, а required = true (по умолчанию), Spring вернёт 400 Bad Request. Для необязательных cookie используй required = false — тогда при отсутствии параметр будет null (для String) или defaultValue. Также помни: @CookieValue работает только в контроллерах Spring MVC — в обычном сервлете её нет. Для нескольких cookie одного имени используй HttpServletRequest или @CookieValue с массивом.
- var выводит тип из инициализатора. Но что, если инициализатор — null? Или метод с неизвестным возвращаемым типом? В видео — четыре строки и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не спотыкаться о вывод типов при использовании var — это важно для современного Java. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила var.
- 💡 Что такое Parallel GC? Parallel GC (включается флагом -XX:+UseParallelGC) — это сборщик мусора в HotSpot, который использует несколько потоков для очистки как молодого, так и старого поколения. Young-поколение собирается копирующим алгоритмом: живые объекты копируются в survivor-области, мёртвые просто игнорируются. Old-поколение собирается через mark-sweep-compact: объекты помечаются, мёртвые удаляются, память уплотняется. Оба этапа распараллелены между потоками, но по-прежнему вызывают stop-the-world паузы — приложение замирает на время сборки. Количество потоков задаётся флагом -XX:ParallelGCThreads, по умолчанию — по числу ядер (или меньше в контейнерах с ограничением CPU). Parallel GC не является сборщиком по умолчанию в современных JVM — там используется G1, поэтому его нужно включать явно. ✅ Когда использовать? • Большая куча (несколько гигабайт) и многоядерный сервер. • Пропускная способность важнее низкой задержки: batch-обработка, фоновые задачи, ETL. • Паузы допустимы, но их длительность критична — Parallel GC делает их короче, чем Serial, за счёт многопоточности. ⚠️ Нюанс: Если у вас контейнер с ограничением CPU (например, 2 ядра), JVM может рассчитать ParallelGCThreads неоптимально — всегда проверяйте фактическое количество потоков через логи GC (-Xlog:gc*). Слишком много потоков даёт overhead на синхронизацию, слишком мало — долгую сборку. Для приложений с требованием к низкой задержке (веб-сервисы, API) лучше смотреть в сторону G1, ZGC или Shenandoah.
- Multi-catch позволяет ловить несколько исключений в одном блоке. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не ловить ошибку компиляции при работе с multi-catch — это частая проблема при рефакторинге. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила перечисления исключений.
- Джун проверяет строки регуляркой через s.matches(...) прямо в цикле. Работает, но под капотом String.matches каждый раз компилирует Pattern с нуля. На 10 000 строках это 10 000 компиляций регулярки — заметный удар по CPU. Сеньор компилирует Pattern один раз и переиспользует его. Разбираем за 16 секунд. 💡 Почему String.matches в цикле — плохо? String.matches(regex) — это удобный метод, но он не кэширует скомпилированный Pattern. Внутри он делает Pattern.compile(regex).matcher(this).matches(). Компиляция регулярного выражения — дорогая операция: парсинг строки, построение автомата, оптимизация. В цикле она выполняется для каждой итерации, хотя регулярка не меняется. Это превращает простую валидацию в узкое место. Pattern.compile(regex) создаёт immutable-объект, который можно переиспользовать. Метод matcher(CharSequence) создаёт лёгкий Matcher для каждой строки. Это даёт ту же логику, но без повторной компиляции. Если регулярка используется хотя бы дважды — её стоит скомпилировать заранее. ✅ Как правильно? • Компилируй Pattern один раз: private static final Pattern EMAIL = Pattern.compile("..."). • Используй EMAIL.matcher(s).matches() вместо s.matches("..."). • Для потокобезопасности Pattern immutable — можно смело держать в статическом поле. ⚠️ Нюанс: Если регулярка используется один раз — String.matches нормально. Оптимизация нужна только при повторном использовании. И не забывай про флаги: Pattern.CASE_INSENSITIVE, Pattern.DOTALL и т.д. — их тоже выноси в константу.
- Поля, блоки инициализации и конструктор — все они участвуют в создании объекта. Но в каком порядке? В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не путаться в порядке инициализации — это важно для понимания JVM. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает порядок выполнения.
- 💡 Что такое рехеширование? HashMap внутри хранит элементы в массиве бакетов. Когда количество элементов достигает порога capacity * loadFactor (по умолчанию 16 * 0.75 = 12), происходит resize: внутренний массив удваивается, а все элементы пересчитываются и перекладываются. Это O(n) операция, которая вызывает паузы. Фактор загрузки — компромисс между памятью и производительностью: ниже значение → меньше коллизий, но больше памяти; выше → экономия памяти, но чаще resize. ✅ Как правильно? • Если знаешь будущее количество элементов, задавай new HashMap(expectedSize / 0.75f + 1), чтобы избежать resize. • Для большинства случаев дефолтные 16 и 0.75 подходят. • Если критична скорость и не жалко памяти, можно снизить loadFactor до 0.5. ⚠️ Нюанс: Частые ресайзы особенно болезненны при добавлении миллионов элементов. Лучше один раз выделить массив с запасом, чем много раз копировать данные.
- Stream.iterate — мощный инструмент, но у него есть тёмная сторона. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не забывать про limit и takeWhile при работе с бесконечными стримами. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает коварство iterate.
- Джун вешает @Scheduled(fixedRate = 3600_000) и ждёт, что задача запустится через час. Но Spring запускает её сразу после старта контекста. Приложение ещё не прогрето: пулы потоков не заполнены, соединения с БД не установлены, кэши пустые. Задача либо падает, либо забивает ресурсы в самый неподходящий момент. Сеньор добавляет initialDelay. Разбираем за 16 секунд. 💡 Зачем нужен initialDelay? @Scheduled без initialDelay означает: первый запуск произойдёт немедленно после инициализации планировщика Spring, то есть в первые секунды старта приложения. Это опасно по нескольким причинам: • Прогрев ещё не завершён. Пул соединений с БД может быть пуст, Hibernate ещё не поднял кэши, а @PostConstruct других бинов может не выполниться. • Конкуренция за ресурсы. Старт приложения и так ресурсоёмкий: миграции, загрузка конфигов, warm-up. Дополнительная тяжёлая задача усугубляет. • Каскадные ошибки. Если задача падает на старте, Spring может откатить инициализацию контекста или залогировать ошибку, которая маскирует реальные проблемы. initialDelay задаёт паузу перед первым запуском. Это даёт приложению время прогреться: установить соединения, заполнить кэши, стабилизироваться. Периодичность (fixedRate или fixedDelay) начинает работать только после первой задержки. ⚠️ Нюанс: initialDelay не влияет на fixedRate и fixedDelay — они начинают отсчёт только после первого запуска. Если initialDelay больше периода, первый запуск всё равно произойдёт через initialDelay, а не через период. Также в Spring Boot можно задать глобальную задержку через spring.task.scheduling.initial-delay, но лучше указывать явно на каждом методе — разные задачи требуют разного времени прогрева.
