💡 Что такое 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 может ввести в заблуждение.
Shorts
- 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-исключения, чем родительский.
- Константный hashCode не сломает HashMap сразу, но заставит её работать как LinkedList. Сколько элементов сохранится и почему это плохо? В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не убить производительность своих мап — это классическая ловушка. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про коллизии.
- Джун блокирует поток через Thread.sleep для каждой отложенной задачи. Сеньор использует DelayQueue — задачи ждут своего времени, не мешая другим. Разбираем за 16 секунд. 💡 Почему sleep — плохо для отложенных задач? Thread.sleep(ms) блокирует весь поток на указанное время. Если нужно выполнить 10 задач с задержкой в 5 секунд, получишь 50 секунд полного простоя. Поток не может делать ничего другого. Кроме того, sleep не даёт точного порядка: задачи выполняются строго по очереди, даже если их время пришло одновременно. DelayQueue — это очередь, где каждый элемент реализует интерфейс Delayed с методом getDelay(). Поток вызывает take() и блокируется только до момента, когда первый элемент станет готов. Если элементы ещё не готовы, поток ждёт, но как только первый «созрел» — получает его. Одновременно готовые задачи извлекаются в порядке getDelay(). Это эффективно и не блокирует поток зря. ⚠️ Нюанс: DelayQueue не гарантирует порядок для элементов с одинаковым временем — только по getDelay(). Если нужна строгая последовательность, добавьте счётчик или используйте PriorityQueue с Delayed.
- Массивы в Java ведут себя не так, как дженерики. Одно безобидное присваивание может уронить программу в рантайме. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не попасться на ArrayStoreException — это частая проблема при рефакторинге и наследовании. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про ковариантность.
- 💡 Что такое Serial GC? Serial GC — это самый базовый сборщик мусора в HotSpot JVM. Он выполняет сборку мусора в одном потоке, что означает полную остановку всех потоков приложения (stop-the-world) на время очистки. Отсутствие многопоточности уменьшает накладные расходы на синхронизацию и делает его эффективным для небольших куч (примерно до 100-200 МБ). Включается флагом -XX:+UseSerialGC. ✅ Когда использовать? • Маленькие клиентские приложения, где куча невелика. • Встраиваемые системы, микроконтроллеры (если Java там вообще используется). • Контейнеры с жёсткими ограничениями памяти и CPU. • Тестирование или простые скрипты, где паузы не важны. ⚠️ Нюанс: Для серверных приложений с большой кучей и многопоточностью Serial GC не подходит: паузы будут слишком длинными. В таких случаях выбирают Parallel GC, G1, ZGC или Shenandoah, которые распределяют работу между потоками или делают паузы минимальными.
- PriorityQueue — отличный выбор для очереди с приоритетом, но есть нюанс с null. Один вызов add может уронить программу. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не наступить на NPE при работе с очередями — это случается чаще, чем хотелось бы. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает ограничения коллекций.
- Джун создаёт новый BigInteger для единицы каждый раз, когда она нужна. Сеньор использует константу BigInteger.ONE. Разбираем за 16 секунд. 💡 Почему new BigInteger("1") — плохо? BigInteger — неизменяемый класс. Это значит, что объект BigInteger.ONE можно безопасно переиспользовать в любом месте, где нужна единица. Создание нового объекта через new BigInteger("1") каждый раз приводит к лишним аллокациям, особенно в циклах или часто вызываемых методах. В больших системах это увеличивает нагрузку на сборщик мусора. Кроме того, BigInteger.ONE — это стандартная константа, которая делает код понятнее. ✅ Как правильно? Используй готовые константы: • BigInteger.ZERO • BigInteger.ONE • BigInteger.TEN Они создаются при загрузке класса и доступны везде.
- remove в ArrayList перегружен: один метод принимает int, другой Object. Когда в списке Integer, вызов remove(1) может удалить не то, что вы ожидали. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не путать удаление по индексу и по значению — это классическая ловушка с автоупаковкой. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает приоритет сигнатур.
- 💡 Какие состояния есть у потока? Java определяет шесть состояний в Thread.State: • NEW — поток создан, но start() ещё не вызван. • RUNNABLE — поток готов к выполнению или выполняется. Важно: это не значит, что он прямо сейчас на процессоре — он может ждать своей очереди у планировщика. • BLOCKED — поток ждёт освобождения монитора (вход в synchronized блок). • WAITING — поток ждёт бессрочно, пока другой поток не вызовет notify() или notifyAll() (через wait(), join() без таймаута). • TIMED_WAITING — поток ждёт с таймаутом (sleep(ms), wait(ms), join(ms)). • TERMINATED — метод run() завершён. Поток нельзя перезапустить. ✅ Что важно помнить: • BLOCKED и WAITING — принципиально разные состояния. BLOCKED связан с монитором, WAITING — с ожиданием сигнала. • getState() возвращает текущее состояние. • Поток в TERMINATED нельзя запустить заново — только создать новый. ⚠️ Нюанс: RUNNABLE объединяет два подсостояния: «готов к запуску» и «выполняется». Разделить их из Java нельзя, но операционная система это делает.
- CompletableFuture.allOf — удобный способ дождаться нескольких асинхронных задач, но если одна из них завершится с ошибкой, поведение может удивить. В видео — короткий пример и четыре варианта. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не пропустить исключения при использовании allOf — это частая ошибка в асинхронном коде. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает тонкости.
- Джун читает весь файл целиком через Files.readAllBytes и получает OutOfMemoryError на больших данных. Сеньор использует буферизованное чтение и обрабатывает файл порциями. Разбираем за 16 секунд. 💡 Почему readAllBytes опасен? Files.readAllBytes загружает всё содержимое файла в массив байт. Если файл больше доступной памяти, приложение падает с OutOfMemoryError. Даже если файл меньше, но таких файлов много, память быстро заканчивается. Для больших файлов нужно использовать потоковое чтение: BufferedReader, Files.newInputStream с буфером, или lines() для текстовых файлов. Это позволяет обрабатывать данные по мере поступления, не загружая всё сразу. ✅ Как правильно? • Для текстовых файлов: Files.newBufferedReader(path).lines() — ленивый стрим строк. • Для бинарных: try (InputStream is = Files.newInputStream(path)) { byte[] buf = new byte[8192]; ... } • Для очень больших: FileChannel с ByteBuffer. ⚠️ Нюанс: Даже Files.readAllLines загружает все строки в память. Для больших файлов используйте BufferedReader.lines() — он читает лениво и не держит весь файл в памяти.
- Ссылки на методы — удобная штука, но checked-исключения могут всё испортить. Один метод с throws — и компилятор не пускает. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не спотыкаться о checked-исключения в стримах — это происходит чаще, чем хотелось бы. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает ограничения method references.
- 💡 Почему String.join лучше ручного цикла? String.join — это статический метод класса String, добавленный в Java 8. Он принимает разделитель и коллекцию (или массив) строк, и возвращает единую строку, в которой элементы соединены этим разделителем. Метод сохраняет порядок элементов, а внутри использует StringBuilder, что делает его эффективным. Ручной цикл с конкатенацией не только многословен, но и создаёт лишние промежуточные объекты, если использовать + в цикле. String.join снимает эти проблемы. ✅ Как правильно? • Для склейки коллекции: String.join(", ", list). • Для массива: String.join("-", arr). • Для произвольного набора: String.join(":", "A", "B", "C"). ⚠️ Нюанс: String.join не пропускает null — он превращает его в строку "null". Если нужно игнорировать null-элементы, используйте stream().filter(Objects::nonNull).collect(Collectors.joining(", ")).
- Статические поля ведут себя не так, как обычные. При наследовании они могут скрываться, и обращение через ссылку родителя может вернуть не то, что вы ожидаете. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не запутаться в статических полях при наследовании — это важный нюанс для понимания JVM. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает про hiding.
- 💡 Почему record неизменяемый? Record (введён в Java 14, стабилизирован в Java 16) спроектирован как носитель неизменяемых данных. Все поля неявно объявлены как private final, поэтому присвоить им новое значение после создания невозможно. Это гарантирует потокобезопасность и предсказуемость. Чтобы "изменить" данные, нужно создать новый объект с обновлёнными значениями. Для этого добавляют методы-хелперы, которые возвращают новый экземпляр, например withAge(int) или withName(String). ✅ Как правильно? Сеньор добавляет в record метод, который возвращает копию с изменённым полем: record Person(String name, int age) { Person withAge(int newAge) { return new Person(name, newAge); } } Теперь обновление выглядит как person.withAge(31), а исходный объект остаётся неизменным. ⚠️ Нюанс: В Java нет встроенного механизма "withers", но вы можете сгенерировать такие методы вручную или через библиотеки (например, Lombok @With). Альтернатива — использовать обычный класс с сеттерами, если изменяемость действительно нужна.
- Лямбды умеют захватывать переменные, но не все. Одна маленькая операция может превратить рабочий код в ошибку компиляции. В видео — короткий пример и четыре варианта ответа. Выбери свой, а завтра я покажу правильный разбор. 📌 Сохрани пост, чтобы не путаться с effectively final при написании лямбд — это частая ошибка новичков и не только. 💬 Напиши в комментариях, какой вариант выбрал — посмотрим, кто знает правила замыкания.
