Родитель A, наследник B. Что напечатает new B()?
class A {
static { p(1); }
{ p(2); }
A() { p(3); }
}
class B extends A {
static { p(4); }
{ p(5); }
B() { p(6); }
}
new B(); // 1 4 2 3 5 6
new B(); // 2 3 5 6
👉 Коротко: сначала один раз готовятся классы (static), от родителя к наследнику. Потом собирается объект — тоже от родителя к наследнику.
🔍 Что происходит по шагам:
1️⃣ Java впервые встречает класс B и готовит его. Но сначала — родителя A: static A → 1, затем static B → 4.
2️⃣ Создаётся объект. Конструктор B первым делом вызывает конструктор A.
3️⃣ Собирается часть A: блок { } → 2, конструктор → 3.
4️⃣ Собирается часть B: блок { } → 5, конструктор → 6.
5️⃣ Второй new B(): классы уже готовы, static не повторяется → 2 3 5 6.
🏠 Аналогия: дом строят снизу вверх. Фундамент (static) заливают один раз. Каждую новую квартиру собирают с первого этажа (A) и выше (B).
⚠️ Где ещё всплывает:
• Блоки { } и значения полей выполняются в том порядке, в котором написаны в классе.
• Опасно вызывать из конструктора родителя метод, который переопределил наследник: поля наследника ещё не заполнены и равны 0 или null.
📌 Запомни: static — один раз, от родителя к наследнику. Объект — каждый раз, тоже от родителя к наследнику.
Как устроен HashMap — за минуту 🗺️
HashMap спрашивают почти на каждом собеседовании. Разбираем по шагам, что происходит внутри, когда ты пишешь map.put("лис", 7).
1️⃣ Ключ → номер корзины
HashMap берёт "лис".hashCode() = 1075332, перемешивает старшие биты с младшими (h ^ h 16 = 1075348) и накладывает маску по размеру массива: 1075348 & 15 = 4. Значит, корзина номер 4.
2️⃣ Корзины — это обычный массив
По умолчанию в нём 16 ячеек, и создаётся он при первом put. Ничего не перебираем — сразу идём в нужную ячейку.
3️⃣ В корзине лежит список узлов
Каждый узел хранит хэш, ключ, значение и ссылку на следующий. Если в корзине уже кто-то есть, это коллизия, и новый узел встаёт в конец списка. get проходит тот же путь и сравнивает сначала хэш, потом equals.
4️⃣ Рост
Когда элементов больше, чем 75% от числа корзин, массив удваивается. Хэш заново не считается: каждый узел остаётся на своём месте или сдвигается ровно на старую длину массива (наш «лис» переехал из 4 в 20).
🌳 Нюанс: если в одной корзине больше 8 узлов, список превращается в красно-чёрное дерево, но только когда корзин уже 64 или больше. Иначе HashMap просто расширяется.
⚠️ Где чаще всего ломается
— переопределил equals без hashCode → равные ключи попадут в разные корзины
— поменял поле ключа после put → ключ «потерялся» в старой корзине
— пишешь из нескольких потоков → нужен ConcurrentHashMap
💡 Полезно знать
— ключ null разрешён, он всегда лежит в корзине 0
— знаешь размер заранее → HashMap.newHashMap(n) (Java 19+), лишних расширений не будет
Ждёшь ab, а получаешь 195 🤯
var r1 = 'a' + 'b'; // 195
var r2 = "" + 'a' + 'b'; // "ab"
var r3 = 'a' + 'b' + ""; // "195"
Почему так:
🔹 char в Java — это не строка, а 16-битное число без знака: код символа в UTF-16. 'a' = 97, 'b' = 98.
🔹 При сложении двух char оба расширяются до int (binary numeric promotion), поэтому 'a' + 'b' = 97 + 98 = 195, и это int.
🔹 Оператор + вычисляется слева направо. Как только слева оказывается String, дальше идёт конкатенация, поэтому "" + 'a' + 'b' даёт "ab".
🔹 В 'a' + 'b' + "" сначала складываются числа (195), и только потом результат превращается в строку "195".
Связанные ловушки:
⚠️ char c = 'a' + 1; компилируется и даёт 'b', потому что это константа и она помещается в char. А char d = c + 1; уже не компилируется: результат int, а сужение нужно делать явно.
⚠️ sb.append('a' + 'b') у StringBuilder добавит "195", а не "ab".
⚠️ System.out.println('a' + 'b') вызовет перегрузку println(int).
Вопрос, на котором путаются даже опытные разработчики 🤯
int i = 0;
i = i++;
System.out.println(i); // 0
Инкремент выполнился, но результат всё равно 0. Разбираемся, что делает JVM.
Что происходит в байткоде:
Строка i = i++ компилируется в три инструкции:
1️⃣ iload_1 кладёт текущее значение i (ноль) на стек операндов.
2️⃣ iinc 1, 1 увеличивает саму переменную, теперь i = 1.
3️⃣ istore_1 берёт со стека старый ноль и записывает его обратно в i.
Итог: единица затёрта, i снова 0.
Главное правило:
🔹 i++ (постфиксный) сначала возвращает старое значение, потом увеличивает.
🔹 ++i (префиксный) сначала увеличивает, потом возвращает новое.
Это прямо прописано в спецификации языка (JLS 15.14.2), так что поведение одинаковое на любой JVM. В C такая строка вообще неопределённое поведение, а Java чётко задаёт порядок вычислений.
В Java есть две независимые иерархии потоков:
• Байтовые (InputStream/OutputStream) — работают с byte. Подходят для бинарных данных: изображения, архивы, PDF, сетевые протоколы. Они ничего не знают о кодировках — просто читают и пишут байты.
• Символьные (Reader/Writer) — работают с char (UTF-16 внутри Java). Подходят для текста: файлы, JSON, CSV, HTML. Они обязаны знать кодировку, чтобы правильно преобразовать байты в символы.
Связь между ними — мосты:
• InputStreamReader — читает байты, декодирует в символы.
• OutputStreamWriter — кодирует символы в байты.
Как каждая коллекция вставляет элемент в Java:
🟦 ArrayList: add(i, x) сдвигает весь хвост массива, это O(n). Зато get(i) за O(1) и дружба с кэшем процессора.
🔗 LinkedList: сама вставка O(1), но сначала надо дойти до нужного узла, а это O(n). Плюс по объекту-узлу на каждый элемент.
⭕ ArrayDeque: кольцевой буфер, head и tail просто бегут по кругу. Добавление и удаление с обоих концов за O(1), без сдвигов.
Итог: по умолчанию ArrayList, для очереди и стека ArrayDeque.
IDENTITY — самая популярная стратегия генерации ID. Но с batch insert у неё особые отношения. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не терять производительность на массовых вставках — эта ловушка всплывает при импорте данных.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает стратегии генерации ID.
Джун пишет авто-конфигурацию и объявляет дефолтный бин PaymentClient без условий. Пока пользователь не добавил свой бин — всё работает. Как только добавил — в контексте два бина одного типа, и приложение падает с NoUniqueBeanDefinitionException или BeanDefinitionOverrideException. Сеньор добавляет @ConditionalOnMissingBean, и дефолт аккуратно отключается.
💡 Что такое @ConditionalOnMissingBean?
Это условная аннотация Spring Boot. Она говорит: «Создай этот бин, только если в контексте нет бина указанного типа (или имени)». Чаще всего используется в авто-конфигурациях, чтобы дать разумный дефолт, который пользователь может заменить своим бином.
По умолчанию тип выводится из возвращаемого типа метода. Можно указать явно: @ConditionalOnMissingBean(PaymentClient.class). Проверка идёт по типу, а не только по имени бина. Поэтому если пользователь назвал свой бин customPaymentClient, но тип тот же PaymentClient, дефолт всё равно отключится.
⚠️ Нюанс: Условие вычисляется в момент обработки конфигурации, поэтому порядок имеет значение. Если бин пользователя объявлен позже, условие может не увидеть его. В авто-конфигурациях Spring Boot это решается через @AutoConfigureAfter/@AutoConfigureBefore. Также не ставь @ConditionalOnMissingBean на бины, которые нужны всегда: если кто-то случайно объявит бин того же типа, дефолт исчезнет, и приложение может упасть с NoSuchBeanDefinitionException. И помни: в Spring Boot 2.1+ переопределение бинов по умолчанию запрещено (spring.main.allow-bean-definition-overriding=false), поэтому «тихое» переопределение больше не сработает.
SELECT * FROM users LIMIT 2 выглядит предсказуемо. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не ловить «плавающие» баги в пагинации — эта ловушка встречается в каждом втором проекте.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает стандарт SQL.
💡 Что такое WeakHashMap?
WeakHashMap — это реализация Map, где ключи хранятся как weak reference (слабая ссылка). Если на ключ не остаётся сильных ссылок, сборщик мусора может удалить ключ, а вместе с ним и запись из мапы. Это делает WeakHashMap идеальным для кэшей, где ключи — временные объекты: метаданные классов, загруженные через ClassLoader, временные сессии, объекты, которые больше не используются в программе.
В отличие от HashMap, WeakHashMap не предотвращает сборку мусора для ключей. Когда GC очищает ключ, запись автоматически удаляется из мапы. При следующем обращении к WeakHashMap (например, size(), get(), put()) вызывается внутренняя очистка, и «мёртвые» записи исчезают.
✅ Когда использовать?
• Кэш, где ключ — объект, который может быть удалён GC (например, ClassLoader, Thread, временный объект).
• Хранение метаданных, привязанных к объектам, которые не должны жить вечно.
• Любая ситуация, где утечка памяти из-за долгоживущих ключей недопустима.
⚠️ Нюанс: WeakHashMap не потокобезопасен. Для многопоточного использования оберни в Collections.synchronizedMap или используй ConcurrentHashMap с WeakReference вручную. Также помни: если значение содержит сильную ссылку на ключ, запись никогда не удалится — это классическая утечка. И System.gc() не гарантирует немедленную очистку: GC может отложить сборку, но при следующем обращении к мапе «мёртвые» записи будут удалены. Для кэшей с ограничением по памяти лучше использовать SoftReference или библиотеки вроде Caffeine.
@Transactional обещает откат при ошибке... В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
📌 Сохрани пост, чтобы не оставлять «полузаписанные» данные в БД — эта ловушка встречается в реальных сервисах постоянно.
💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила отката в Spring.
Джун пишет шесть @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 вложенного — это позволяет настроить общий контекст, а потом уточнить его для группы.
@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.
