Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Ko'proq ko'rsatish📈 Telegram kanali Java: fill the gaps analitikasi
Java: fill the gaps (@java_fillthegaps) Rus til segmentidagi kanali faol ishtirokchi. Hozirda hamjamiyat 12 549 obunachidan iborat bo'lib, Texnologiyalar & Aralashmalar toifasida 10 121-o'rinni va Rossiya mintaqasida 52 862-o'rinni egallagan.
📊 Auditoriya ko‘rsatkichlari va dinamika
невідомо sanasidan buyon loyiha tez o‘sib, 12 549 obunachiga ega bo‘ldi.
07 Iyun, 2026 dagi oxirgi ma’lumotlarga ko‘ra kanal barqaror faollikka ega. Oxirgi 30 kunda obunachilar soni -46 ga, so‘nggi 24 soatda esa 0 ga o‘zgardi va umumiy qamrov yuqori darajada qolmoqda.
- Tasdiqlash holati: Tasdiqlanmagan
- Jalb etish (ER): Auditoriya o‘rtacha 34.72% darajada jalb etiladi. Nashrdan keyingi dastlabki 24 soatda kontent odatda umumiy obunachilar sonining N/A% ini tashkil etuvchi reaksiyalarni to‘playdi.
- Post qamrovi: Har bir post o‘rtacha 0 marta ko‘riladi; birinchi sutkada odatda 0 ta ko‘rish yig‘iladi.
- Reaksiyalar va o‘zaro ta’sir: Auditoriya faol: har bir postga o‘rtacha 0 ta reaksiya keladi.
- Tematik yo‘nalishlar: Kontent redis, hashmap, linkedhashmap, индекс, фича kabi asosiy mavzularga jamlangan.
📝 Tavsif va kontent siyosati
Muallif resursni shaxsiy fikrni ifoda etish maydoni sifatida ta’riflaydi:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Yuqori yangilanish chastotasi (oxirgi ma’lumot 08 Iyun, 2026 da olingan) sababli kanal doimo dolzarb va katta qamrovli bo‘lib qoladi. Analitika auditoriya kontent bilan faol hamkorlik qilishini, uni Texnologiyalar & Aralashmalar toifasidagi muhim ta’sir nuqtasiga aylantirishini ko‘rsatadi.
final int count = 100;
count всегда будет равен 100. Каждый, кто напишет
count = 200;будет осуждён компилятором. Для ссылок схема такая же:
final User admin = User.createAdmin();Ссылка
admin всегда будет указывать на объект User с параметрами админа. Никто не может её переприсвоить:
❌ admin = new User(…)Effectively final называется переменная, значение которой не меняется после инициализации. По сути это тот же final, но без ключевого слова. Чтобы компилятор не ругался, надо выполнить два условия: 1️⃣ Локальная переменная однозначно определена до начала лямбда-выражения Так не скомпилируется:
int x; if (…) х = 10Вот так норм:
int x; if (…) х = 10; else х = 15;2️⃣ Переменная не меняется внутри лямбды и после неё
int х = 10; …лямбда… ❌ х = 15 User user = … …лямбда… ❌ user = userRepository.findByName(…) ✅ user.setTIN(…)❓ Зачем нужно такое ограничение? JLS 15.27.2 говорит, что ограничение помогает избежать многопоточных проблем: The restriction to effectively final variables prohibits access to dynamically-changing local variables, whose capture would likely introduce concurrency problems С первого взгляда звучит разумно. Основное применение лямбд — в рамках Stream API. В Stream API есть опция parallel(), которая запускает выполнение в разных потоках. Там и возникнут concurrency problems. Но я не принимаю это объяснение, потому что: 🤔 С каких пор компилятор волнуют многопоточные проблемы? Вся многопоточка отдана под контроль разработчика с начала времён 🤔 Если локальная переменная станет полем класса, то компилятор перестанет ругаться. При этом вероятность concurrency problems увеличится в разы Моя гипотеза: требование final/effectively final связано с особенностями реализации лямбд и ограничением модели памяти. Это технические сложности в JVM и ничего больше. Отсутствие многопоточных проблем, о которых говорится в JLS, это всего лишь следствие, а не причина. ❓ Как же менять переменные внутри лямбд? 1️⃣ Сделать переменную полем класса:
int count;
public void m() {
list.forEach(v -> count++);
}
Не лучший вариант, переменная доступна теперь другим потокам. Concurrency problems!
2️⃣ Использовать Atomic обёртку
Для примитивов:
AtomicInteger count = new AtomicInteger(0); list.forEach(v -> count.incrementAndGet())Для ссылок:
AtomicReference<User> user = new AtomicReference<>(); …map(i -> user.set(…))3️⃣ Использовать массив с одним элементом
int[] res = new int[] {0};
list.forEach(v -> res[0]++);
Популярный вариант, который подходит и для примитивов, и для ссылок. Но мне больше нравится вариант с Atomic:)List<T> asList(T… a)Метод ждёт на вход объекты ссылочных типов. В случае stringArr всё ок, передаются 4 ссылки на объект String, и создаётся список с 4 элементами. Во второй части ситуация менее однозначная. Для массивов и коллекций не работает автоматическое приведение типов и боксинг/анбоксинг: ❌ List<Child> не приводится автоматически к List<Parent> ❌ Массив int не приводится автоматически к массиву Integer В метод уже приходит ссылка — ссылка на массив примитивов. JVM всё устраивает, и она создаёт List из ссылок на массив. Получится
List<int[]>, в котором будет один элемент — ссылка на {1,2,3}. Массив с числом не сравнить, поэтому ответ на вопрос перед постом: true false
⭐️ Заметка 1: немного смуты здесь вносит var. Если писать целиком
List<Integer> res = Arrays.asList(intArray)то компилятор сразу укажет на несоответствие типов ⭐️ Заметка 2: сигнатура contains выглядит так:
boolean contains(Object o)Метод примет что угодно — строку, примитив (здесь выполнится боксинг) или экземпляр StringBuilder. Поэтому ошибок компиляции нет ❓ Как получить нормальный список из массива примитивов?
var intList = Arrays.stream(intArray).boxed().collect(toList());Закончу на оптимистичной ноте. В рамках Project Valhalla в JVM добавят три новых типа данных: value objects, primitive classes, specialized generics. В двух словах об этом не рассказать, но есть шанс, что через 10 лет код выше будет работать, как ожидается🤭
error — ожидаемые ошибки, с которыми можно справиться. Например, файл не найден или у даты неверный формат
Многие методы возвращают пару результат-ошибка:
result, err := process()Сразу после вызова метода проверяем, всё ли ок:
if err != nil { обработка ошибки }
Что не нравится:
❌ Если метод возвращает разные ошибки, то разработчику нужно самому найти в исходном коде возможные варианты и написать что-то вроде “если ошибка типа А, то … , если типа Б, то …”
❌ Большинство ошибок в стандартной библиотеке — обычный error с текстом. Обрабатывать в таком виде очень неудобно
❌ Компилятор не требует обработки ошибок
🔹 panic — непоправимые ошибки, например, выход на пределы массива.
Аналог RuntimeException, паника поднимается по стеку вызовов, пока не встретит обработчик. Если не встретит, программа завершается.
Что не нравится:
❌ Большинство паник содержат просто строчку с текстом
❌ Обработчики ловят всё подряд
Эти недостатки вижу не только я. Для Go версии 2 (сейчас 1.19) идут активные обсуждения, как сделать работу с ошибками лучше.
2️⃣ Оригинальное форматирование
В большинстве языков для перевода даты в строку и обратно используются шаблоны типа yyyy-MM-dd
Go выбрал другой путь. Задать формат — значит написать, как выглядит в этом представлении 2 января 2006 года, время 15:04:05.
Пример: чтобы отформатировать переменную dateTime в виде "сначала время через двоеточие, потом дата через дефис" пишем
currentTime.Format("15:04:05 02-01-2006"))
Почему такая дата? Потому что в американском формате это 01.02 03:04:05 06 -07 (месяц, число, время, год, часовой пояс)
Какие преимущества у такого оригинального форматирования? Абсолютно никаких.
К счастью, есть сторонние библиотеки, работающие с привычным yyyy-MM-dd
3️⃣ Неудобная работа с наборами элементов
Коллекции в java — прекрасные абстракции для обработки данных. Например, ArrayList — коллекция, в основе которой лежит массив с конечной длиной. ArrayList скрывает часть сложности — если нужно добавить элемент в середину, то все манипуляции с массивом ArrayList берёт на себя.
А с появлением Stream API работать с данными — сплошное удовольствие🥰
В Go абстракция над массивом называется slice (срез), у неё есть буфер и довольно специфичное поведение.
(если вы заинтригованы, рекомендую этот текст и это видео)
Целевые кейсы по работе с данными в Go скорее всего отличаются от джавовских. Поэтому и набор методов другой. Например, в стандартной библиотеке нет метода contains. Если надо — пиши сам:
for _, a := range s {
if a == e { return true }
}
putIfAbsent, indexOf, isEmpty, метод вставки в начало/середину — всего этого нет.
Абстракции в Go, кажется, хорошо подходят для оконных функций (посчитать среднее за 5 минут, максимальное за час) или случаев, когда исходные данные не меняются.
Но для энтерпрайзных ситуаций это выглядит сложно, неудобно и ненадёжно.
❓ Что дальше?
Для меня в обучении интересно не только прокачать hard skills, но и набраться новых идей. Даже простое изучение синтаксиса привело к каким-то мыслям, так что продолжу разбираться с Go. В планах изучить основные библиотеки, паттерны и лучшие практики. Посмотреть на внутрянку, бенчмарки, изучить продакшн кейсы.
Об этом писать не буду, канал всё-таки про джаву. Но если встречу что-нибудь интересное, чего нет в java инфополе, то обязательно поделюсь:)name, count := processUser(user)Подобные штуки доступны и в других языках, например, в Python. Вернуть два значения — сверхпопулярный кейс, во многих java проектах для этих целей используют Map.Entry или создают класс Pair. До сих пор не понимаю, почему в джаве нельзя вернуть пару. Технически это не должно быть сложно, можно сделать что-то среднее между дженериками и LambdaMetaFactory. Или добавить класс Pair в стандартную библиотеку. 2️⃣ Нет наследования Только интерфейсы и композиция. Никаких проблем с абстрактными классами и сложными иерархиями. Одобряю👌 3️⃣ Объект можно передать по ссылке и по значению В java всё однозначно: ▪️
void m(int value) — примитив копируется и манипуляции с value не отразятся на переданной переменной
▪️ void m(User user) — ссылка копируется, но указывает на тот же объект
В Go вариантов больше:
▫️ func m(value int) — примитив копируется как в джаве
▫️ func m(value *int) — передаём ссылку на примитив, внутри метода ей можно присвоить другое значение
▫️ func m(value User) — в метод передаётся полная копия объекта
▫️ func m(value *User) — передаём исходную ссылку на объект. Её можно переприсвоить новому объекту, и сам объект, конечно, можно менять
4️⃣ Оператор select
для получения самого быстрого результата от асинхронных задач.
Как это выглядит: допустим, мы отправили три задачи в асинхронное исполнение. Пишем:
select {
результат задачи 1: код А
результат задачи 2: код Б
результат задачи 3: код Ц
}
Какая задача первой вернёт результат, такой код и выполнится. При этом нам сразу доступен результат завершённой задачи.
Самый близкий java аналог — конструкция
CompletableFuture.anyOf(задача1, задача2, задача3).thenRun(код)Код в
thenRun выполнится, когда одна из задач завершится. Затем нужно пройтись по всем объектам CompletableFuture, чтобы выяснить, какая именно задача завершилась, и забрать у неё результат. В go эту задачу выполнить гораздо проще.
За кадром осталось много конструкций, которые выглядят по-другому, но я пока не поняла, чем они лучше аналогов в java. Возможно, когда перейду к изучению лучших практик, плюсы станут более весомыми. А может и нет:)
Но не всё так радужно, и в следующем посте опишу особенности Go, которые мне НЕ понравились😈name, count := processUser(user)Подобные штуки доступны и в других языках, например, в Python. Вернуть два значения — сверхпопулярный кейс, во многих java проектах для этих целей используют
Map.Entry или создают класс Pair.
До сих пор не понимаю, почему в джаве нельзя вернуть пару. Технически это не должно быть сложно, можно сделать что-то среднее между дженериками и LambdaMetaFactory. Или добавить класс Pair в стандартную библиотеку.
2️⃣ Нет наследования
Только интерфейсы и композиция. Никаких проблем с абстрактными классами и сложными иерархиями. Одобряю👌
3️⃣ Объект можно передать по ссылке и по значению
В java всё однозначно:
▪️ void m(int value) — примитив копируется и манипуляции с value не отразятся на переданной переменной
▪️ void m(User user) — ссылка копируется, но указывает на тот же объект
В Go вариантов больше:
▫️ func m(value int) — примитив копируется как в джаве
▫️ func m(value *int) — передаём ссылку на примитив, внутри метода ей можно присвоить другое значение
▫️ func m(value User) — в метод передаётся полная копия объекта
▫️ func m(value *User) — передаём исходную ссылку на объект. Её можно переприсвоить новому объекту, и сам объект, конечно, можно менять
4️⃣ Оператор select
для получения самого быстрого результата от асинхронных задач.
Как это выглядит: допустим, мы отправили три задачи в асинхронное исполнение. Пишем:
select {
результат задачи 1: код А
результат задачи 2: код Б
результат задачи 3: код Ц
}
Какая задача первой вернёт результат, такой код и выполнится. При этом нам сразу доступен результат завершённой задачи.
Самый близкий java аналог — конструкция
CompletableFuture.anyOf(задача1, задача2, задача3).thenRun(код)Код в
thenRun выполнится, когда одна из задач завершится. Затем нужно пройтись по всем объектам CompletableFuture, чтобы выяснить, какая именно задача завершилась, и забрать у неё результат. В go эту задачу выполнить гораздо проще.
За кадром осталось много конструкций, которые выглядят по-другому, но я пока не поняла, чем они лучше аналогов в java. Возможно, когда перейду к изучению лучших практик, плюсы станут более весомыми. А может и нет:)
Но не всё так радужно, и в следующем посте опишу особенности Go, которые мне НЕ понравились😈
За кадром осталось много конструкций, которые выглядят по-другому, но я пока не поняла, чем они лучше аналогов в java. Возможно, когда перейду к изучению лучших практик, плюсы станут более весомыми. А может и нет:)
Но не всё так радужно, и в следующем посте опишу особенности Go, которые мне НЕ понравились😈WebSecurityConfigurerAdapter#configure(HttpSecurity http)
Чтобы увидеть процесс наглядно, поставьте брейкпойнт в методе FilterChainProxy#doFilterInternal. Там увидите все фильтры, можете погулять по ним и отследить, что происходит с запросом
2️⃣ Авторизация/аунтефикация
скрывается за фасадом — бином AuthenticationManager.
Чтобы авторизовать пользователя, в коде проекта нужно вызвать метод
authenticationManager.authenticateМожет в фильтре, может в контроллере, зависит от механизма безопасности. Метод
authenticate пройдётся по всем заданным источникам авторизации. Результат можно положить в SecurityContextHolder, и он будет доступен из любого места в коде. Также бросится событие AuthenticationSuccessEvent, но обычно его игнорируют:)
Источники авторизации называются *Provider (например, DaoAuthenticationProvider) и определяются в конфиге. В итоге всё что нужно от разработчика это:
☝️определить провайдеры
✌️ вызвать authenticate в нужном месте
Посмотреть список текущих провайдеров: брейкпойнт в методе ProviderManager#authenticate.
Задать список провайдеров: определить в конфиге WebSecurityConfigurerAdapter метод configure(AuthenticationManagerBuilder auth)
Что с этим знанием делать на практике
Если вам дали задачу на настройку Spring Security, не бросайтесь сразу гуглить
spring security jwt/oauth/ldap example
Велик шанс сделать что-нибудь не то. Лучше сделать так:
Шаг 1 (самый важный): разобраться в механизме безопасности, который нужно реализовать. Что, откуда и как передаётся, где и как валидируется и сохраняется
Шаг 2: прикинуть, как эти требования ложатся на архитектуру секьюрити. Что сделать в фильтрах, а что отдельным методом
Шаг 3 (желательный): обсудить полученное решение со старшим товарищем
Теперь можно гуглить и выбрать подходящий туториал. Поняв, что именно искать, вы в итоге потратите меньше времени и сохраните высокую самооценку🙂Parent и Child не переопределяют друг друга и относятся к разным классам. Над методом в классе Child нельзя поставить Override и вызвать внутри super.getName();
Поэтому правильный ответ в опросе выше — "Parent". Метод определяется во время компиляции по типу указателя.
❓ Что из этого можно вынести?
Если посмотреть на getName, то непонятно, статический он или обычный, учитывается тип экземпляра или нет. Это затрудняет чтение кода и считается плохой практикой. Настолько плохой, что Intellij IDEA даже не показывает статические методы в выпадающем списке для объекта.
Поэтому best practice — вызывать статические методы, обращаясь к классу: Parent.getName()Теперь о грустном. Вопрос про связывание часто входит в списки java interview questions, но почти все статьи содержат неверную информацию. Пишут, что ▪️ Статическое связывание используют
private, final, static методы и конструкторы
▪️ Динамическое — методы интерфейсов и перегруженные методы
Кажется логичным, но давайте проверим. Для разных типов связывания используется разные инструкции байткода. Посмотреть их можно через консоль
javap -c -v LovelyService.classили в IDEA: View → Show Bytecode Нас интересуют инструкции invoke*. Немного поиграв с кодом можно увидеть, что для public, protected, private и final методов используется
invokevirtual — динамическая типизация. Статические методы используют инструкцию invokestatic.
Сразу возникает вопрос:
❓ Почему для private и final методов используется динамическое связывание? Ведь метод точно не переопределяется и это известно во время компиляции
Для final ответ кроется в спецификации java, пункт 13.4.17. Суть такая: final метод может однажды перестать быть final, и кто-то может его переопределить. Когда класс, который переопределил метод, будет взаимодействовать со старым байткодом, то ничего не должно сломаться.
Правила работы с private методами описаны в спецификации JVM, пункт 5.4.6. Причина использования invokevirtual не указана, но подозреваю, что ситуация как у final
❓ Зачем это знать?
Для написания кода это абсолютно не важно. Но это яркий пример некорректной информации, которая бездумно копируется в джуниорские опросники👎
А вот на курсе многопоточки мы постоянно лазаем по исходникам java.util.concurrent, поэтому инфа абсолютно живая и актуальная. Минутка рекламы, но почему бы и нет:) Присоединяйтесь: https://fillthegaps.ru/mt6
Endi mavjud! Telegram Tadqiqoti 2025 — yilning asosiy insaytlari 
