Java Talks
Иконка канала Java Talks

Java Talks

3 подписчика

277просмотров

Родитель 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 — один раз, от родителя к наследнику. Объект — каждый раз, тоже от родителя к наследнику.

366просмотров

Как устроен 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+), лишних расширений не будет

405просмотров

Ждёшь 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).

341просмотр

Вопрос, на котором путаются даже опытные разработчики 🤯 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 чётко задаёт порядок вычислений.

243просмотра

В Java есть две независимые иерархии потоков: • Байтовые (InputStream/OutputStream) — работают с byte. Подходят для бинарных данных: изображения, архивы, PDF, сетевые протоколы. Они ничего не знают о кодировках — просто читают и пишут байты. • Символьные (Reader/Writer) — работают с char (UTF-16 внутри Java). Подходят для текста: файлы, JSON, CSV, HTML. Они обязаны знать кодировку, чтобы правильно преобразовать байты в символы. Связь между ними — мосты: • InputStreamReader — читает байты, декодирует в символы. • OutputStreamWriter — кодирует символы в байты.

3просмотра

Как каждая коллекция вставляет элемент в Java: 🟦 ArrayList: add(i, x) сдвигает весь хвост массива, это O(n). Зато get(i) за O(1) и дружба с кэшем процессора. 🔗 LinkedList: сама вставка O(1), но сначала надо дойти до нужного узла, а это O(n). Плюс по объекту-узлу на каждый элемент. ⭕ ArrayDeque: кольцевой буфер, head и tail просто бегут по кругу. Добавление и удаление с обоих концов за O(1), без сдвигов. Итог: по умолчанию ArrayList, для очереди и стека ArrayDeque.

1просмотр

IDENTITY — самая популярная стратегия генерации ID. Но с batch insert у неё особые отношения. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не терять производительность на массовых вставках — эта ловушка всплывает при импорте данных. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает стратегии генерации ID.

4просмотра

Джун пишет авто-конфигурацию и объявляет дефолтный бин 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), поэтому «тихое» переопределение больше не сработает.

3просмотра

SELECT * FROM users LIMIT 2 выглядит предсказуемо. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не ловить «плавающие» баги в пагинации — эта ловушка встречается в каждом втором проекте. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает стандарт SQL.

144просмотра

💡 Что такое 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.

7просмотров

@Transactional обещает откат при ошибке... В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не оставлять «полузаписанные» данные в БД — эта ловушка встречается в реальных сервисах постоянно. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила отката в Spring.

463просмотра

Джун пишет шесть @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 вложенного — это позволяет настроить общий контекст, а потом уточнить его для группы.

2просмотра

@Mock и @Spy — два инструмента Mockito, которые часто путают. Один создаёт заглушку, другой — обёртку над реальным объектом. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не путаться в моках и шпионах — это важно для написания честных тестов. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает разницу.

292просмотра

💡 Что такое 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: он держит файловый дескриптор.

2просмотра

@WebMvcTest — slice-тест, который поднимает не всё приложение, а только часть контекста. Но что именно попадает в этот slice? В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не тратить секунды на медленные тесты — slice-тесты ускоряют CI в разы. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает, что поднимает @WebMvcTest.

199просмотров

Джун оборачивает вызов внешнего сервиса в цикл с тремя попытками и sleep. Пока сервис работает — всё нормально. Когда он не отвечает — каждая ошибка превращается в три запроса и три секунды ожидания. Сеньор ставит @CircuitBreaker из Resilience4j и получает защиту, которая работает умнее. 💡 Что такое Circuit Breaker? Это паттерн «предохранитель». Он работает как электрический автомат: • CLOSED — всё нормально, запросы проходят. • OPEN — слишком много ошибок. Запросы сразу идут в fallback, не доходя до сервиса. • HALF_OPEN — через некоторое время пропускается несколько пробных запросов. Если они успешны — цепь замыкается. Если нет — снова OPEN. Зачем это нужно: • Экономия ресурсов. Не тратим потоки и время на заведомо провальные вызовы. • Защита сервиса. Не создаём дополнительную нагрузку на сервис, который и так не справляется. • Быстрый fallback. Пользователь получает ответ мгновенно, а не ждёт три секунды в цикле.

2просмотра

COUNT(*) и COUNT(column) выглядят почти одинаково, но NULL меняет всё. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не ошибиться в аналитике — эта разница ломает отчёты чаще, чем кажется. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает, как NULL влияет на агрегаты.

2просмотра

Map.of — удобный способ создать неизменяемую мапу. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не полагаться на порядок в Map.of — это частая причина «плавающих» тестов. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает контракт Map.

171просмотр

Джун генерирует число через (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 предсказуем.

4просмотра

Set.of — удобный способ создать неизменяемое множество. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не ловить IllegalArgumentException при создании множеств — это происходит чаще, чем кажется. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает контракт Set.of.