Метод 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, но лучше указывать явно на каждом методе — разные задачи требуют разного времени прогрева.
OutOfMemoryError — это Error, а не Exception. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы понимать разницу между Error и Exception — это важно для написания надёжного кода.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про Throwable.
💡 Что такое ExecutorService?
ExecutorService — это интерфейс из java.util.concurrent, который представляет пул потоков. Вместо того чтобы создавать Thread вручную, ты отправляешь задачи в пул, а он распределяет их между потоками. Потоки переиспользуются, что экономит ресурсы. Пул можно создать через фабрики Executors. По умолчанию задачи ставятся в очередь, если все потоки заняты.
✅ Когда использовать?
• Когда есть много независимых задач (обработка запросов, вычисления).
• Когда нужно ограничить количество одновременно работающих потоков.
• Когда нужен результат выполнения задачи (Future).
⚠️ Нюанс: ExecutorService не завершается автоматически. Если не вызвать shutdown() или shutdownNow(), приложение может не остановиться. shutdown() перестаёт принимать новые задачи и ждёт завершения текущих, shutdownNow() пытается прервать выполняющиеся задачи. Также не забывай про awaitTermination, если нужно дождаться полного завершения пула.
Лямбды реализуют функциональные интерфейсы, и не все исключения можно просто так пробросить. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не ловить ошибки компиляции при работе со стримами и checked-исключениями.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает ограничения лямбд.
Джун тащит HttpServletRequest в метод и парсит заголовок через getHeader. Сеньор ставит @RequestHeader на параметр — Spring делает всё сам. Разбираем за 16 секунд.
💡 Почему @RequestHeader лучше?
@RequestHeader — это аннотация Spring MVC, которая связывает значение HTTP-заголовка с параметром метода контроллера. Spring сам достаёт заголовок, конвертирует в нужный тип (String, int, enum, UUID, дата и т.д.) и, если заголовка нет, возвращает 400 Bad Request — без ручных проверок на null.
Преимущества:
• Меньше кода: одна аннотация вместо ручного парсинга и null-проверок.
• Типобезопасность: Spring сконвертирует значение в нужный тип.
• Слабая связанность: метод контроллера не зависит от HttpServletRequest, его проще тестировать.
• Валидация: можно совместить с @Validated, @NotNull, @Pattern.
⚠️ Нюанс: Для нескольких заголовков одного имени используйте @RequestHeader MultiValueMap headers или HttpHeaders. А для заголовков, которые часто используются (например, Authorization), Spring Security даёт готовые механизмы — не парсите его руками.
try без catch, только finally. Внутри бросается исключение. Многие думают, что finally его «съест». Но Java так не работает. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не терять исключения из-за finally — это классическая ловушка.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает порядок выполнения.
💡 Что такое CopyOnWriteArrayList?
CopyOnWriteArrayList — потокобезопасная реализация List из пакета java.util.concurrent. Его главная особенность: при каждой модификации (add, set, remove) создаётся новая копия внутреннего массива. Старый массив остаётся неизменным, и все текущие итераторы продолжают работать с ним. Это даёт fail-safe поведение: изменение списка во время итерации не вызывает ConcurrentModificationException.
Чтение (get, итерация) работает без блокировок и очень быстро — массив не меняется, пока не произойдёт запись. Запись, наоборот, дорогая: копирование всего массива занимает O(n) и создаёт нагрузку на память. Итератор работает со snapshot и не видит элементов, добавленных после его создания — чтобы получить актуальные данные, нужно получить новый итератор.
✅ Когда использовать?
• Много чтений, мало записей.
• Список слушателей событий, подписчиков, обработчиков.
• Кэш конфигураций, который обновляется редко.
• Ситуации, когда итерация не должна прерываться при модификации.
⚠️ Нюанс: Не используй CopyOnWriteArrayList при частых записях — копирование массива на каждый add убьёт производительность. Для частых изменений подойдут ConcurrentLinkedQueue, ConcurrentHashMap.newKeySet() или Collections.synchronizedList(new ArrayList()). Также помни, что fail-safe итератор — это не «магическая защита»: если логика зависит от актуальных данных, snapshot может ввести в заблуждение.
Pattern matching для instanceof появился в Java 16. Как он ведёт себя с null? Многие ждут NPE, но Java удивляет. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не бояться instanceof с null — этот паттерн безопасен.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про pattern matching.
Джун загружает 100 пользователей и их заказы. Hibernate делает 101 запрос. Сеньор ставит @BatchSize(size = 50) и получает 3 запроса.
💡 Что такое N+1 и как @BatchSize его лечит?
Когда ты загружаешь список сущностей, а у каждой есть ленивая коллекция, Hibernate по умолчанию идёт в базу за каждой коллекцией отдельно. Это N+1: один запрос на список + N запросов на каждую коллекцию.
@BatchSize(size = N) говорит Hibernate: «Когда один из пользователей запросит свои заказы, подгрузи заказы сразу для N пользователей одним запросом». То есть вместо 100 отдельных SELECT-ов уйдёт 2 пачки по 50. Плюс один изначальный — итого 3.
Настройка действует на уровне коллекции (или на уровне класса для сущностей).
⚠️ Нюанс: @BatchSize не заменяет JOIN FETCH. Если данные нужны сразу в одном запросе — используй JOIN FETCH или @EntityGraph. @BatchSize — когда коллекции нужны лениво и хочется избежать N+1 без дублирования строк. Комбинация обоих подходов даёт лучший результат.
Параллельный стрим обычно не гарантирует порядок. Но что, если использовать forEachOrdered? В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не путать forEach и forEachOrdered — это важно при параллельной обработке.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает тонкости Stream API.
💡 В чём разница?
throw — это оператор, который создаёт и выбрасывает объект исключения. Например, throw new IOException("...").
throws — это ключевое слово в сигнатуре метода, которое объявляет, что метод может выбросить одно или несколько checked-исключений. Например, void readFile() throws IOException. Оно информирует вызывающий код о необходимости обработки (try-catch или проброс выше). Для unchecked-исключений (наследников RuntimeException) throws не обязателен, потому что компилятор не требует их обработки.
✅ Как правильно?
• Бросаешь исключение в теле метода? Используй throw.
• Метод может выбросить checked-исключение? Объяви через throws.
• Для unchecked throws опционален, но может быть добавлен для документирования.
⚠️ Нюанс: Один метод может объявлять несколько исключений: throws IOException, SQLException. Если метод переопределяет метод родителя, он не может объявлять более широкие checked-исключения, чем родительский.
