Джун помечает массив как volatile и уверен, что все элементы стали видимыми между потоками. Но volatile даёт гарантию только для самой ссылки, а не для содержимого массива. Сеньор использует AtomicReference и иммутабельные копии, чтобы обеспечить настоящую потокобезопасность. Разбираем за 16 секунд.
Почему volatile не спасает?
Ключевое слово volatile гарантирует, что запись в поле будет немедленно видна другим потокам, а чтение всегда вернёт самое актуальное значение. Но это работает только для самой переменной, которая в нашем случае является ссылкой на массив. Изменение элемента data[0] не приводит к перезаписи ссылки, поэтому другие потоки могут не увидеть обновлённое значение элемента. Это называется «тонкой» видимостью.
✅ Как правильно?
Сеньор использует AtomicReference с иммутабельным массивом: он создаёт копию, изменяет её и атомарно заменяет ссылку на новую. Так гарантируется и видимость, и атомарность всей операции. Альтернативно можно использовать synchronized блок для всех операций с массивом.
⚠️ Нюанс: Если вы работаете с примитивными массивами и хотите изменять отдельные элементы, рассмотрите AtomicIntegerArray или CopyOnWriteArrayList для объектов. Это устранит гонку данных без лишних копий.
Иногда метод, создающий исключение, возвращает null, когда ошибки нет. Но если этот null попадает в throw, происходит неожиданное. В видео — короткий пример и четыре вариан та. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не получить NPE там, где ждёшь своё исключение — это реальная ошибка при работе с ошибками.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает тонкости.
Что такое var?
var — это ключевое слово, добавленное в Java 10 для локального вывода типов. Оно позволяет компилятору автоматически определять тип переменной на основе инициализатора. Например, var list = new ArrayList String(); эквивалентно ArrayList String list = new ArrayList();. При этом Java остаётся статически типизированным языком: тип вычисляется один раз на этапе компиляции и не может измениться. var можно использовать только для локальных переменных с инициализацией, но не для полей, параметров методов или возвращаемых значений.
✅ Как правильно?
• Используй var, когда тип очевиден из правой части: var user = new User();
• Не используй, если тип неясен и это ухудшает читаемость: var result = someMethod();
• Не пытайся использовать var без инициализации или для не-локальных переменных — это ошибка компиляции.
⚠️ Нюанс: var не работает с null без явного приведения, потому что тип нельзя вывести. Также var не может быть использован в лямбдах или дженериках, где требуется явное указание типа.
Кеширование Integer может сыграть злую шутку при синхронизации. Один и тот же объект из пула, две разные переменные — и вы блокируете один монитор, даже не подозревая. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не ловить трудноуловимые блокировки в многопоточном коде — это классика, о которой часто забывают.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про пул Integer.
Джун обходит LinkedList через get(i) в цикле и получает квадратичную сложность. Сеньор использует for-each или Iterator, что работает за линейное время. Разбираем за 16 секунд.
Почему list.get(i) медленный в LinkedList?
ArrayList хранит элементы в массиве, поэтому get(index) выполняется мгновенно (O(1)). Но LinkedList хранит элементы в узлах, каждый из которых ссылается на следующий/предыдущий. Чтобы получить элемент по индексу i, нужно пройти по ссылкам от начала или конца, что занимает O(n) в худшем случае. Если вызывать get(i) внутри цикла для каждого индекса, суммарная сложность становится O(n²), и на больших списках это катастрофически замедляет программу.
✅ Как правильно?
Сеньор обходит LinkedList через for-each (что под капотом использует Iterator) или явный Iterator. Это гарантирует последовательный проход по узлам без повторного поиска, что даёт O(n).
⚠️ Нюанс: Если тебе действительно нужен доступ по индексу, используй ArrayList. А LinkedList хорош для частых вставок и удалений в начале/середине.
Когда класс реализует два интерфейса с одинаковыми default-методами, Java оказывается перед выбором. Решит ли она сама, или потребует вмешат ельства? В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не получить сюрприз при использовании default-методов — это часто встречается при работе с множественным наследованием интерфейсов.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила разрешения конфликтов.
Что такое generics?
Generics (обобщения) появились в Java 5. Они позволяют указать тип элементов коллекции или класса, например List String, и компилятор будет следить, чтобы в этот список не попал Integer. Это ловит ошибки на этапе компиляции, а не в рантайме. Без дженериков приходилось использовать Object и явное приведение типов, что часто приводило к ClassCastException.
✅ Ключевые особенности:
• Типобезопасность: компилятор проверяет типы.
• Стирание типов: в рантайме дженерики исчезают, поэтому List String и List Integer — это один класс. Это ограничивает проверки instanceof и создание массивов обобщённых типов.
• Ограничения типов: можно указать T extends Number, чтобы требовать определённые возможности от типа.
⚠️ Нюанс: Нельзя использовать примитивные типы в дженериках: List int — ошибка, только List Integer. Также нельзя создавать new T(), потому что тип стирается.
✅ Когда использовать?
Для коллекций, API, методов, возвращающих обобщённые типы, и любых ситуаций, где нужна безопасность типов.
Когда есть несколько перегруженных методов, Java должна выбрать самый подходящий. Но как она решает, если присутствуют varargs и более общие типы? В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не путаться в перегрузке с varargs — это может привести к неожиданным вызовам в твоём коде.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила выбора.
Джун пишет три цикла и городит ручные переменные. Сеньор использует IntSummaryStatistics из стрима — одну строку для всех метрик. Разбираем за 16 секунд.
Почему summaryStatistics лучше?
Класс IntSummaryStatistics (и аналогичные для Long и Double) собирает статистику по числам: количество, сумму, среднее, минимум и максимум. Получить его можно через stream().mapToInt(...).summaryStatistics(). Внутри всё оптимизировано: один проход по данным. Ручные циклы же часто приводят к дублированию и ошибкам, особенно если нужно учитывать пустые списки (тогда getAverage() вернёт 0.0).
✅ Как правильно?
Сеньор использует IntSummaryStatistics для комплексной статистики. Для единичных операций можно применять stream().min(), max(), average() отдельно. Главное — не плодить лишние проходы.
⚠️ Нюанс: Если список пустой, getMin() и getMax() вернут Integer.MAX_VALUE и Integer.MIN_VALUE соответственно, а getAverage() — 0.0. Проверяйте getCount() перед использованием.
finally — это блок, который выполняется всегда. Но что будет, если и try, и finally бросают исключение? Какое из них попадёт наружу? В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не терять исключения из try в своих программах — это классическая ошибка.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто понимает finally.
Как работает ForkJoinPool?
ForkJoinPool реализует алгоритм work-stealing. Задача, наследующая RecursiveTask (или RecursiveAction), в методе compute() проверяет, достаточно ли мал фрагмент работы. Если да — выполняет его напрямую. Если нет — делит на две подзадачи: одну отправляет в пул через fork(), другую обрабатывает текущим потоком, а затем через join() ждёт результат. Благодаря work-stealing простаивающие потоки забирают задачи у занятых, что даёт высокую загрузку ядер.
✅ Когда использовать?
• Задача рекурсивная и хорошо делится (сортировка, поиск в дереве, обработка больших массивов).
• Операция CPU-тяжёлая, а не блокирующая (I/O, сеть, сон).
• Нужен контроль над числом потоков — создавайте собственный ForkJoinPool.
⚠️ Нюанс: ForkJoinPool.commonPool() используется внутри parallelStream(), и блокирующие операции в нём могут замедлить всю JVM. Для блокирующих задач используйте ExecutorService.
Сравнение многомерных массивов — ловушка, в которую попадаются даже опытные. Простой equals может соврать. В видео — короткий пример и четыре варианта ответа. Выбери св ой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не путать equals и deepEquals — это частая ошибка на собеседованиях.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто разбирается в массивах.
Джун ❌ пишет цикл и вручную копирует данные.
Сеньор ✅ преобразует список в одну строку через стрим.
Стримы созданы для таких задач. Метод map применяет функцию к каждому элементу и возвращает новый стрим. toList() собирает всё в список. Вся операция — в одной цепочке, без лишних переменных и циклов.
double в Java — это не точная арифметика, и это может сыграть злую шутку. Обычное сложение двух чисел и сравнение с третьим может дать неожиданный результат. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы помнить про погрешность чисел с плавающей точкой — это всплывает в финансовых расчётах.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто понимает IEEE 754.
Абстрактный класс (abstract class) — это класс, который нельзя инстанцировать напрямую. Он может содержать поля (состояние), конструкторы, обычные и абстрактные методы. Он подходит, когда несколько классов имеют общую структуру и поведение.
Интерфейс (interface) — это контракт: набор абстрактных методов, которые класс обязан реализовать. Он не имеет состояния и конструкторов. Один класс может реализовать несколько интерфейсов, решая проблему множественного наследования.
✅ Правило от сеньора:
• Нужен только контракт без общего состояния? → Интерфейс.
• Есть общее состояние или частичная реализация, которую унаследуют несколько классов? → Абстрактный класс.
• Нужно и то, и другое? → Интерфейс + абстрактный базовый класс (паттерн Adapter).
⚠️ Нюанс: С Java 8 интерфейсы могут содержать default-методы с реализацией, но они всё ещё не могут хранить состояние. Если нужны поля — только абстрактный класс.
В Java есть пул строк и метод intern(), который может превратить разные объекты в одну ссылку. Но все ли это понимают? В видео — короткий сниппет и четыре ва рианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не путаться в сравнениях строк — этот нюанс часто встречается на собеседованиях.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто реально знает пул строк.
Почему нельзя игнорировать InterruptedException?
InterruptedException — это сигнал, что кто-то (другой поток, планировщик, shutdown-хук) хочет, чтобы текущий поток прекратил длительную операцию. Когда вы ловите это исключение, JVM сбрасывает флаг прерывания потока. Если просто проглотить исключение и ничего не сделать, код, вызывающий ваш метод, не узнает, что поток был прерван — флаг isInterrupted() вернёт false. В результате поток не завершится, а продолжит работать, игнорируя запрос на остановку. Это превращает поток в «зомби», который нельзя корректно остановить.
✅ Как правильно?
Сеньор всегда восстанавливает флаг прерывания или позволяет исключению проброситься выше. Стандартный паттерн:
java
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // восстанавливаем флаг
}
Или пробросить исключение дальше, объявив throws InterruptedException в сигнатуре метода.
⚠️ Нюанс: Никогда не восстанавливайте флаг, если вы собираетесь пробросить исключение выше — тогда флаг восстановится автоматически. Восстановление нужно именно в тех случаях, когда вы вынуждены «проглотить» исключение на текущем уровне, но хотите, чтобы код выше знал о прерывании.
Удаление элементов из списка в цикле — коварная штука. Один маленький remove — и половина элементов выжила. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор.
Сохрани пост, чтобы не терять элементы в своих циклах — эта ошибка встречается у каждого новичка.
Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про сдвиг индексов.
В чём разница?
Duration измеряет количество времени в наносекундах, секундах, минутах и часах. Он работает с LocalTime, LocalDateTime, Instant — объектами, которые содержат время.
Period измеряет количество времени в годах, месяцах и днях. Он работает с LocalDate и другими объектами без точного времени.
Главное: их нельзя взаимозаменять. Попытка использовать Duration с LocalDate или Period с LocalTime приведёт к ошибке компиляции.
✅ Как правильно?
• Если нужно измерить или добавить часы/минуты/секунды → используй Duration.
• Если нужно измерить или добавить дни/месяцы/годы → используй Period.
• Хранишь интервал во времени (сколько работал сервис) → Duration.
• Считаешь возраст или срок действия → Period.
⚠️ Нюанс: Duration можно получить из разницы двух Instant или LocalDateTime, а Period — из разницы двух LocalDate. Для дат со временем можно использовать Duration.between() или Period.between(), но результат будет разным по смыслу.
Блок finally выполняется всегда, но влияет ли он на уже готовый return? Этот пример встречается на собеседованиях и ставит в тупик даже опытных. Выбери од ин из четырёх вариантов, а завтра я разберу правильный ответ.
Сохрани пост, чтобы не забыть эту тонкость.
Напиши в комментариях свой вариант — интересно, сколько человек попадётся.
