Java: fill the gaps
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк 🔥Тот самый курс по многопочке🔥 https://fillthegaps.ru/mt Комплименты, вопросы, предложения: @utki_letyat
Show more📈 Analytical overview of Telegram channel Java: fill the gaps
Channel Java: fill the gaps (@java_fillthegaps) in the Russian language segment is an active participant. Currently, the community unites 12 549 subscribers, ranking 10 121 in the Technologies & Applications category and 52 862 in the Russia region.
📊 Audience metrics and dynamics
Since its creation on невідомо, the project has demonstrated rapid growth, gathering an audience of 12 549 subscribers.
According to the latest data from 07 June, 2026, the channel demonstrates stable activity. Although there has been a change in the number of participants by -46 over the last 30 days and by 0 over the last 24 hours, overall reach remains high.
- Verification status: Not verified
- Engagement rate (ER): The average audience engagement rate is 34.72%. Within the first 24 hours after publication, content typically collects N/A% reactions from the total number of subscribers.
- Post reach: On average, each post receives 0 views. Within the first day, a publication typically gains 0 views.
- Reactions and interaction: The audience actively supports content: the average number of reactions per post is 0.
- Thematic interests: Content is focused on key topics such as redis, hashmap, linkedhashmap, индекс, фича.
📝 Description and content policy
The author describes the resource as a platform for expressing subjective opinions:
“Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк
🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt
Комплименты, вопросы, предложения: @utki_letyat”
Thanks to the high frequency of updates (latest data received on 08 June, 2026), the channel maintains relevance and a high level of publication reach. Analytics show that the audience actively interacts with content, making it an important point of influence in the Technologies & Applications category.
stream().filter(x -> x<3)В байткоде на месте вызова лямбды появляется инструкция
invokedynamic #2, 0Следуем по указателям из Constant Pool и попадаем в секцию бутстрап-методов. Видим там вызов
LambdaMetafactory.metafactory(…)
Этот фабричный метод возвращает объект CallSite, внутри которого спрятался MethodHandle.
Больше в байткоде нет информации. Понятно, что работа с лямдой - это какое-то перенаправление, а вся магия описана в классе LambdaMetafactory.
Класс лежит в JDK, идём туда и видим:
1️⃣ С помощью ASM библиотечки создаётся внутренний класс. Он реализует интерфейс Predicate и называется как-то так:
class Smth$$Lambda$14/0x0000000800c02460Класс динамический и существует только в памяти. 2️⃣ Создаётся объект этого класса и привязывается к MethodHandle внутри ConstantCallSite. 3️⃣ Все обращения к лямбде переадресуется этому объекту. ❓Чем это лучше анонимных классов? ✅ Класс создаётся и загружается динамически. Кажется, что это долго и сложно, но на деле получается быстрее, чем загрузка файла с диска ✅ Есть кэширование ✅ Новый класс наравне со всеми участвует в JIT оптимизациях ❓Почему нельзя вынести лямбду в отдельный метод и просто вызывать его? Чтобы встроить решение в остальной java код. Если в коде пишется
Predicate p = i -> x<3;То ожидается, что p - это объект. Его можно куда-то передать и вызвать его методы. Логика вроде "назовём это объектом, но на самом деле это метод" - это слишком большое усложнение. Так не работает. Новые фичи должны вписываться в систему, а не обходить её. ❓Зачем столько переадресаций? Почему из бутстрап метода надо идти в другой класс? Нельзя было сделать встроенную инструкцию? Текущий код LambdaMetaFactory со временем изменится. Добавится больше кэширования, семантика лямбд расширится в следующих версиях java. Если вся логика описана в JDK, то гораздо легче поддерживать совместимость версий. Такая вот эпопея. Я весь процесс поняла не с первого раза и даже не со второго. Поэтому решила провести вас маленькими шажками, и описать всё максимально просто.
invokedynamic #7, 02️⃣ В пуле констант находим такую запись
#7 = InvokeDynamic #0:#83️⃣ Идём по ссылкам и попадаем в специальную секцию .class файла под названием BootstrapMethods. Если компилятор использует invokedynamic, то он обязан прописать такой метод. 4️⃣ Там мы находим набор инструкций, который по заданным аргументам укажет на конкретный метод. В итоге всё сводится к получению и вызову MethodHandle. В чём фишка: В других invoke* алгоритм связывания жёстко задан внутри JVM, а для invokedynamic логика связывания прописывается в отдельном методе, который может быть любым. Такая вот общая схема. Дальше идут оптимизации. Например, бутстрап метод возвращает не сам MethodHandle, а более гибкий CallSite. У него три реализации: одна с неизменным MethodHandle и две с переменным invokedynamic был создан для поддержки динамических языков. Но стало понятно, что с ним можно реализовать много классных фич в самой java. Так что на следующей неделе соберём всё вместе и погрузимся в детали реализации лямбда-выражений🤓
javap -c -v UserInfo.classПри первом использовании файл с байткодом загружается с диска и регистрируется внутри JVM. Нам интересны четыре сущности: 1️⃣ ConstantPool - табличка из class-файла с именами полей, методов и аргументов. Выглядит как-то так:
#14=Utf8 Autumn #16=Class #18 #17=NameAndType #19:#202️⃣ Список байткод инструкций
29: getstatic #39 32: aload_3 33: invokevirtual #45Все конкретные методы и аргументы обозначаются через ссылки из Constant Pool. 3️⃣ Табличка виртуальных методов для класса 4️⃣ Табличка с интерфейсами и реализациями Таблицы 3 и 4 лежат внутри JVM в особой структуре, чтобы по ним было легко перемещаться. Каждый из invoke* методов работает по своему алгоритму. Понятно, что для protected метода нужно походить по иерархии, а для приватного это не нужно. Что тут важно: все структуры строятся при загрузке класса и больше не меняются. В Java 7 в JVM добавилась инструкция invokedynamic для поддержки динамических языков. Зачем в JVM поддержка других языков? А потому что JVM - очень развитая и крутая: 🍁 Кроссплатформенность - берёт на себя работу с операционной системой и даёт заняться чистейшим программированием 🍁 Изоляция - можно безопасно запускать несколько приложений 🍁 JIT компиляторы 🍁 Поддержка многопоточности, сборщики мусора, рефлекшн Думаете, у всех такое есть? А вот и нет. Но есть один недостаток. JVM совершенно не приспособлена под динамические языки - JavaScript, Python, Ruby. Все invoke* методы должны заранее знать все классы и типы аргументов. Поэтому в JVM добавили новую инструкцию invokedynamic. А о том, как она работает, поговорим в пятницу.
Lookup pl = MethodHandles.publicLookup();2️⃣ Задаём сигнатуру метода
MethodType mt = MethodType.methodType(String.class, Integer.class, Long.class);Первый аргумент (String.class) - это возвращаемое значение, остальные - входные параметры. 3️⃣ Получаем указатель на метод
MethodHandle mh = all.findVirtual(User.class, "findName", mt);Это ссылка на метод findName в классе User, который принимает на вход два параметра и возвращает String. 4️⃣ Подставляем аргументы и вызываем
String res = mh.invoke(1, 2L);Выбор методов на каждом этапе гораздо шире, но разбирать мы их, конечно, не будем. Где пригодится MethodHandle? Конечно же в библиотеках и фреймворках, которые работают с аннотациями. Но для энтерпрайза у MethodHandle тоже есть микро-кейс: объявление логгера. Ну вы знаете, в классах бизнес-логики часто добавляют логгирование:
LoggerFactory.getLogger(AccountService.class);Эта строка копируется из класса в класс, меняется только имя в скобках. Операция настолько простая, что можно забыть это сделать🙂 Если использовать MethodHandle:
Logger.getLogger(MethodHandles.lookup().lookupClass());то можно спокойно копировать строчку и ничего не менять. В целом основная миссия MethodHandle - работать в паре с invokedynamic. Но об этом чуть позже🙂
fun(s: Integer): boolean { return s<3; }
Легко для понимания, сложно для реализации. Java - ООП язык, всё в JVM заточено под работу объектов и примитивов. Добавить новый тип данных - чудовищно огромная работа. Как функция будет хранится, вызываться и взаимодействовать с другими объектами? Будет ли она работать с дженериками? Тысячи вопросов и сложностей.
Второй вариант: пусть лямбда реализует интерфейс с одним методом:
Predicate p = i -> i<3;Выглядит по-джавовски и отлично впишется в текущую систему. Существующие библиотеки будут работать с лямбдами по умолчанию. Но какой объект будет присвоен переменной p? Часть 2: реализация Здесь тоже два варианта. Первый - сделать внутренний класс с одним методом и создать его экземпляр. ✅ Простая реализация ❌ Придётся компилировать и загружать класс для каждой лямбды, а при каждом вызове создавать новый объект. С учётом того, что лямбды станут основой Stream API и использоваться для обработки данных - решение ужасное и непроизводительное. Второй вариант - компилировать код лямбды в отдельный метод и подставлять в исходный код MethodHandle. Мы рассмотрим MethodHandle на следующей неделе, но если кратко - это указатель на метод. Компилируем исходный пример в:
public boolean lambda(Integer i) { return i<3; }
MethodHandle mh = LDC["lambda"];
И подставляем mh в исходный код:
...stream().filter(mh)...В теории решение отличное, на практике - неподходящее. ❌ Теряется информация о входных и выходных типах, приходится проверять их в рантайме ❌ Библиотечные методы не принимают на вход MethodHandle Как же сделаны лямбды? Об этом поговорим на следующей неделе: ▫️ Сначала вернёмся во времена java 6 и посмотрим как вызываются методы в JVM ▫️ Изучим новую байткод инструкцию java 7 и чуть глубже обсудим MethodHandle После этого говорить про лямбды будет легко и понятно.
Function<Integer,Integer> sum = (a,b) → a+b;В чём преимущество лямбда-выражений? Очевидный ответ - в краткости. Раньше чтобы "передать поведение" нужен был отдельный или анонимный класс, а теперь есть переменная с анонимной функцией. Удобно и понятно. Но у лямбд есть ещё одно интересное свойство. Посмотрим на него на примере обхода коллекций. В далёкие времена обойти коллекцию можно было двумя способами: ▫️С помощью цикла ▫️Через итератор Этот подход называется external iteration или внешний обход. Последовательно идём по элементам коллекции и применяем к ним некоторую логику. С коллекцией общаемся через интерфейс. Главный путь оптимизации - оптимизация логики обработки. Лямбда-выражения поменяли сценарий работы. Теперь мы как бы передаём функцию внутрь структуры данных. Структура данных сама решает, как применить функцию исходя из деталей своей реализации. Теперь уже логика обработки скрыта за функциональным интерфейсом, а структура данных ей пользуется. Это называется internal iteration или внутренний обход. Итого у нас разрыв шаблона. Двадцать лет обход коллекций шёл по сценарию 🔸Логика обработки - ведущий игрок, все оптимизации здесь 🔸Структура данных - абстракция, скрыта за интерфейсом С появлением лямбда выражений и Stream API субъект и объект поменялись местами: 🔹Логика обработки доступна через интерфейс 🔹Структура данных - главная, оптимизации происходят на этом уровне Теперь работу над коллекцией легко делить между потоками. Раньше это было невозможно - логика обработки могла быть любой и параллелить в каждом случае нужно по-разному. Деление на подзадачи на уровне структуры данных гораздо проще, и теперь у нас есть библиотечный метод parallel() в Stream API. Понятно, что такой подход применим не только к обходу элементов в коллекции, но и к взаимодействию любых компонентов в целом. Смена ролей часто приводит к свежему взгляду на привычные действия. И в жизни, и в написании кода🙂
Iterator it=list.iterator(); while(it.hasNext()) int result = it.next();Метод next возвращает текущий элемент и сдвигает указатель на следующий. Метод hasNext проверяет, ссылается ли этот указатель куда-нибудь. Этот паттерн повторяется снова и снова и называется Iteration. Итератор лежит в основе синтаксиса for (T e: collection) 🔸Самое важное: указатель на следующий элемент вычисляется заранее. Возможный минус: если мы удалили или поменяли элемент, который должен вывестись следующим, то итератор не подхватит изменений и выведет старое значение. Что по задаче: Обход через for использует итератор. Указатель на следующий элемент вычисляется заранее. Более подходящий новый элемент не отображается, и выводится 2 элемента. Метод forEach использует траверс и вычисляет следующий элемент только когда он запрашивается. Поэтому новый ключ подхватывается, и в консоль попадают 3 элемента. ❓Почему нельзя использовать траверс по умолчанию? — Итератор проще и работает быстрее, а условия для пропуска элемента при обходе встречаются редко. ❓Зачем нужно несколько вариантов? — ConcurrentHashMap может перестраиваться во время обхода. Чтобы во время перестройки не выводить дубликаты, используется траверс со сложной логикой. Итого: при выводе элементов ConcurrentHashMap через for и forEach используются разные алгоритмы обхода, поэтому результат вывода тоже разный. ❗️Для однопоточных коллекций между for и forEach нет никакой разницы, в обоих случаях используется итератор.
String src = … String x = src ?: "default"; ▫️Еслиsrc равен null, то
x = "default"
▫️Если src НЕ равен null, то x = src
По сути это сокращённая форма тернарного оператора:
String x = src==null ? src : "default"Есть два оператора, которые похожи на элвис: 🔸 ?. (с точкой) — null-safe member selection operator
str?.toString()= если
str не равен null, то вызвать метод toString()
🔸 ?[] — null-safe array access
lines?[6]= если массив
lines существует, то прочитать 6 элемент
Все операторы неплохо соединяются между собой:
String aMember = g?.members?[0]?.name ?: "nobody";Но элвис оператор это только ?: В java таких конструкций нет. Иногда это используют как аргумент того, что джава неудобная и несовременная. На самом деле элвис оператор мог появиться уже в java 7. Пользователи java 5 называли элвис самой ожидаемой фичей. Но он не попал в итоговый релиз. Давайте разберём, почему в java элвис оператор так и не появился, а в котлине, другом JVM языке, он есть. Часть 2: зачем нужен null Цель элвис оператора — облегчить работу с null. Значение null в джава коде выступает в двух ролях: 1️⃣ Ожидаемое значение и часть бизнес-логики Поле необязательное или ещё не готово. Разработчик знает об этом заранее и учитывает в коде. Элвис оператор прекрасно подходит для этого сценария. 2️⃣ Индикатор ошибки Что-то пошло не так: пропущен обязательный параметр, соединение с БД оборвалось, инициализация не завершилась. Получаем NullPointerException, разбираемся с причинами и устраняем ошибку. Здесь элвис оператор замаскирует проблему и сделает только хуже. Ошибка прикроется значением по умолчанию и пройдёт мимо нас. Часть 3: Kotlin Котлин использует другой подход: по умолчанию переменные НЕ могут быть null. Свойство nullability задаётся явно — через вопросик:
var str: String? = "abc"Все null значения под чётким контролем. Поэтому элвис оператор не скрывает ошибок, а помогает писать лаконичный код. Захотели nullability — чётко это обозначили. Хотим избавиться от этого свойства — используем элвис оператор. Часть 4: Optional В java 8 появился способ явно указать, что значение может быть пустым — класс Optional. Метод orElse выполняет функции элвис оператора: если внутри Optional ничего нет, то возвращается значение внутри orElse. Итог Во времена java 5 ситуация была сложной: null мог означать и ошибку, и нормальное значение. Чтобы не провоцировать скрытые ошибки, комьюнити отклонило добавление элвис оператора. После появления Optional нулл в коде почти всегда указывает на ошибку. Использовать элвис оператор в такой ситуации - нецелесообразно, поэтому маловероятно, что он появится в будущих версиях java.
Available now! Telegram Research 2025 — the year's key insights 
